diff --git a/db/index.js b/db/index.js
index c94bc2d..c613666 100644
--- a/db/index.js
+++ b/db/index.js
@@ -1,11 +1,15 @@
import {
addDoc,
+ and,
collection,
+ deleteDoc,
doc,
getDoc,
getDocs,
getFirestore,
limit,
+ or,
+ orderBy,
query,
setDoc,
where,
@@ -16,6 +20,7 @@ import {
getCachedMessages,
queueSetCashedMessages,
} from "../src/functions/messaging/cachedMessages";
+import { encryptMessage } from "../src/functions/encodingAndDecoding";
export async function addDataToCollection(dataObject, collectionName, uuid) {
try {
@@ -291,16 +296,22 @@ export async function updateMessage({
fromPubKey,
toPubKey,
onlySaveToLocal,
+ retrivedContact,
+ privateKey,
+ currentTime,
}) {
try {
const messagesRef = collection(db, "contactMessages");
const timestamp = new Date().getTime();
+ const useEncription = retrivedContact.isUsingEncriptedMessaging;
- const message = {
+ let message = {
fromPubKey,
toPubKey,
message: newMessage,
timestamp,
+ serverTimestamp: currentTime,
+ isGiftCard: !!newMessage?.giftCardInfo,
};
if (onlySaveToLocal) {
@@ -310,6 +321,14 @@ export async function updateMessage({
});
return true;
}
+ if (useEncription) {
+ let messgae =
+ typeof message.message === "string"
+ ? message.message
+ : JSON.stringify(message.message);
+ const encripted = await encryptMessage(privateKey, toPubKey, messgae);
+ message.message = encripted;
+ }
await addDoc(messagesRef, message);
console.log("New message was published:", message);
@@ -320,51 +339,224 @@ export async function updateMessage({
}
}
-export async function syncDatabasePayment(
- myPubKey,
- updatedCachedMessagesStateFunction
-) {
+export async function syncDatabasePayment(myPubKey, privateKey) {
try {
const cachedConversations = await getCachedMessages();
const savedMillis = cachedConversations.lastMessageTimestamp;
console.log("Retrieving docs from timestamp:", savedMillis);
const messagesRef = collection(db, "contactMessages");
- const receivedMessagesQuery = query(
- messagesRef,
- where("toPubKey", "==", myPubKey),
- where("timestamp", ">", savedMillis)
- );
-
- const sentMessagesQuery = query(
+ const combinedQuery = query(
messagesRef,
- where("fromPubKey", "==", myPubKey),
- where("timestamp", ">", savedMillis)
+ and(
+ where("timestamp", ">", savedMillis),
+ or(
+ where("toPubKey", "==", myPubKey),
+ where("fromPubKey", "==", myPubKey)
+ )
+ ),
+ orderBy("timestamp")
);
- const [receivedSnapshot, sentSnapshot] = await Promise.all([
- getDocs(receivedMessagesQuery),
- getDocs(sentMessagesQuery),
- ]);
-
- const receivedMessages = receivedSnapshot.docs.map((doc) => doc.data());
- const sentMessages = sentSnapshot.docs.map((doc) => doc.data());
- const allMessages = [...receivedMessages, ...sentMessages];
-
- if (allMessages.length === 0) {
- updatedCachedMessagesStateFunction();
- return;
- }
+ const snapshot = await getDocs(combinedQuery);
+ const allMessages = snapshot.docs.map((doc) => doc.data());
+ if (allMessages.length === 0) return [];
+ console.log(allMessages);
console.log(`${allMessages.length} messages received from history`);
- queueSetCashedMessages({
- newMessagesList: allMessages,
+ const processedMessages = await processWithRAF(
+ allMessages,
myPubKey,
- });
+ privateKey
+ );
+
+ return processedMessages;
} catch (err) {
console.error("Error syncing database payments:", err);
// Consider adding error handling callback if needed
- updatedCachedMessagesStateFunction();
+ return [];
+ }
+}
+
+function processWithRAF(allMessages, myPubKey, privateKey, onProgress) {
+ return new Promise((resolve, reject) => {
+ // Create worker
+ const worker = new Worker(
+ new URL("../src/workers/messageWorker.js", import.meta.url),
+ { type: "module" }
+ );
+
+ worker.onmessage = function (e) {
+ const { type, data, current, total } = e.data;
+
+ if (type === "PROGRESS") {
+ console.log(`Processing: ${current}/${total}`);
+ if (onProgress) {
+ onProgress(current, total);
+ }
+ } else if (type === "COMPLETE") {
+ worker.terminate();
+ resolve(data);
+ }
+ };
+
+ worker.onerror = function (error) {
+ console.error("Worker error:", error);
+ worker.terminate();
+ reject(error);
+ };
+
+ // Send data to worker
+ worker.postMessage({
+ type: "PROCESS_MESSAGES",
+ data: { allMessages, myPubKey, privateKey },
+ });
+ });
+}
+
+export async function isValidNip5Name(wantedName) {
+ try {
+ const usersRef = collection(db, "nip5Verification");
+ const q = query(
+ usersRef,
+ where("nameLower", "==", wantedName.toLowerCase())
+ );
+ const querySnapshot = await getDocs(q);
+ return querySnapshot.empty;
+ } catch (error) {
+ console.error("Error checking unique name:", error);
+ return false;
+ }
+}
+export async function addNip5toCollection(dataObject, uuid) {
+ try {
+ if (!uuid) throw Error("Not authenticated");
+
+ const db = getFirestore();
+ const docRef = doc(db, "nip5Verification", uuid);
+
+ await setDoc(docRef, dataObject, { merge: true });
+
+ return true;
+ } catch (e) {
+ console.error("Error adding document: ", e);
+ return false;
+ }
+}
+export async function deleteNip5FromCollection(uuid) {
+ try {
+ if (!uuid) throw Error("Not authenticated");
+
+ const db = getFirestore();
+ const docRef = doc(db, "nip5Verification", uuid);
+
+ await deleteDoc(docRef);
+
+ console.log("Document deleted");
+ return true;
+ } catch (e) {
+ console.error("Error deleting document", e);
+ return false;
+ }
+}
+
+export async function addGiftToDatabase(dataObject) {
+ try {
+ const db = getFirestore();
+ const docRef = doc(db, "blitzGifts", dataObject.uuid);
+
+ await setDoc(docRef, dataObject, { merge: false });
+
+ console.log("Gift added to database with ID: ", dataObject.uuid);
+ return true;
+ } catch (e) {
+ console.error("Error adding gift to database: ", e);
+ return false;
+ }
+}
+
+export async function updateGiftInDatabase(dataObject) {
+ try {
+ const db = getFirestore();
+ const docRef = doc(db, "blitzGifts", dataObject.uuid);
+
+ await setDoc(docRef, dataObject, { merge: true });
+
+ console.log("Gift updated with ID: ", dataObject.uuid);
+ return true;
+ } catch (e) {
+ console.error("Error adding gift to database: ", e);
+ return false;
+ }
+}
+
+export async function getGiftCard(cardUUID) {
+ try {
+ const db = getFirestore();
+ const docRef = doc(db, "blitzGifts", cardUUID);
+
+ const docSnap = await getDoc(docRef);
+
+ if (docSnap.exists()) {
+ const userData = docSnap.data();
+ return userData;
+ }
+ } catch (e) {
+ console.error("Error adding gift to database: ", e);
+ return false;
+ }
+}
+
+export async function deleteGift(uuid) {
+ try {
+ const db = getFirestore();
+ const docRef = doc(db, "blitzGifts", uuid);
+
+ await deleteDoc(docRef);
+
+ console.log("Gift deleted:", uuid);
+ return true;
+ } catch (e) {
+ console.error("Error deleting gift:", e);
+ return false;
+ }
+}
+
+export async function handleGiftCheck(cardUUID) {
+ try {
+ const db = getFirestore();
+ const docRef = doc(db, "blitzGifts", cardUUID);
+
+ const docSnap = await getDoc(docRef);
+
+ if (docSnap.exists()) return { didWork: true, wasClaimed: false };
+ else return { didWork: true, wasClaimed: true };
+ } catch (e) {
+ console.error("Error adding gift to database: ", e);
+ return { didWork: false };
+ }
+}
+
+export async function reloadGiftsOnDomesday(uuid) {
+ try {
+ const db = getFirestore();
+
+ const q = query(
+ collection(db, "blitzGifts"),
+ where("createdBy", "==", uuid)
+ );
+
+ const snapshot = await getDocs(q);
+
+ const results = snapshot.docs.map((doc) => ({
+ id: doc.id,
+ ...doc.data(),
+ }));
+
+ return results;
+ } catch (e) {
+ console.error("Error fetching gifts by creator:", e);
+ return [];
}
}
diff --git a/db/initializeFirebase.js b/db/initializeFirebase.js
index 352867d..6de0c4a 100644
--- a/db/initializeFirebase.js
+++ b/db/initializeFirebase.js
@@ -24,40 +24,67 @@ export const db = getFirestore(app);
export const storage = getStorage(app);
export const functions = getFunctions(app);
+let initializationPromise = null;
+let lastInitializedKey = null;
+
export async function initializeFirebase(publicKey, privateKey) {
+ const cacheKey = publicKey;
try {
- // Initialize App Check first
- // Sign in anonymously;
- if (import.meta.env.MODE === "development") {
- connectFunctionsEmulator(functions, "localhost", 5001);
+ if (initializationPromise && lastInitializedKey === cacheKey) {
+ console.log("Reusing existing initialization promise");
+ return initializationPromise;
}
- const currentUser = auth.currentUser;
- console.log("current auth", {
- currentUser,
- publicKey,
- });
-
- if (currentUser && currentUser?.uid === publicKey) {
- return currentUser;
+ if (lastInitializedKey !== cacheKey) {
+ initializationPromise = null;
}
- await signInAnonymously(auth);
- const isSignedIn = auth.currentUser;
- console.log(isSignedIn.uid, "signed in");
- const token = await fetchBackend(
- "customToken",
- { userAuth: isSignedIn?.uid },
- privateKey,
- publicKey
- );
- if (!token) throw new Error("Not able to get custom token from backend");
- console.log("custom sign in token from backend", token);
- await auth.signOut();
+ lastInitializedKey = cacheKey;
+
+ initializationPromise = (async () => {
+ try {
+ // Initialize App Check first
+ // Sign in anonymously;
+ if (import.meta.env.MODE === "development") {
+ connectFunctionsEmulator(functions, "localhost", 5001);
+ }
+
+ const currentUser = auth.currentUser;
+ console.log("current auth", {
+ currentUser,
+ publicKey,
+ });
+
+ if (currentUser && currentUser?.uid === publicKey) {
+ return currentUser;
+ }
+
+ await signInAnonymously(auth);
+ const isSignedIn = auth.currentUser;
+ console.log(isSignedIn.uid, "signed in");
+ const token = await fetchBackend(
+ "customToken",
+ { userAuth: isSignedIn?.uid },
+ privateKey,
+ publicKey
+ );
+ if (!token)
+ throw new Error("Not able to get custom token from backend");
+ console.log("custom sign in token from backend", token);
+ await auth.signOut();
- const customSignIn = await signInWithCustomToken(auth, token);
- console.log("custom sign in user id", customSignIn.user);
- return customSignIn;
+ const customSignIn = await signInWithCustomToken(auth, token);
+ console.log("custom sign in user id", customSignIn.user);
+ return customSignIn;
+ } catch (error) {
+ console.error("Error initializing Firebase:", error);
+ // Clear the cache on error so the next call can retry
+ initializationPromise = null;
+ lastInitializedKey = null;
+ throw new Error(String(error.message));
+ }
+ })();
+ return initializationPromise;
} catch (error) {
console.error("Error initializing Firebase:", error);
throw new Error(String(error.message));
diff --git a/db/interactionManager.js b/db/interactionManager.js
index 74bc727..5b74a1c 100644
--- a/db/interactionManager.js
+++ b/db/interactionManager.js
@@ -30,11 +30,14 @@ const PRESET_LOCAL_DATA = {
lastUpdated: new Date().getTime(),
data: null,
},
- enabledDeveloperSupport: {
- isEnabled: true,
- baseFee: BLITZ_FEE_SATS,
- baseFeePercent: BLITZ_FEE_PERCET,
- },
+ didViewNWCMessage: false,
+ userSelectedLanguage: "en",
+ // [NWC_IDENTITY_PUB_KEY]: "",
+ userBalanceDenomination: "sats",
+ didViewSeedPhrase: false,
+ enabledBTKNTokens: null,
+ defaultSpendToken: "bitcoin",
+ thousandsSeperator: "space",
};
async function sendDataToDB(newObject, uuid) {
diff --git a/db/photoStorage.js b/db/photoStorage.js
index b0760e3..360df67 100644
--- a/db/photoStorage.js
+++ b/db/photoStorage.js
@@ -1,30 +1,44 @@
-import { getStorage } from "firebase/storage";
+import {
+ deleteObject,
+ getDownloadURL,
+ getStorage,
+ ref,
+ uploadBytes,
+} from "firebase/storage";
import { BLITZ_PROFILE_IMG_STORAGE_REF } from "../src/constants";
+import { storage } from "./initializeFirebase";
-export async function setDatabaseIMG(publicKey, imgURL) {
+export async function setDatabaseIMG(publicKey, imgBlob) {
try {
- const reference = getStorage().ref(
+ if (!(imgBlob instanceof Blob)) {
+ throw new Error("Expected a Blob object");
+ }
+
+ const reference = ref(
+ storage,
`${BLITZ_PROFILE_IMG_STORAGE_REF}/${publicKey}.jpg`
);
- await reference.putFile(imgURL.uri);
+ await uploadBytes(reference, imgBlob);
- const downloadURL = await reference.getDownloadURL();
+ const downloadURL = await getDownloadURL(reference);
return downloadURL;
} catch (err) {
console.log("set database image error", err);
return false;
}
}
+
export async function deleteDatabaseImage(publicKey) {
try {
- const reference = getStorage().ref(
+ const reference = ref(
+ storage,
`${BLITZ_PROFILE_IMG_STORAGE_REF}/${publicKey}.jpg`
);
- await reference.delete();
+ await deleteObject(reference);
return true;
} catch (err) {
- console.log("delete profime imgage error", err);
+ console.log("delete profile image error", err);
if (err.message.includes("No object exists at the desired reference")) {
return true;
}
diff --git a/locales/en/translation.json b/locales/en/translation.json
index 9bc3c5f..5d40733 100644
--- a/locales/en/translation.json
+++ b/locales/en/translation.json
@@ -71,7 +71,6 @@
"hide": "Hide",
"show": "Show",
"understandText": "I understand",
- "retyr": "Retry",
"reportError": "Report Error",
"address": "Address",
"string": "String",
@@ -103,17 +102,33 @@
"selectOption": "Select an option",
"slideToConfirm": "Slide to confirm",
"token": "Token",
- "veriable": "Veriable",
+ "veriable": "Variable",
"instant": "Instant",
"andLower": "and",
"onLower": "on",
- "offLower": "off"
+ "offLower": "off",
+ "recover": "Recover",
+ "search": "Search",
+ "review": "Review",
+ "loadMore": "Load More",
+ "invoice": "Invoice",
+ "annonName": "Anonymous User",
+ "claim": "Claim",
+ "manage": "Manage",
+ "reclaim": "Expired",
+ "optionalFlag": "(Optional)",
+ "gift": "Gift"
},
"languages": {
"english": "English",
"spanish": "Español",
- "italian": "Italiano"
+ "italian": "Italiano",
+ "portuguese_br": "Português (Brasil)",
+ "german": "Deutsch",
+ "french": "Française",
+ "swedish": "Svenska",
+ "russian": "русский"
},
"weekdays": {
@@ -178,7 +193,7 @@
"date": "Date",
"time": "Time",
"memo": "Memo",
- "roostockSwap": "Rootstock to Spark Swap"
+ "roostockSwap": "Rootstock to Spark Transfer"
},
"createAccount": {
@@ -194,7 +209,7 @@
},
"disclaimerPage": {
"header": "Self-custodial",
- "subHeader": "Blitz cannot access your funds or help recover them if lost. By continuing, you agree to Blitz Wallet's terms and conditions.",
+ "subHeader": "Blitz cannot access your funds or help recover them if lost. By continuing, you agree to Blitz Wallet's Terms and Conditions.",
"imgCaption": "'With great power comes great responsibility' - Uncle Ben",
"acceptPrefix": "I accept the ",
"terms&Conditions": "Terms and Conditions",
@@ -202,18 +217,18 @@
"continueBTN": "Next"
},
"verifyKeyPage": {
- "header": "Let's confirm your backup!"
+ "header": "Let's confirm your seed phrase!"
},
"keySetup": {
"generateKey": {
"header": "This is your password to your money, if you lose it you lose your money!",
- "subHeader": "Write it down with pen and paper and keep it safe!",
+ "subHeader": "Write it down with a pen and paper and keep it safe!",
"disclaimer": "WE CAN NOT HELP YOU IF YOU LOSE IT",
- "errorText": "Error Fetching recovery phrase",
- "keyGenError": "Not able to generate valid seed",
+ "errorText": "Error fetching seed phrase",
+ "keyGenError": "Unable to generate a valid seed phrase",
"seedPrivacyMessage": "Make sure no one is around who can see your screen.",
"showIt": "Show it",
- "invalidSeedError": "Not able to generate valid seed"
+ "invalidSeedError": "Unable to generate a valid seed phrase"
},
"pin": {
"savePinError": "Unable to save pin",
@@ -226,16 +241,16 @@
},
"restoreWallet": {
"home": {
- "header": "Enter your recovery phrase",
+ "header": "Enter your seed phrase",
"continueBTN": "Restore wallet",
- "error1": "Please enter all of your keys.",
- "error2": "This is not a valid mnemonic.",
+ "error1": "Please enter all of your seed phrase words.",
+ "error2": "This is not a valid seed phrase.",
"error3": "Your words are not the same as the generated words",
- "noSeedInString": "Unable to find seed in string",
- "invalidWordLen": "Not every word is of valid length",
- "cannotFind12Words": "Unable to find 12 words from copied recovery phrase.",
- "noSeedInNumberArray": "Unable to find seed in number array",
- "noSeedInQr": "Unable to get seed from QR code",
+ "noSeedInString": "Unable to find a seed phrase in the string",
+ "invalidWordLen": "Not all words are of valid length",
+ "cannotFind12Words": "Unable to find 12 words from copied seed phrase.",
+ "noSeedInNumberArray": "Unable to find a seed phrase in number array",
+ "noSeedInQr": "Unable to get a seed phrase from QR code",
"scanQr": "Scan QR"
}
},
@@ -248,17 +263,18 @@
"adminLogin": {
"pinPage": {
- "isBiometricEnabledConfirmAction": "Since biometric setting are enabled you cannot use the default pin login method. Would you like to terminate your account?",
+ "isBiometricEnabledConfirmAction": "We couldn’t verify your biometrics.\n\nDo you want to delete all wallet data? Only continue if your seed phrase is securely backed up.",
"wrongPinError": "Wrong PIN, try again",
"enterPinMessage": "Enter PIN",
- "attemptsText": "{{attempts}} attempts left"
+ "attemptsText": "{{attempts}} attempts left",
+ "biometricsHeader": "Open with Biometrics"
}
},
"apps": {
"appList": {
"AI": "AI",
- "AIDescription": "Chat with the latest generative Ai models",
+ "AIDescription": "Chat with the latest generative AI models",
"SMS": "SMS",
"SMSDescription": "Send and receive SMS messages worldwide using Bitcoin",
"VPN": "VPN",
@@ -266,11 +282,11 @@
"onlineListings": "Shops",
"onlineListingsDescription": "Directory of online businesses accepting Bitcoin payments",
"Soon": "Soon",
- "SoonDescription": "More apps coming soon"
+ "SoonDescription": "More integrations coming soon"
},
"VPN": {
"VPNPlanPage": {
- "backupLoadingMessage": "Retriving invoice",
+ "backupLoadingMessage": "Retrieving invoice",
"countrySearchPlaceholder": "Search by country",
"createVPNBTN": "Buy VPN",
"noLocationError": "Please select a country for the VPN to be located in.",
@@ -278,8 +294,8 @@
"backupPaymentError": "Error paying invoice.",
"createInvoiceError": "Error creating invoice.",
"payingInvoiveError": "Error paying invoice.",
- "runningTries": "Running {{runCount}} for {{maxTries}} tries",
- "configError": "Not able to get VPN config, please reach out to LNVPN.",
+ "runningTries": "Attempt {{runCount}} of {{maxTries}} attempts",
+ "configError": "Unable to get VPN config. Please reach out to LNVPN.",
"paymentMemo": "Store - VPN"
},
"confirmationSlideUp": {
@@ -302,17 +318,17 @@
"30": "year"
},
"historicalPurchasesPage": {
- "deleteVPNConfirmMessage": "Are you sure you want to remove this VPN.",
+ "deleteVPNConfirmMessage": "Are you sure you want to remove this VPN?",
"country": "Country:",
"createdAt": "Created at:",
"duration": "Duration:",
"paymentHash": "Payment Hash:",
"title": "Purchases",
- "retryClaim": "Trying to create file",
- "noPurchases": "You have no VPN configuations",
+ "retryClaim": "Attempting to create file",
+ "noPurchases": "You have no VPN configurations",
"assistanceText": "For assistance, reach out to LNVPN",
"noValidCountryCodeError": "Not able to get valid country code",
- "claimConfigError": "Not able to get VPN config, please reach out to LNVPN.",
+ "claimConfigError": "Unable to get VPN config. Please reach out to LNVPN.",
"contact": "Contact"
},
"home": {
@@ -324,8 +340,8 @@
},
"generatedFile": {
"title": "Wiregurard Config File",
- "iosDownloadInstructions": "When dowloading, click save to files.",
- "androidDownloadInstructions": "When dowloading, you will need to give permission to a location where we can save the config file to."
+ "iosDownloadInstructions": "When downloading, click save to Files.",
+ "androidDownloadInstructions": "When downloading, you will need to give permission for a location to save the config file to."
}
},
"chatGPT": {
@@ -333,15 +349,16 @@
"loadingMessage": "Loading previous chats",
"addCreditsPage": {
"title": "Add Credits",
- "description": "In order to use the latest generative AI models, you must buy credits. Choose an option below to begin.",
+ "description": "In order to use the latest generative AI models, you must purchase credits. Choose an option below to begin.",
"supportedModels": "Supported Models",
- "feeInfo": "Depending on the length of your question and response, the number of searches you get might be different. Blitz adds a 150 sat fee + 0.5% of purchase price onto all purchases.",
+ "feeInfo": "Depending on the length of your question and response, the number of searches you get might differ. Blitz adds a 150 sat fee + 0.5% of purchase price onto all purchases.",
"price": "Price: ",
"estSearches": "Est. searches: {{num}}",
"casualPlanTitle": "Casual Plan",
"proPlanTitle": "Pro Plan",
"powerPlanTitle": "Power Plan",
- "paymentMemo": "Store - chatGPT"
+ "paymentMemo": "Store - chatGPT",
+ "notAvailableMessage": "Unfortunately, due to Apple’s in-app purchase guidelines, this feature is not currently available on iOS. We’re exploring ways to bring it to iOS in the future."
},
"countrySearch": {
"inputPlaceholder": "Search for a country"
@@ -702,7 +719,7 @@
"termsAndConditions4": "Privacy policy"
},
"expandedGiftCardPage": {
- "purchasingCardMessage": "Purchasing gift card, do not leave the page.",
+ "purchasingCardMessage": "Purchasing gift card. Do not leave the page.",
"selectamount": "Select an amount",
"sendingto": "Sending to: ",
"minMaxPurchaseAmount": "You can buy a {{min}} amount of {{max}} {{currency}}",
@@ -712,7 +729,7 @@
"terms": "Terms",
"cardDescription": "Card Description",
"cardTerms": "Card terms",
- "dailyPurchaseAmountError": "You have hit your daily purchase limit",
+ "dailyPurchaseAmountError": "You have reached your daily purchase limit",
"giftMessage": "Add a note to your gift",
"giftMessagePlaceholder": "Type your message here",
"selectForContactBTN": "Select Gift Card",
@@ -735,7 +752,7 @@
"sms4sats": {
"home": {
- "pageDescription": "Send and Receive sms messages without giving away your personal phone number.",
+ "pageDescription": "Send and receive SMS messages without giving away your personal phone number.",
"pricingError": "Unable to get SMS pricing"
},
"confirmationSlideUp": {
@@ -755,8 +772,8 @@
"invalidInputError": "Must have a {{errorTyoe}}",
"invalidCountryError": "Not a valid country",
"payingMessage": "Paying...",
- "runningTries": "Running {{runCount}} for {{maxTries}} tries",
- "notAbleToSettleInvoice": "Not able to settle invoice.",
+ "runningTries": "Attempt {{runCount}} of {{maxTries}} attempts",
+ "notAbleToSettleInvoice": "Unable to settle invoice.",
"paymentMemo": "Store - SMS Send"
},
"sentPayments": {
@@ -772,14 +789,14 @@
"fetchOrderError": "Failed to fetch order status",
"failedUpdate": "Failed to update order status",
"refundedOrder": "Order has been refunded, funds will automatically return to your wallet after 21 minutes",
- "reclaimComplete": "Rety complete. If no code is generated, you will be auto-refunded after 21 minutes.",
+ "reclaimComplete": "Retry complete. If no code is generated, you will be auto-refunded after 21 minutes.",
"noCode": "No code",
"rateLimitError": "Too many requests. You can only make 5 requests every 30 seconds. Please wait before trying again."
},
"receivePage": {
"paymentMemo": "Store - SMS Receive",
"selectCountry": "Select Country",
- "autoSelectInfoMessage": "Choose the country of the phone number you want to use. For the best chance of receiving your code, we recommend keeping it on Auto Select so the system picks the most reliable option.",
+ "autoSelectInfoMessage": "Choose the country of the phone number you want to use. For the best chance of receiving your code, we recommend keeping it on 'Auto Select' so the system picks the most reliable option.",
"autoSelect": "Auto Select",
"inputPlaceholder": "Search for a service",
"loacationLoadingMessage": "Getting location-based services",
@@ -840,7 +857,7 @@
"settingsText": "Go to settings to let Blitz Wallet access your camera",
"noCameraDevice": "You do not have a camera device.",
"noFlash": " Device does not have a torch",
- "sanningResponse": "Only QRcodes are accepted.",
+ "sanningResponse": "Only QR codes are accepted.",
"qrDecodeError": "Error decoding QR code"
},
"confirmClipboard": {
@@ -851,10 +868,10 @@
"see_all_txs": "See all transactions",
"no_transaction_history": "Send or receive a transaction for it to show up here",
"contactsPage": {
- "header": "Select recipient",
+ "header": "Select contact",
"inputTextPlaceholder": "Search username...",
"subHeader": "All Contacts",
- "giftsText": "My Gifts"
+ "giftsText": "Gifts from Contacts"
},
"cameraPage": {
"noCameraAccess": "No access to camera",
@@ -866,23 +883,28 @@
"receivePages": {
"editPaymentInfo": {
"receive_amount": "receive amount",
- "descriptionInputPlaceholder": "Description..."
+ "descriptionInputPlaceholder": "Description...",
+ "editAmount": "Edit amount"
},
"buttonContainer": {
- "format": "Choose Network",
+ "format": "Other receiving methods",
"infoMessage": "Custom amounts aren’t supported on Spark or Rootstock payments."
},
"switchReceiveOptionPage": {
- "title": "Choose Network",
+ "title": "Receiving methods",
"actionBTN": "Show {{action}}",
"sparkWarningMessage": "Receiving directly via Spark will expose your balance to the person paying and is not considered private.",
- "swapWarning": "{{swapType}} payments will be swapped into Spark. Payments below {{amount}} won’t be swapped. Funds will only be swapped after the {{swapType}} payment is confirmed.",
- "unalignedSeeds": "Warning: You’re not using your main wallet account. All Liquid swaps will be sent to your main wallet.",
- "rootstockWarningText": "Rootstock payments will be swapped into Spark. Payments below {{amount}} won’t be swapped. Funds will only be swapped after the Rootsock payment is confirmed.",
+ "swapWarning": "{{swapType}} payments will be received into Spark. Payments below {{amount}} won’t be transferred. Funds will only be transferred after the {{swapType}} payment is confirmed.",
"oneMinute": "~ {{numMins}} minute",
"tenMinutes": "~ {{numMins}} minutes",
- "notUsingMainAccountWarning": "Warning: You’re not using your main wallet account. All {{swapType}} swaps will be sent to your main wallet."
+ "notUsingMainAccountWarning": "Warning: You’re not using your main wallet account. All {{swapType}} transfers will be sent to your main wallet.",
+ "sparkDesc": "Instant, Supports Tokens",
+ "sparkTitle": "Spark Address",
+ "bitcoinTitle": "Bitcoin Address",
+ "lightningTitle": "Lightning Invoice",
+ "liquidTitle": "Liquid Address",
+ "rootstockTitle": "Rootstock Address"
},
"editLNURLContact": {
"informationMessage": "Changing your username updates how others find you in Blitz Contacts and your Lightning address (username @blitzwalletapp.com).\n\nA Lightning address lets others easily send you Bitcoin. Just share your address (username@blitzwalletapp.com) to get paid.",
@@ -894,26 +916,30 @@
"initialLoadingMessage": "Preparing invoice details",
"alreadyPaidInvoiceError": "You have already paid this invoice",
"descriptionPlaceholder": "Payment description...",
- "fallbackErrorMessage": "Error decoding invoice"
+ "fallbackErrorMessage": "Error decoding invoice",
+ "connectToSparkMessage": "Connecting to your wallet.",
+ "switchTokenText": "Use Different Token"
},
"handlingAddressErrors": {
- "invlidFormat": "Addresses should be text only. Please check and try again.",
- "parseError": "We couldn’t recognize the payment type. Please enter a valid Spark, on-chain, LNURL, or BOLT11 invoice.",
+ "invlidFormat": "Invalid format: Addresses should be text only. Please check and try again.",
+ "parseError": "We couldn’t recognize the payment type. Please enter a valid Spark address, Bitcoin address, or a Lightning address/invoice.",
"paymentProcessingError": "Error processing payment info",
"tooLowSendingAmount": "The sending amount is too high to cover the payment and fees. Maximum send amount is {{amount}}",
"processInputError": "Unable to process input",
"unkonwDecodeError": "Unknown decoding error occurred",
"invalidInputType": "Not a valid address type",
- "expiredLightningInvoice": "This lightning invoice has expired",
+ "expiredLightningInvoice": "This Lightning invoice has expired",
"lnurlAuthStartMeessage": "Starting LNURL auth",
"lnurlFailedAuthMessage": "Failed to authenticate LNURL",
"lnurlConfirmMessage": "LNURL successfully authenticated",
- "lnurlPayInvoiceError": "Unable to retrive invoice from LNURL, please try again.",
- "lnurlWithdrawlStart": "Generating invoice for withdrawl",
- "lnurlWithdrawlInvoiceError": "Unable to generate invoice for lnurl withdrawl",
+ "lnurlPayInvoiceError": "Unable to retrieve invoice from LNURL. Please try again.",
+ "lnurlWithdrawlStart": "Generating invoice for withdrawal",
+ "lnurlWithdrawlInvoiceError": "Unable to generate invoice for LNURL withdrawal",
"waitingForLnurlWithdrawl": "Waiting for payment...",
"lnurlWithdrawlSuccess": "Withdrawal successful! Your payment is on its way.",
- "invoiceDetails": "Loading details"
+ "invoiceDetails": "Loading details",
+ "timeoutError": "The process took too long to complete. Please try again.",
+ "payingToSameAddress": "Sending to your own {{addressType}} address is not allowed."
},
"errorScreen": {
@@ -938,7 +964,7 @@
"acceptButton": {
"noSendAmountError": "Please enter a send amount",
"liquidError": "{{overFlowType}} send amount {{amount}}",
- "onchainError": "Minimum on-chain send amount is {{amount}}",
+ "onchainError": "Minimum payment to a Bitcoin address is {{amount}}",
"lnurlPayError": "{{overFlowType}} send amount {{amount}}",
"lrc20FeeError": "You need {{amount}} to send tokens. Your balance is {{balance}}. Please receive some Bitcoin first.",
"balanceError": "Not enough funds to cover sending amount and fees"
@@ -962,11 +988,17 @@
},
"manualEnterSendAddress": {
"title": "Enter in destination",
- "paymentTypesDesc": "Blitz wallet can send to spark, on-chain, LNURL and BOLT 11 addresses"
+ "paymentTypesDesc": "Blitz Wallet can send to Spark addresses, Bitcoin addresses, and Lightning addresses/invoices"
},
"sparkErrorScreen": {
"connectionError": "We’re having trouble connecting to your wallet. Please let us know about this issue.\n\nRemember, your seed phrase keeps your funds safe.",
"retry": "We couldn’t load your wallet right now. Please try again. \n\nYour funds are safe as long as you have your seed phrase."
+ },
+ "backupSeedWarning": {
+ "header": "Backup Your Seed Phrase",
+ "description": "You have money in your wallet, but you haven’t saved your seed phrase. Your seed phrase is a list of words that unlock your wallet. If you lose this device and don’t have it saved, your money could be gone forever.",
+ "backupBTN": "Backup Now",
+ "laterBTN": "I'll do it later"
}
},
"exportTransactions": {
@@ -976,7 +1008,7 @@
"txFees": "Transaction Fees (SAT)",
"amount": "Amount (SAT)",
"sent/received": "Sent/Received",
- "title": "Export your payment history in CSV (comma seperated value) format.",
+ "title": "Export your payment history in CSV (comma separated value) format.",
"paymentsCounter": "{{number}} payments",
"paymentsCounterTracker": "{{number}} of {{total}}",
"loadingMessage": "Loading saved transactions"
@@ -985,12 +1017,16 @@
"contacts": {
"contactsPage": {
+ "contactsHeader": "Contacts",
+ "addContactsText": "Add Contact",
"searchContactsPlaceholder": "Search added contacts",
"noContactsMessage": "You have no contacts",
"editContactProfileMessage": "Edit your profile to begin using contacts.",
"addContactButton": "Add Contact",
"editContactButton": "Edit Profile",
- "unknownSender": "Unknown sender"
+ "unknownSender": "Unknown sender",
+ "noContactSearch": "This contact isn't in your list yet.",
+ "notFound": "not found."
},
"addContactsHalfModal": {
"noContactsMessage": "Not able to find contact.",
@@ -1011,7 +1047,7 @@
"noDescription": "No description"
},
"editMyProfilePage": {
- "navTitle": "Edit Contact Profile",
+ "navTitle": "Edit Profile",
"nameInputDesc": "Name",
"nameInputPlaceholder": "Enter your name",
"lnurlInputDesc": "LNURL Address",
@@ -1021,17 +1057,18 @@
"bioInputDesc": "Bio",
"bioInputPlaceholder": "Enter a short bio",
"addContactBTN": " Add Contact",
- "unqiueNameRegexError": "You can only have letters, numbers, or underscores in your username, and must contain at least 1 letter.",
+ "unqiueNameRegexError": "You can only have letters, numbers, or underscores in your username, and it must contain at least 1 letter.",
"usernameAlreadyExistsError": "This username already exists, please choose another.",
"unableToSaveError": "Unable to save profile image, please try again.",
- "deleteProfileImageError": "Unable to delete profile image, please try again."
+ "deleteProfileImageError": "Unable to delete profile image, please try again.",
+ "deleateWarning": "Are you sure you want to delete this contact? Messages older than a week cannot be restored."
},
"sendAndRequestPage": {
"descriptionPlaceholder": "Payment description",
"profileMessage": "Paying {{name}}",
"contactMessage": "{{name}} paid you",
- "giftCardappVersionError": "You cannot send a gift card to this contact. They have not updated to the latest verison of Blitz Wallet.",
+ "giftCardappVersionError": "You cannot send a gift card to this contact. They have not updated to the latest version of Blitz Wallet.",
"giftCardDescription": "You sent {{name}} a {{giftCardName}} Gift Card",
"cardDetailsError": "We couldn’t load your gift card right now. Please try again.",
"giftCardRange": "Send {{amount}} - {{amount2}}",
@@ -1072,11 +1109,12 @@
"pushNotificationUpdateMessage": "{{name}} {{option}} your request",
"acceptProfileMessage": "Paid {{name}}'s request",
"acceptPayingContactMessage": "{{name}} paid your request",
- "requestTitle": "Received request for "
+ "requestTitle": "Received request for {{amount}}",
+ "send": "Send"
},
"viewAllGiftCards": {
"cardNamePlaceH": "Gift Card",
- "noCardsText": "Your gift cards will appear here once you receive them",
+ "noCardsText": "Your gift cards will appear here when a contact sends you one.",
"header": "Your Gift Cards",
"cardsLenghSingular": "{{num}} card",
"cardsLenghPlurl": "{{num}} cards"
@@ -1084,34 +1122,78 @@
"editGiftCard": {
"header": "What would you like to do with your gift?"
}
+ },
+ "showProfileQr": {
+ "header": "Share Profile",
+ "lnAddress": "Lightning Address",
+ "blitzContact": "Blitz Contact",
+ "copyMessage": "Copied!",
+ "lnurlCopy": "Copy Address",
+ "blitzCopy": "Copy Link",
+ "scanProfile": "Scan Profile"
}
},
"settings": {
+ "index": {
+ "editProfile": "Edit Profile",
+ "showQR": "Show QR",
+ "profileHead": "Profile",
+ "settingsHead": "Settings",
+ "seedPopup": "A seed phrase is a list of words that lets you recover your Bitcoin if your device is lost or damaged. Write it down and store it safely. Never give it to anyone—if someone gets it, they can take all your money."
+ },
"about": {
"header1": "Software",
"text2": "Blitz is a free and open source app under the ",
"text3": "Apache License",
"text4": " Version 2.0",
- "text5": "This is a self-custodial Bitcoin Lightning wallet. Blitz does not have access to your funds, if you lose your backup phrase it may result in a loss of funds.",
+ "text5": "This is a self-custodial Bitcoin Spark wallet compatible with the Lightning Network. Blitz does not have access to your funds, if you lose your seed phrase it may result in a loss of funds.",
"text6": "Blitz uses ",
"text7": "Breez Liquid SDK, ",
"text8": "Spark, ",
"text9": "and ",
"text10": "Boltz API.",
"header2": "Good to know",
- "text12": "Blitz is a powered by the Spark protocol. Spark is an ",
+ "text12": "Blitz is powered by the Spark protocol. Spark is an ",
"text13": "off-chain protocol ",
"text14": "where Spark Operators update the state of Bitcoin ownership off-chain ",
"text15": "allowing for fast, low-fee, and non-custodial transfers without touching the blockchain.",
"text16": "Creator",
"text17": "Version:",
- "learnMore": "Learn More"
+ "learnMore": "Learn More",
+ "webpackVerified": "Webpack Verified",
+ "webpackUnvarified": "Webpack Unverified",
+ "showTechnicals": "Show Technical Details",
+ "hideTechnicals": "Hide Technical Details",
+ "backendHash": "Backend Hash",
+ "expectedHash": "Expected Hash"
},
"language": {
"inputPlaceHolder": ""
},
"displayOptions": {
+ "appearance": "Appearance",
+ "theme": "Theme",
+ "light": "Light",
+ "lightsOut": "Lights out",
+ "dim": "Dim",
+ "balDisplay": "Balance Display",
+ "prev": "Preview",
+ "denomination": "Denomination",
+ "displayForm": "Display Format",
+ "homepage": "Homepage",
+ "txToDisplay": "Transactions to Display",
+ "transactionsLabel_15": "15 Transactions",
+ "transactionsLabel_20": "20 Transactions",
+ "transactionsLabel_25": "25 Transactions",
+ "transactionsLabel_30": "30 Transactions",
+ "transactionsLabel_35": "35 Transactions",
+ "transactionsLabel_40": "40 Transactions",
+ "privFeat": "Privacy & Features",
+ "cameraSwipe": "Swipe for camera",
+ "cameraSwipeDesc": "Swipe right on any main screen to open the camera.",
+ "unkownSenders": "Hide Unknown Senders",
+ "unknownSenderDesc": "Hide transactions from unknown contacts",
"text1": "Dark Mode Style",
"text2": "Lights out",
"text3": "Dim",
@@ -1119,17 +1201,25 @@
"text5": "Current denomination",
"text6": "Please reconnect to the internet to switch your denomination",
"text7": "How to display",
- "text8": "fiat",
+ "fiat": "Fiat",
+ "hidden": "Hidden",
+ "symbol": "Symbol",
+ "word": "Word",
"text9": "Example",
"text10": "Home Screen",
"text11": "Swipe for camera",
"text12": "Swipe right from the main screen—whether you're on Contacts, Wallet, or the Home page—to quickly open the camera for scanning.",
"text13": "Displayed Transactions",
"text14": "Contacts Page",
- "text15": "Hide Unknown Senders"
+ "text15": "Hide Unknown Senders",
+ "sparkTitle": "BTKN Spark Tokens",
+ "sliderTitle_enabled": "Show BTKN tokens",
+ "thousandsSeperator": "Thousands Separator",
+ "space": "Space",
+ "local": "Local Number Format"
},
"fastPay": {
- "text1": "Error adding inputed value, plase try again.",
+ "text1": "Error adding input value. Please try again.",
"text2": "Amount cannot be 0",
"text3": "Enable Fast Pay",
"text4": "Fast pay threshold (SAT)",
@@ -1140,46 +1230,46 @@
"text2": "Security Type",
"text3": "Pin",
"text4": "Biometric",
- "text5": "Device does not have a Biometric profile. Create one in settings to continue.",
+ "text5": "Device does not have a Biometric profile. Create one in Settings to continue.",
"text6": "Error logging in with Biometrics",
"text7": "Device does not support Biometric login",
"unsuccesfullLoginSwitch": "Unable to switch login type",
"toggleSecurityModeError": "Something went wrong while switching your security settings. Please try again.",
"noBiometricsError": "Device does not support Biometric login",
- "noBiometricProfileError": "Device does not have a Biometric profile. Create one in settings to continue.",
+ "noBiometricProfileError": "Device does not have a Biometric profile. Create one in Settings to continue.",
"biometricSignInError": "Error logging in with Biometrics",
"migratingStorageMessgae": "Migrating storage to new security.",
"createPinPageHeader": "{{type}} your Blitz PIN",
- "invalidPinconfirmation": "PIN's do not match. Try again!"
+ "invalidPinconfirmation": "PINs do not match. Try again!",
+ "randomPinKeyboardToggle": "Shuffle Keypad",
+ "randomPinKeyboardInfo": "Enable this feature to shuffle the keypad numbers every time you log in. It adds an extra layer of security by making it harder for anyone nearby to learn your PIN pattern."
},
"seedPhrase": {
- "header": "Keep this phrase in a secure and safe place",
+ "header": "Keep this seed phrase in a secure and safe place",
"headerDesc": "Do not share it with anyone!",
- "qrWarning": "Are you sure you want to show this QR Code?\n\nScanning your seed is convenient, but be sure you're using a secure and trusted device. This helps keep your wallet safe.",
+ "qrWarning": "Are you sure you want to show this QR Code?\n\nScanning your seed phrase is convenient but be sure you're using a secure and trusted device. This helps keep your wallet safe.",
"wordsText": "Words",
"qrText": "QR Code",
- "showSeedWarning": "Are you sure you want to show your recovery phrase?"
+ "showSeedWarning": "Are you sure you want to show your seed phrase?"
},
"crashReporting": {
- "enabled": "Enabled",
- "disabled": "Disabled",
- "crashreporting": "crash reporting",
+ "crashreporting_enabled": "Enabled crash reporting",
+ "crashreporting_disabled": "Disabled crash reporting",
"descriptionText": "Crash data helps us improve the stability and performance of our application.\n\nWhen a crash occurs, the device information that is automatically recorded includes:\n\n• Operating System: OS version, device orientation, and jailbreak status\n• Device Details: Model, orientation, and available RAM\n• Crash Information: Date of the crash and the app version"
},
"accounts": {
- "inputPlaceholder": "Account name",
- "buttonCTA": "Swap Funds"
+ "inputPlaceholder": "Search existing account"
},
"resetWallet": {
"header": "Are you sure?",
"dataDeleteHeader": "Select data to delete from this device.",
- "dataDeleteDesc": "Any option that is selected will be removed forever. If your seed is forgotten, you WILL lose your funds.",
- "seedAndPinOpt": "Delete seed phrase and pin from my device",
- "localData": "Delete locally stored data from my device",
+ "dataDeleteDesc": "This will delete all wallet data from your device. If you don’t have a copy of your seed phrase saved, you WILL lose access to your funds.",
+ "boxNotChecked": "Please check the box to confirm you want to reset your wallet before resetting.",
+ "seedAndPinOpt": "Delete all data from my device",
"balanceText": "Your balance is",
- "localStorageError": "Not able to delete local stored information",
- "secureStorageError": "Not able to delete secure stored information"
+ "localStorageError": "Unable to delete locally stored information",
+ "secureStorageError": "Unable to delete securely stored information"
},
"sparkInfo": {
"title": "Wallet Info",
@@ -1189,48 +1279,52 @@
"sparkLrc20": {
"balanceError": "Your current wallet balance is {{balance}}. Blitz adds a {{fee}} fee to all BTKN payments. Make sure you have some Bitcoin in your wallet to send tokens.",
"title": "Spark Settings",
- "sliderTitle": "{{switchType}} BTKN",
- "sliderDesc": "BTKN is Spark’s native token. Enabling BTKN allows you send and receive tokens on the Spark network.\n\nBlitz is a Bitcoin focused wallet. We do not promote or endorse the use of tokens. This feature exists because we believe users should have the freedom to use the technology they want to use.\n\nBlitz also applies a {{fee}} fee to all token transactions, with 20% of that fee donated to support open-source Bitcoin development."
+ "sliderTitle_enabled": "Enabled BTKN",
+ "sliderTitle_disabled": "Disabled BTKN",
+ "sliderDesc": "BTKN is Spark’s native token. Enabling BTKN allows you to send and receive tokens on the Spark network.\n\nBlitz is a Bitcoin-focused wallet. We do not promote or endorse the use of tokens. This feature exists because we believe users should have the freedom to use the technology they want to use.",
+ "selectTokenHeader": "Default Token",
+ "selectTokenInformationPopup": "Choose which token you’d like to use first when sending payments. Don’t worry—you can still change it on the payment page anytime."
},
"viewSwapsHome": {
- "swaps": "Swaps",
+ "swaps": "Network Transfers",
"selectionTitle": "Choose Network",
"liquid": "Liquid",
"rootstock": "Rootstock",
- "pageTitle": "{{type}} Swaps"
+ "pageTitle": "{{type}} Transfers"
},
"viewRoostockSwaps": {
- "loadingMessage": "Loading saved Roostock swaps",
- "noSwapsMessage": "You have no saved Roostock swaps"
+ "loadingMessage": "Loading saved Roostock transfers",
+ "noSwapsMessage": "You have no saved Roostock transfers"
},
"rootstockSwapInfo": {
"title": "Submarine Swap",
"id": "ID",
- "swapDetails": "Swap Details",
+ "swapDetails": "Transfer Details",
"claimAddress": "Claim Address",
"timeoutBHeight": "Timeout Block Height",
"expAmount": "Expected Amount",
"invoice": "Invoice",
- "refundSwap": "Refund swap"
+ "refundSwap": "Refund transfer"
},
"viewAllLiquidSwaps": {
"rescanComplete": "Rescan complete",
- "swapStartedMessage": "The swap has started. It may take 10–20 seconds for the payment to show up.",
- "balanceError": "Current liquid balance is {{balance}} but the minimum swap amount is {{swapAmount}}",
- "loadingMessage": "Getting liquid info",
+ "swapStartedMessage": "The transfer has started. It may take 10–20 seconds for the payment to show up.",
+ "balanceError": "Current Liquid balance is {{balance}} but the minimum transfer amount is {{swapAmount}}",
+ "loadingMessage": "Getting Liquid info",
"breakdownHead": "Balance Breakdown",
"incoming": "{{amount}} pending incoming",
"outgoing": "{{amount}} pending outgoing",
- "balance": "{{amount}} liquid balance",
+ "balance": "{{amount}} Liquid balance",
"rescan": "Rescan",
"totalBalance": "Total Balance",
- "swapMessage": "Blitz Wallet will try to swap your Liquid funds into Spark when you first load the app or when you receive Liquid payments.\n\nHowever, in some cases a swap might be missed. To move these funds into Spark manually, just click the Swap button below.",
- "swap": "Swap"
+ "swapMessage": "Blitz Wallet will try to transfer your Liquid funds into Spark when you first load the app or when you receive Liquid payments.\n\nHowever, in some cases a transfer might be missed. To move these funds into Spark manually, just click the Transfer button below.",
+ "swap": "Transfer"
},
"notifications": {
- "mainToggle": "{{state}} notifications",
+ "mainToggle_enabled": "Enabled notifications",
+ "mainToggle_disabled": "Disabled notifications",
"mainToggleDesc": "Notifications let you stay informed about important events and updates happening in the app.",
"optionsTitle": "Notification options",
"contact": "Contact",
@@ -1246,13 +1340,13 @@
"nwcDesc": "Connect your Blitz Wallet to apps using Nostr."
},
"fiatCurrency": {
- "title": "Display Currency",
+ "title": "Currency",
"placeholderText": "Search currency",
"saveCurrencyError": "We were unable to save the selected currency, please try again."
},
"feeInformation": {
"title": "",
- "description": "Blitz Wallet offers free and open‑source products for the Bitcoin community. \n\nTo help keep the project sustainable, we’ve added a small transaction fee to each payment.\n\nHere’s how those fees are distributed:",
+ "description": "Blitz Wallet provides free and open-source tools for the Bitcoin community.\n\nTo support the long-term sustainability of the project, a small transaction fee will be applied to each payment.\n\nOnce fees are active, they will be displayed here.",
"upTo": "Up To",
"fixedFee": "Fixed Fee",
"percent": "Percent",
@@ -1266,9 +1360,9 @@
"regexError": "Name can only include letters or numbers.",
"takenNameError": "Name already taken",
"nameConfirmationMessage": "NIP-05 added successfully! Please note that it may take up to 24 hours to appear, as the list is updated once per day.",
- "title": "Nip5 Verification",
- "desc": "Nip5 turns your long Npub into a small email-like address, similar to a lightning address.",
- "dataIsInvalid": "Something went wrong, please try again,",
+ "title": "NIP-05 Verification",
+ "desc": "NIP-05 turns your long Npub into a small, email-like address, similar to a Lightning address.",
+ "dataIsInvalid": "Something went wrong. Please try again.",
"usernameInputLabel": "Username",
"usernameInputPlaceholder": "Satoshi...",
"publicKeyLabel": "Username",
@@ -1277,7 +1371,7 @@
"invalidPubKey": "Invalid pubkey: must be a non-empty string",
"invalidNpub": "Invalid npub format",
"decodeNpubError": "Failed to decode npub",
- "invlaidFormat": "Invalid pubkey format: must be either 64-character hex string or npub"
+ "invlaidFormat": "Invalid pubkey format: must be either a 64-character hex string or npub"
}
},
"nwc": {
@@ -1309,30 +1403,38 @@
"wanringMessage": "To send money from your Nostr Connect wallet, you’ll first need to add funds—either by receiving money or transferring from your main wallet.\n\n You can transfer funds from your main wallet to NWC in the account settings page."
},
"noNotifications": {
- "wanringMessage": "In order to use Nostr Wallet Connect you need to have push notification for Nostr Wallet Connect enabled.\n\nPlease enable push notifications in the settings and try again."
+ "wanringMessage": "In order to use Nostr Wallet Connect you need to have push notifications for Nostr Wallet Connect enabled.\n\nPlease enable push notifications in the settings and try again."
},
"showSeedPage": {
- "loadingMessage": "Generating Mnemonic",
- "wanringMessage": "To keep your wallet safe, Nostr Wallet Connect creates a separate seed phrase from your main wallet's seed.\n\nThis wallet uses the second derivation path and can always be recovered using your main wallet's seed.\n\nIf you're not tech-savvy and unsure about recovering the wallet address, please write down this seed phrase.",
- "viewSeed": "View seed"
+ "loadingMessage": "Generating seed phrase",
+ "wanringMessage": "To keep your wallet safe, Nostr Wallet Connect creates a separate seed phrase from your main wallet's seed phrase.\n\nThis wallet uses the second derivation path and can always be recovered using your main wallet's seed phrase.\n\nIf you're not tech-savvy and unsure about recovering the wallet address, please write down this seed phrase.",
+ "viewSeed": "View seed phrase"
}
},
"accountComponents": {
+ "homepage": {
+ "addNewAccount": "Add",
+ "swap": "Transfer",
+ "viewSeed": "View seed phrase",
+ "editAccount": "Edit account",
+ "swapAccountError": "You need at least two accounts to make a transfer.",
+ "noAccountsFound": "No accounts match your search."
+ },
"accountPaymentPage": {
- "noAmountError": "Please enter an amount to be swapped",
- "noAccountError": "Please select an account for funds to be swapped from",
- "noAccountToError": "Please select an account for funds to be swapped to",
- "loadingFeeError": "Cannot start swap when fee is being calculated",
+ "noAmountError": "Please enter an amount to be transferred",
+ "noAccountError": "Please select an account for funds to be transferred from",
+ "noAccountToError": "Please select an account for funds to be transferred to",
+ "loadingFeeError": "Cannot start transfer when fee is being calculated",
"alreadyStartedTransferError": "A transfer has already been started",
"balanceError": "Sending amount is greater than your balance and fees",
- "noSendAddressError": "Not able to get transfer address",
- "noAccountInformation": "Not able to get account information",
- "title": "Swap",
+ "noSendAddressError": "Unable to get transfer address",
+ "noAccountInformation": "Unable to get account information",
+ "title": "Transfer",
"selectAccount": "Select Account",
- "inputPlaceHolderText": "Account Swap"
+ "inputPlaceHolderText": "Account Transfer"
},
"createAccountPage": {
- "alreadyUsingSeedError": "This seed already exists for another account",
+ "alreadyUsingSeedError": "This seed phrase already exists for another account",
"editTitle": "Edit Account",
"createTitle": "Create Account",
"updateTitle": "Update Account",
@@ -1341,10 +1443,10 @@
"inputDesc": "Account Name",
"nameTakenError": "This name is currently in use. Please use a differnt account name.",
"nameInputPlaceholder": "Name...",
- "seedHeader": "Account Seed"
+ "seedHeader": "Account seed phrase"
},
"viewAccountPage": {
- "informationMessage": "Are you sure you want to show this QR Code?\n\nScanning your seed is convenient, but be sure you're using a secure and trusted device. This helps keep your wallet safe."
+ "informationMessage": "Are you sure you want to show this QR Code?\n\nScanning your seed phrase is convenient, but be sure you're using a secure and trusted device. This helps keep your wallet safe."
}
},
"posPath": {
@@ -1375,10 +1477,10 @@
"updateContactMessage": "The recipient needs to update their Blitz app to get paid.",
"payingMessage": "Paying...",
"notifyngMessage": "Notifiying...",
- "errorPaying": "Unable to pay blitz contact. Try again later.",
+ "errorPaying": "Unable to pay Blitz contact. Try again later.",
"invalidName": "Name is not an LNURL or a Blitz user.",
"removeEmployeeWarning": "Are you sure you want to remove this employee?",
- "startingPaymentProcessMessage": "'Begining payment process, please do not leave this page or the app.",
+ "startingPaymentProcessMessage": "Begining payment process, please do not leave this page or the app.",
"goBack": "Go back",
"lastSale": "Last sale:",
"numTipsPaid": "Tips paid:",
@@ -1390,6 +1492,7 @@
"unpaidtips": "Tips: {{number}}",
"title": "Tips to pay",
"empNamePlaceholder": "Employee name",
+ "noEmployees": "You currently don’t have any employees with saved tips. Once an employee uses the point-of-sale, their tips will appear here so you can pay them.",
"noTips": "Employee has no tips"
},
"items": {
@@ -1438,21 +1541,28 @@
"shopTitle": "Shop with Bitcoin",
"shopDescription": "Buy gift cards from thousands of different merchants around the world",
"callToAction": "Anything you want here?",
- "featuredApps": "Anything you want here?"
+ "featuredApps": "Anything you want here?",
+ "comingSoon": "This feature is temporarily unavailable. We're working to restore it as soon as possible."
},
"confirmTxPage": {
"failedToSend": "Failed to send",
- "confirmMessage": "{{direction}} succesfully",
+ "confirmMessage": "{{direction}} successfully",
"paymentErrorMessage": "There was an issue sending this payment, please try again.",
"sendReport": "Send report to developer",
- "lnurlAuthSuccess": "Wallet authentication successful! You’re now logged in."
+ "lnurlAuthSuccess": "Wallet authentication successful! You’re now logged in.",
+ "emailReport": "Send via Email",
+ "copyReport": "Copy to Clipboard",
+ "confirmMessage_sent": "Sent successfully",
+ "confirmMessage_received": "Received successfully"
},
"expandedTxPage": {
- "confirmMessage": "{{direction}} amount",
+ "confirmMessage_sent": "Sent amount",
+ "confirmMessage_received": "Received amount",
"paymentStatus": "Payment status",
"confReqired": "Confs required",
- "detailsBTN": "Technical details"
+ "detailsBTN": "Technical details",
+ "contactPaymentType": "Contact"
},
"explorePage": {
"timeLeft": "({{time}} left)",
@@ -1465,24 +1575,38 @@
"loadingText": "We're setting things up. Hang tight! This could take up to a minute.",
"dbInitError": "Unable to open the database. Please try again.",
"userSettingsError": "Unable to load user settings. Please try again.",
- "liquidWalletError": "Wallet information was not set properly, please try again.",
- "liquidWalletError2": "We were unable to set up your wallet. Please try again."
+ "liquidWalletError": "Wallet information was not set properly. Please try again.",
+ "liquidWalletError2": "We were unable to set up your wallet. Please try again.",
+ "dbInitError1": "We’re unable to set up your app.",
+ "dbInitError2": "Please try again. If this issue continues, you can safely recover your funds using your seed phrase."
},
"receiveBtcPage": {
- "onchainFeeMessage": "On-chain payments have a network fee and 0.1% Spark fee.\n\nIf you send money to yourself, you’ll pay the network fee twice — once to send it and once to claim it.\n\nIf someone else sends you money, you’ll only pay the network fee once to claim it.",
- "liquidFeeMessage": "Liquid payments need to be swapped into Spark.\n\nThis process includes a lockup fee of about {{fee}}, a claim fee of around {{claimFee}}, and a 0.1% service fee from Boltz based on the amount you’re sending.",
- "rootstockFeeMessage": "Rootstock payments need to be swapped into Spark.\n\nThis process includes a lockup fee of about {{fee}}, and a 0.1% service fee from Boltz based on the amount you’re sending.",
- "generatingInvoice": "Generating Invoice"
+ "onchainFeeMessage": "Payments received to Bitcoin addresses have a network fee and a 0.1% Spark fee.\n\nIf you send money to yourself, you'll pay the network fee twice — once to send it and once to claim it.\n\nIf someone else sends you money, you'll only pay the network fee once to claim it.",
+ "liquidFeeMessage": "Payments received to Liquid addresses need to be transferred into Spark.\n\nThis process includes a lockup fee of about {{fee}}, a claim fee of around {{claimFee}}, and a 0.1% service fee from Boltz based on the amount you're sending.",
+ "rootstockFeeMessage": "Payments received to Rootstock addresses need to be transferred into Spark.\n\nThis process includes a lockup fee of about {{fee}}, and a 0.1% service fee from Boltz based on the amount you're sending.",
+ "generatingInvoice": "Generating Invoice",
+ "amountPlaceholder": "Any amount",
+ "invoiceDescription_lightningInvoice": "Lightning invoice",
+ "invoiceDescription_lightningAddress": "Lightning address",
+ "invoiceDescription_bitcoinAddress": "Bitcoin address",
+ "invoiceDescription_liquidAddress": "Liquid address",
+ "invoiceDescription_sparkAddress": "Spark address",
+ "invoiceDescription_rootstockAddress": "Rootstock address",
+ "header_lightning": "Bitcoin via Lightning",
+ "header_bitcoin": "Bitcoin",
+ "header_spark": "Bitcoin/Tokens via Spark",
+ "header_liquid": "Bitcoin via Liquid",
+ "header_rootstock": "Bitcoin via Rootstock"
},
"settingsContent": {
"about": "About",
"language": "Language",
- "display currency": "Display Currency",
+ "display currency": "Currency",
"display options": "Display Options",
"edit contact profile": "Edit Contact Profile",
- "view all swaps": "Swaps",
+ "view all swaps": "Network Transfers",
"fast pay": "Fast Pay",
"blitz fee details": "Blitz Fee Details",
"crash reports": "Crash Reports",
@@ -1495,6 +1619,7 @@
"spark info": "Spark Info",
"delete wallet": "Delete Wallet",
"general": "General",
+ "preferences": "Preferences",
"security": "Security",
"technical settings": "Technical Settings",
"experimental features": "Experimental Features",
@@ -1504,7 +1629,7 @@
},
"technicalTransactionDetails": {
"txHash": "Transaction Hash",
- "paymentId": "Payment Id",
+ "paymentId": "Payment ID",
"preimage": "Payment Preimage",
"address": "Payment Address",
"bitcoinTxId": "Bitcoin Txid",
@@ -1528,6 +1653,120 @@
"maxSupply": "Max Supply",
"tokenTicker": "Token Ticker",
"tokenPubKey": "Token Public Key"
+ },
+ "giftPages": {
+ "expired": "Expired",
+ "lessThanMin": "Less than a minute left",
+ "timeLeft": {
+ "days_one": "{{count}} day left",
+ "days_other": "{{count}} days left",
+ "hours_one": "{{count}} hour left",
+ "hours_other": "{{count}} hours left",
+ "minutes_one": "{{count}} minute left",
+ "minutes_other": "{{count}} minutes left"
+ },
+ "shareMessage": "You've received a {{icon}}{{amount}} gift! Claim it here:\n{{link}}",
+ "fundGiftMessage": "Creating Gift",
+ "reclaimGiftMessage": "Reclaiming Gift",
+
+ "giftsHome": {
+ "title": "Bitcoin Gifts"
+ },
+
+ "giftsOverview": {
+ "noGiftsHead": "No gifts yet",
+ "noGiftsDesc": "Create your first gift to share Bitcoin with friends and family",
+ "createGift": "Create Gift",
+ "claimed": "Claimed",
+ "expired": "Expired",
+ "unclaimed": "Unclaimed",
+ "reclaimed": "Reclaimed"
+ },
+
+ "createGift": {
+ "noAmountError": "Please enter an amount to create your gift.",
+ "connectError": "We couldn’t set up your gift wallet. Please try again.",
+ "addressError": "We couldn’t get the address for your gift wallet.",
+ "saveError": "We couldn’t save your gift. Please try again.",
+ "fundError": "We couldn’t add funds to your gift.",
+ "startProcess1": "Preparing your gift…",
+ "startProcess2": "Setting things up…",
+ "startProcess3": "Adding funds to your gift…",
+ "header": "Create Gift",
+ "inputPlaceholder": "Add a message or note...",
+ "button": "Create Gift",
+ "disclaimer": "When you create a gift, the money stays locked for 7 days. If no one claims it by then, you can go to the reclaim page to get it back."
+ },
+
+ "giftConfirmation": {
+ "header": "Gift Created!",
+ "whatToDo": "Share this gift with anyone using the QR code or link below",
+ "details": "Gift Details",
+ "expire": "Expires",
+ "giftLink": "Gift Link",
+ "shareGift": "Share Gift",
+ "createAnother": "Create Another",
+ "done": "Done"
+ },
+ "claimHome": {
+ "header": "Enter gift link",
+ "desc": "Paste the gift link you received or scan the QR code to claim your gift",
+ "inputPlaceholder": "Enter gift link",
+ "claim": "Claim"
+ },
+ "reclaimPage": {
+ "header": "Reclaim your gift",
+ "desc": "Enter or choose the gift ID to return an unclaimed, expired gift to your wallet.",
+ "inputPlaceholder": "Enter gift ID",
+ "dropdownPlaceHolder": "Select Expired Gift",
+ "button": "Reclaim Gift",
+ "noReclaimsMessage": "You have no expired gifts right now. Come back after a gift has expired to reclaim it.",
+ "advancedModeBTN": "Advanced Mode"
+ },
+ "advancedMode": {
+ "header": "Advanced Mode",
+ "currentIndexHead": "Current Gift Index",
+ "infoHead": "How Bitcoin Gifts Work",
+ "infoDesc1": "Blitz gifts use separate wallets created from your main wallet. This lets you recover unclaimed funds while keeping your main wallet protected.",
+ "infoDesc2": "Most expired gifts can be reclaimed on the regular reclaim page. If a gift wasn’t fully received or the normal reclaim option doesn’t work, you can use this page to select a specific gift and check if any money is still left.",
+ "inputHead": "Enter Gift Number",
+ "invalidGiftNum": "Please enter a valid gift number between 1 and {{currentGiftIndex}}",
+ "restoreGiftBTN": "Restore Gift",
+ "advancedWarning": "This advanced feature should only be used when the standard reclaim process doesn't work. If you're unsure, try the normal reclaim page first."
+ },
+ "claimPage": {
+ "parseError": "We couldn’t read the gift details from this link.",
+ "noGiftForReclaim": "We couldn’t find this gift. It may have already been claimed.",
+ "expiredOrClaimed": "This gift has already been claimed or has expired.",
+ "noGiftSeed": "We couldn’t reach the gift right now. Please try again later.",
+ "giftWalletInitError": "We were unable to reach the gift. Please try again later.",
+ "noBalanceErrorReclaim": "There are no funds left to reclaim from this gift.",
+ "notExpired": "You cannot reclaim this gift yet.",
+ "nobalanceError": "We couldn’t find an amount for this gift. Please try again later, or try restoring the gift.",
+ "nobalanceErrorExpert": "There’s no amount available to restore for this gift.",
+ "balanceMismatchError": "There was an issue getting the gift amount. Please try again.",
+ "paymentError": "We couldn’t claim the gift. Please try again.",
+ "claimHead": "Accept Gift",
+ "reclaimHead": "Refund Gift",
+ "claimLoading": "Claiming your gift… please stay in the app.",
+ "reclaimLoading": "Returning your funds… please stay in the app.",
+ "claimAmountHead": "Receving amount",
+ "reclaimAmountHead": "Amount Restored",
+ "reclaimAmountHeadExpert": "Gift Number Restored",
+ "networkFeeWarningExpert": "Any amount found, minus network fees, will be added to your balance",
+ "networkFeeWarning": "This amount, minus network fees, will be added to your balance",
+ "buttonTextClaiming": "Claiming",
+ "reclaimButton": "Reclaim Gift",
+ "claimButton": "Accept Gift",
+ "confirmMessage": "Your gift has been claimed, and you will receive your funds shortly",
+ "defaultDesc": "Blitz Gift",
+ "claimingGiftMessage1": "Preparing Gift...",
+ "giftBalanceMessage_0": "Just a moment while we get things ready...",
+ "giftBalanceMessage_1": "We’re gathering your gift information...",
+ "giftBalanceMessage_2": "Almost there—please keep the app open...",
+ "giftBalanceMessage_3": "Finishing up the last steps...",
+ "claimingGiftMessage4": "Claiming Gift..."
+ }
}
}
},
@@ -1550,42 +1789,45 @@
"requestError": "There was an error with the request, please try again.",
"invoiceRetrivalError": "There was an error retrieving the invoice, please try again.",
"payingInvoiceError": "There was an error paying the invoice, please try again.",
- "noMailAppsFoundError": " No mail apps found on your device. Please install a mail app to use this feature.",
- "retriveingPhotoesError": " There was an error retrieving photos, please try again.",
- "clipboardContentError": "No data in clipboard",
+ "noMailAppsFoundError": "No mail apps found on your device. Please install a mail app to use this feature.",
+ "retriveingPhotoesError": "There was an error retrieving photos, please try again.",
+ "clipboardContentError": "No data in the clipboard",
"userDataFetchError": "Unable to get user from database.",
- "updateContactMessageError": "There was a problem updating this message, please try again.",
- "declinePaymentError": "There was a problem declining this payment, please try again.",
- "payingContactError": "We were not able to get the contacts address, please try again.",
- "noUserFoundDeeplinkError": "Not able to find any user for this username.",
+ "updateContactMessageError": "There was a problem updating this message. Please try again.",
+ "declinePaymentError": "There was a problem declining this payment. Please try again.",
+ "payingContactError": "We were unable to get the contact's address. Please try again.",
+ "noUserFoundDeeplinkError": "Unable to find any user for this username.",
"cannotAddSelfError": "You cannot add yourself as a contact.",
- "fullDeeplinkError": "Unable to retrive contact information, please try again.",
+ "fullDeeplinkError": "Unable to retrieve contact information. Please try again.",
"processingDeepLinkError": "Failed to process link: {{error}}",
- "legacyContactError": "Contact has not updated thier wallet yet. Please ask them to update their wallet to send this.",
+ "legacyContactError": "Contact has not updated their wallet yet. Please ask them to update their wallet to send this.",
"contactInvoiceGenerationError": "Error generating invoice. Make sure this is a valid LNURL address.",
- "explorePagenoDataError": "We were unable to retrive Blitz user count.",
+ "explorePagenoDataError": "We were unable to retrieve Blitz user count.",
"savingFileError": "Error saving file to document",
"savingFilePermissionsError": "Error getting permissions",
"writtingFileError": "Error writing file to filesystem",
"createTransactionsFileError": "Unable to create transaction file",
- "noQrInScanError": "Not able to get find QRcode from image.",
- "noDataInQRError": "Not able to get find data from image.",
- "noInvoiceInImageError": "Not able to get invoice from image.",
- "savingImageError": "Unable to save image, pleae try again.",
- "invalidSeedError": "Did not enter a valid seed",
- "invalidSeedWordLengthErorr": "Not every word is of valid length",
- "invalidSeedLengthError": "Unable to find 12 words from copied recovery phrase.",
+ "noQrInScanError": "Unable to find QR code in image.",
+ "noDataInQRError": "Unable to find data in image.",
+ "noInvoiceInImageError": "Unable to get invoice from image.",
+ "savingImageError": "Unable to save image. Please try again.",
+ "invalidSeedError": "Did not enter a valid seed phrase",
+ "invalidSeedWordLengthErorr": "Not all words are of valid length",
+ "invalidSeedLengthError": "Unable to find 12 words from copied seed phrase.",
"noGooglePlay": "Google Play Services are required to receive notifications.",
- "noNotificationPermission": "Blitz doesn’t have notification permissions. Enable them in settings to use notifications.",
+ "noNotificationPermission": "Blitz doesn’t have notification permissions. Enable them in Settings to use notifications.",
"rootstockInvoiceError": "There was a problem generating your Rootstock address. Please try again.",
"liquidInvoiceError": "There was a problem generating your Liquid address. Please try again.",
- "bitcoinInvioceError": "There was a problem generating your Bitcoin address. Please try again.",
+ "bitcoinInvoiceError": "There was a problem generating your Bitcoin address. Please try again.",
"sparkInvioceError": "There was a problem generating your Spark address. Please try again.",
- "lightningInvioceError": "There was a problem generating your Lightning address. Please try again.",
+ "lightningInvoiceError": "There was a problem generating your Lightning invoice. Please try again.",
"sparkConnectionError": "Spark connection failed. Please try again.",
- "giftCardExpiration": "Gift cards can only be recovered for 7 days after they are sent. If this app is deleted or local storage is cleared after that time, your gift cards will be lost. Be sure to save the claim links or gift card codes somewhere safe.",
+ "giftCardExpiration": "Gift cards can only be recovered for 7 days after they are sent. If this app is deleted or local storage is cleared after that time, your gift cards will be lost. Be sure to save the claim links or gift card codes in a safe place.",
"receivedRootstock": "Rootstock payment received — your swap is starting. It may take up to a minute, so please keep the app open.",
- "receivedLiquid": "Liquid payment received — your swap is starting. It may take up to a minute, so please keep the app open."
+ "receivedLiquid": "Liquid payment received — your swap is starting. It may take up to a minute, so please keep the app open.",
+ "invalidData": "The data you entered isn’t valid",
+ "paymentError": "Something went wrong while processing your payment. Please try again. If it still doesn’t work, close the app and reopen it before trying again.",
+ "paymentFeeError": "We couldn’t retrieve the payment fee. Please try again. If the issue continues, close the app and reopen it before trying again."
},
"loadingScreen": {
@@ -1599,9 +1841,12 @@
"depositLabel": "Deposit address payment"
}
},
+ "share": {
+ "contact": "Hi, add me on Blitz!"
+ },
"swapMessages": {
- "liquid": "Liquid to Spark Swap"
+ "liquid": "Liquid to Spark Transfer"
},
"tabs": {
"home": "Wallet",
@@ -1609,6 +1854,10 @@
"contacts": "Contacts"
},
"pushNotifications": {
+ "paymentReceived": {
+ "title": "Payment received",
+ "body": "You received {{totalAmount}}"
+ },
"LNURL": {
"zapped": "You were zapped {{totalAmount}}",
"regular": "You just received {{totalAmount}}"
diff --git a/package-lock.json b/package-lock.json
index 6ab38d8..273a2ef 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,16 +1,16 @@
{
"name": "blitz-web-app",
- "version": "0.1.3",
+ "version": "0.1.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "blitz-web-app",
- "version": "0.1.3",
+ "version": "0.1.4",
"dependencies": {
"@bitcoinerlab/secp256k1": "^1.2.0",
"@breeztech/breez-sdk-liquid": "^0.11.2",
- "@buildonspark/spark-sdk": "^0.4.3",
+ "@buildonspark/spark-sdk": "^0.5.2",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.1.1",
@@ -22,7 +22,7 @@
"@vulpemventures/secp256k1-zkp": "^3.2.1",
"bech32": "^2.0.0",
"bip21": "^3.0.0",
- "bitcoin-address-parser": "^1.0.1",
+ "bitcoin-address-parser": "^1.0.3",
"bitcoinjs-lib": "^6.1.7",
"bolt11": "^1.4.1",
"boltz-core": "^3.0.0",
@@ -32,6 +32,7 @@
"events": "^3.3.0",
"firebase": "^11.9.0",
"framer-motion": "^12.10.0",
+ "i18next": "^25.7.3",
"idb": "^8.0.3",
"intl-pluralrules": "^2.0.1",
"js-sha256": "^0.11.1",
@@ -1591,9 +1592,9 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.27.6",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
- "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -1666,18 +1667,17 @@
"license": "MIT"
},
"node_modules/@bufbuild/protobuf": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.9.0.tgz",
- "integrity": "sha512-rnJenoStJ8nvmt9Gzye8nkYd6V22xUAnu4086ER7h1zJ508vStko4pMvDeQ446ilDTFpV5wnoc5YS7XvMwwMqA==",
+ "version": "2.10.2",
+ "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.2.tgz",
+ "integrity": "sha512-uFsRXwIGyu+r6AMdz+XijIIZJYpoWeYzILt5yZ2d3mCjQrWUTVpVD9WL/jZAbvp+Ed04rOhrsk7FiTcEDseB5A==",
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@buildonspark/spark-sdk": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/@buildonspark/spark-sdk/-/spark-sdk-0.4.3.tgz",
- "integrity": "sha512-CSV3xI5fBKdy4xeGpl2CgYuKX2Qx2+QU0xpjDM+dGGR7OqOffXNboQRW2YL6e7/siGDWx8w+Aboxp+knQokYRA==",
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/@buildonspark/spark-sdk/-/spark-sdk-0.5.2.tgz",
+ "integrity": "sha512-hiXwJRdzOhV4cCJoJTcdMaCPNkJcGp3j7fqpj7O5eAZhJ6i20THGlRZVDeHE/QKPo/gzGkRqC1oD9X6fiTEBdQ==",
"license": "Apache-2.0",
"dependencies": {
- "@bitcoinerlab/secp256k1": "^1.1.1",
"@bufbuild/protobuf": "^2.2.5",
"@lightsparkdev/core": "^1.4.4",
"@noble/curves": "^1.8.0",
@@ -1686,7 +1686,6 @@
"@opentelemetry/context-async-hooks": "^2.0.0",
"@opentelemetry/core": "^2.0.0",
"@opentelemetry/instrumentation": "^0.203.0",
- "@opentelemetry/instrumentation-fetch": "^0.203.0",
"@opentelemetry/instrumentation-undici": "^0.14.0",
"@opentelemetry/sdk-trace-base": "^2.0.0",
"@opentelemetry/sdk-trace-node": "^2.0.1",
@@ -1701,7 +1700,6 @@
"bare-crypto": "^1.9.2",
"bare-fetch": "^2.4.1",
"buffer": "^6.0.3",
- "eciesjs": "^0.4.13",
"eventemitter3": "^5.0.1",
"js-base64": "^3.7.7",
"light-bolt11-decoder": "^3.2.0",
@@ -1710,7 +1708,8 @@
"nice-grpc-common": "^2.0.2",
"nice-grpc-opentelemetry": "^0.1.18",
"nice-grpc-web": "^3.3.7",
- "ts-proto": "^2.6.1",
+ "ts-proto": "2.8.3",
+ "ua-parser-js": "^2.0.6",
"uuidv7": "^1.0.2"
},
"engines": {
@@ -1740,20 +1739,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@ecies/ciphers": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.4.tgz",
- "integrity": "sha512-t+iX+Wf5nRKyNzk8dviW3Ikb/280+aEJAnw9YXvCp2tYGPSkMki+NRY+8aNLmVFv3eNtMdvViPNOPxS8SZNP+w==",
- "license": "MIT",
- "engines": {
- "bun": ">=1",
- "deno": ">=2",
- "node": ">=16"
- },
- "peerDependencies": {
- "@noble/ciphers": "^1.0.0"
- }
- },
"node_modules/@emnapi/runtime": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
@@ -3663,9 +3648,9 @@
}
},
"node_modules/@lightsparkdev/core": {
- "version": "1.4.5",
- "resolved": "https://registry.npmjs.org/@lightsparkdev/core/-/core-1.4.5.tgz",
- "integrity": "sha512-SQo1wNNMfJNFdZPFsbYOylEa6t5X9S+FnKneju9qbvWZz9vfsb6B+MkvVW7DUGoMhisjtPzmR691qedfvstWIg==",
+ "version": "1.4.7",
+ "resolved": "https://registry.npmjs.org/@lightsparkdev/core/-/core-1.4.7.tgz",
+ "integrity": "sha512-/u97sL1IrTsY4Bsk9MsUbqnV5NWv8R6pH1a86E9oadcJumU5TGbWVS1E0ZGIWnqZf0QktHtu4XiBc9OyzzMFMA==",
"license": "Apache-2.0",
"dependencies": {
"dayjs": "^1.11.7",
@@ -3912,18 +3897,6 @@
}
}
},
- "node_modules/@noble/ciphers": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
- "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
- "license": "MIT",
- "engines": {
- "node": "^14.21.3 || >=16"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- }
- },
"node_modules/@noble/curves": {
"version": "1.9.7",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
@@ -3982,9 +3955,9 @@
}
},
"node_modules/@opentelemetry/context-async-hooks": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.1.0.tgz",
- "integrity": "sha512-zOyetmZppnwTyPrt4S7jMfXiSX9yyfF0hxlA8B5oo2TtKl+/RGCy7fi4DrBfIf3lCPrkKsRBWZZD7RFojK7FDg==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.2.0.tgz",
+ "integrity": "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==",
"license": "Apache-2.0",
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -3994,9 +3967,9 @@
}
},
"node_modules/@opentelemetry/core": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.1.0.tgz",
- "integrity": "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
+ "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
@@ -4025,88 +3998,6 @@
"@opentelemetry/api": "^1.3.0"
}
},
- "node_modules/@opentelemetry/instrumentation-fetch": {
- "version": "0.203.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fetch/-/instrumentation-fetch-0.203.0.tgz",
- "integrity": "sha512-Z+mls3rOP2BaVykDZLLZPvchjj9l2oj3dYG1GTnrc27Y8o3biE+5M1b0izblycbbQHXjMPHQCpmjHbLMQuWtBg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "2.0.1",
- "@opentelemetry/instrumentation": "0.203.0",
- "@opentelemetry/sdk-trace-web": "2.0.1",
- "@opentelemetry/semantic-conventions": "^1.29.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-fetch/node_modules/@opentelemetry/core": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz",
- "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/semantic-conventions": "^1.29.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.0.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-fetch/node_modules/@opentelemetry/resources": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz",
- "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "2.0.1",
- "@opentelemetry/semantic-conventions": "^1.29.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.3.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-fetch/node_modules/@opentelemetry/sdk-trace-base": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz",
- "integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "2.0.1",
- "@opentelemetry/resources": "2.0.1",
- "@opentelemetry/semantic-conventions": "^1.29.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.3.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-fetch/node_modules/@opentelemetry/sdk-trace-web": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.0.1.tgz",
- "integrity": "sha512-R4/i0rISvAujG4Zwk3s6ySyrWG+Db3SerZVM4jZ2lEzjrNylF7nRAy1hVvWe8gTbwIxX+6w6ZvZwdtl2C7UQHQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "2.0.1",
- "@opentelemetry/sdk-trace-base": "2.0.1"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.0.0 <1.10.0"
- }
- },
"node_modules/@opentelemetry/instrumentation-undici": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.14.0.tgz",
@@ -4124,12 +4015,12 @@
}
},
"node_modules/@opentelemetry/resources": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.1.0.tgz",
- "integrity": "sha512-1CJjf3LCvoefUOgegxi8h6r4B/wLSzInyhGP2UmIBYNlo4Qk5CZ73e1eEyWmfXvFtm1ybkmfb2DqWvspsYLrWw==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
+ "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
"license": "Apache-2.0",
"dependencies": {
- "@opentelemetry/core": "2.1.0",
+ "@opentelemetry/core": "2.2.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
@@ -4140,13 +4031,13 @@
}
},
"node_modules/@opentelemetry/sdk-trace-base": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.1.0.tgz",
- "integrity": "sha512-uTX9FBlVQm4S2gVQO1sb5qyBLq/FPjbp+tmGoxu4tIgtYGmBYB44+KX/725RFDe30yBSaA9Ml9fqphe1hbUyLQ==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz",
+ "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==",
"license": "Apache-2.0",
"dependencies": {
- "@opentelemetry/core": "2.1.0",
- "@opentelemetry/resources": "2.1.0",
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/resources": "2.2.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
@@ -4157,14 +4048,14 @@
}
},
"node_modules/@opentelemetry/sdk-trace-node": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.1.0.tgz",
- "integrity": "sha512-SvVlBFc/jI96u/mmlKm86n9BbTCbQ35nsPoOohqJX6DXH92K0kTe73zGY5r8xoI1QkjR9PizszVJLzMC966y9Q==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.2.0.tgz",
+ "integrity": "sha512-+OaRja3f0IqGG2kptVeYsrZQK9nKRSpfFrKtRBq4uh6nIB8bTBgaGvYQrQoRrQWQMA5dK5yLhDMDc0dvYvCOIQ==",
"license": "Apache-2.0",
"dependencies": {
- "@opentelemetry/context-async-hooks": "2.1.0",
- "@opentelemetry/core": "2.1.0",
- "@opentelemetry/sdk-trace-base": "2.1.0"
+ "@opentelemetry/context-async-hooks": "2.2.0",
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/sdk-trace-base": "2.2.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -4174,13 +4065,13 @@
}
},
"node_modules/@opentelemetry/sdk-trace-web": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.1.0.tgz",
- "integrity": "sha512-2F6ZuZFmJg4CdhRPP8+60DkvEwGLCiU3ffAkgnnqe/ALGEBqGa0HrZaNWFGprXWVivrYHpXhr7AEfasgLZD71g==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.2.0.tgz",
+ "integrity": "sha512-x/LHsDBO3kfqaFx5qSzBljJ5QHsRXrvS4MybBDy1k7Svidb8ZyIPudWVzj3s5LpPkYZIgi9e+7tdsNCnptoelw==",
"license": "Apache-2.0",
"dependencies": {
- "@opentelemetry/core": "2.1.0",
- "@opentelemetry/sdk-trace-base": "2.1.0"
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/sdk-trace-base": "2.2.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -4190,9 +4081,9 @@
}
},
"node_modules/@opentelemetry/semantic-conventions": {
- "version": "1.37.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.37.0.tgz",
- "integrity": "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==",
+ "version": "1.38.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz",
+ "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
@@ -5591,9 +5482,9 @@
"license": "MIT"
},
"node_modules/bare-crypto": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.11.6.tgz",
- "integrity": "sha512-CjNxA/7ewj4D1Rhp8RROtQNEjGKhByr8J6PcR8v9W9Hde3e9kT+tbBbBigHgkXGsDniwYIZJ16ccvRAOOgevPw==",
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.12.0.tgz",
+ "integrity": "sha512-GLeZOS2RQkEawVNPvBKmVZQWwSs4jLhculTvuOfLdP+RTMN1Gv3pz+oI9fcQj7/LE2QJM4cPOHfx4u0izBaTHA==",
"license": "Apache-2.0",
"dependencies": {
"bare-stream": "^2.6.3"
@@ -5617,9 +5508,9 @@
}
},
"node_modules/bare-events": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.0.tgz",
- "integrity": "sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA==",
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
+ "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
"license": "Apache-2.0",
"peerDependencies": {
"bare-abort-controller": "*"
@@ -5631,9 +5522,9 @@
}
},
"node_modules/bare-fetch": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/bare-fetch/-/bare-fetch-2.5.0.tgz",
- "integrity": "sha512-2f/mx0hw4CDH2yX2cT7tVZGPnXcUswZCYu/EY72hHfbAipZJwZtDl8f3DsXJG+yXyPBNw3TNPicMbYPpfuaIfw==",
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/bare-fetch/-/bare-fetch-2.5.1.tgz",
+ "integrity": "sha512-BdJie1S9y3TW0pzF6Q/dP95QDjlUPXexiJWSnKFIM/OHID6ITJk2XEQQ25rsGqwLqxQ4felfGkj13mC/ao27mg==",
"license": "Apache-2.0",
"dependencies": {
"bare-form-data": "^1.1.3",
@@ -5664,15 +5555,22 @@
"bare-stream": "^2.6.5"
}
},
+ "node_modules/bare-http-parser": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.0.1.tgz",
+ "integrity": "sha512-A3LTDTcELcmNJ3g5liIaS038v/BQxOhA9cjhBESn7eoV7QCuMoIRBKLDadDe08flxyLbxI2f+1l2MZ/5+HnKPA==",
+ "license": "Apache-2.0"
+ },
"node_modules/bare-http1": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.1.1.tgz",
- "integrity": "sha512-Bcn2YieuzGhlrzWrw9xjlXcI52JvWSkbvWEi+VxezID752OOIoKW1qt5lDR+7K1C12jP4IILnlNCZ8afnxCmxA==",
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.1.6.tgz",
+ "integrity": "sha512-IbQZYJZDB+0sriMTH7sW/ygO+mfSm+d5H/qr4IUhKVH4kiGwADw3/rmSIGAnkhppS0kehbypjpmII54nBbkaNg==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.6.0",
+ "bare-http-parser": "^1.0.0",
"bare-stream": "^2.3.0",
- "bare-tcp": "^2.0.3"
+ "bare-tcp": "^2.2.0"
},
"peerDependencies": {
"bare-buffer": "*",
@@ -5688,20 +5586,20 @@
}
},
"node_modules/bare-https": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.0.0.tgz",
- "integrity": "sha512-qmjNZmYQ4nn+k3CLlxVyOqWYamdBPqE7psR5/lFWG39fskAR4C2h29d1Ka5BeWOGDAWhXImFIwZUxwCE/7xeLA==",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.1.tgz",
+ "integrity": "sha512-1yamjW5TIPBoiwRDGVrTLhbsT8sWKTpDCGiNrJylVFUJD29rwhwrBud9KU/Xe1fWuVcefeRjKoyd2ztFVDv+Sw==",
"license": "Apache-2.0",
"dependencies": {
"bare-http1": "^4.0.0",
- "bare-tcp": "^2.0.0",
+ "bare-tcp": "^2.2.0",
"bare-tls": "^2.0.0"
}
},
"node_modules/bare-net": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.1.1.tgz",
- "integrity": "sha512-r5f8pzsBE7oZ52Z+bRPaIJX9knfSKo2heT77reJk2U1X0Np1rPtlnexsH6z0jqSQZrZhetmm+Tt0iD+LnIv7DQ==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.2.0.tgz",
+ "integrity": "sha512-UF7cAbHsGE+H6uEqWF5IULBow1x58chZz4g3ALgHtv7wZsFcCbRDt0JKWEumf5Oma3QWS1Q6aLi0Rpll8RElMg==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.2.2",
@@ -5711,9 +5609,9 @@
}
},
"node_modules/bare-pipe": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.0.7.tgz",
- "integrity": "sha512-y8C6MWpr6EnzUJ1naE04VNTwM1AW3e6abkEDxgQkcTUVPPpHijxld1zpPT1RjWdPuA0TEc8UvEgMtWomnl5X9A==",
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.2.tgz",
+ "integrity": "sha512-btXtZLlABEDRp50cfLj9iweISqAJSNMCjeq5v0v9tBY2a7zSSqmfa2ZoE1ki2qxAvubagLUqw6VDifpsuI/qmg==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.0.0",
@@ -5745,9 +5643,9 @@
}
},
"node_modules/bare-tcp": {
- "version": "2.0.9",
- "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.0.9.tgz",
- "integrity": "sha512-9pxbeUmAgoLitQJNe/gebVM3erWnDq2rvT07CCLUO/bgylbJbCQ8TqyUpnyDhjL2wOtOA5VhcGUkQ0djRGbSjg==",
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.2.tgz",
+ "integrity": "sha512-bYnw1AhzGlfLOD4nTceUXkhhgznZKvDuwjX1Au0VWaVitwqG40oaTvvhEQVCcK3FEwjRTiukUzHnAFsYXUI+3Q==",
"license": "Apache-2.0",
"dependencies": {
"bare-dns": "^2.0.4",
@@ -5880,9 +5778,9 @@
"license": "MIT"
},
"node_modules/bitcoin-address-parser": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/bitcoin-address-parser/-/bitcoin-address-parser-1.0.1.tgz",
- "integrity": "sha512-4GVUL8qT8rZ0wJilBsxKpvhLaa2v3w2DyLIOgfoyQmmPyjWuRNY3JRcyVq4Z3VsNdYzmPOm/dFNPWraBXLyFBw==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bitcoin-address-parser/-/bitcoin-address-parser-1.0.3.tgz",
+ "integrity": "sha512-zzXg9zj3/LVUCGz5e9oPPklKHsboLJw7/g86TjlHrDGBvWxpNhslxalKnIQX+TjPCBiZIQ2YOVme8srdqZlCCw==",
"license": "Apache-2.0",
"dependencies": {
"base58-js": "^3.0.2",
@@ -6739,9 +6637,9 @@
}
},
"node_modules/dayjs": {
- "version": "1.11.18",
- "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
- "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
+ "version": "1.11.19",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
+ "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
"license": "MIT"
},
"node_modules/debug": {
@@ -6869,6 +6767,26 @@
"minimalistic-assert": "^1.0.0"
}
},
+ "node_modules/detect-europe-js": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/detect-europe-js/-/detect-europe-js-0.1.2.tgz",
+ "integrity": "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/faisalman"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ua-parser-js"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/faisalman"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@@ -6949,23 +6867,6 @@
"node": ">= 0.4"
}
},
- "node_modules/eciesjs": {
- "version": "0.4.16",
- "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.16.tgz",
- "integrity": "sha512-dS5cbA9rA2VR4Ybuvhg6jvdmp46ubLn3E+px8cG/35aEDNclrqoCjg6mt0HYZ/M+OoESS3jSkCrqk1kWAEhWAw==",
- "license": "MIT",
- "dependencies": {
- "@ecies/ciphers": "^0.2.4",
- "@noble/ciphers": "^1.3.0",
- "@noble/curves": "^1.9.7",
- "@noble/hashes": "^1.8.0"
- },
- "engines": {
- "bun": ">=1",
- "deno": ">=2",
- "node": ">=16"
- }
- },
"node_modules/ecpair": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ecpair/-/ecpair-3.0.0.tgz",
@@ -7961,9 +7862,9 @@
"license": "ISC"
},
"node_modules/graphql": {
- "version": "16.11.0",
- "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz",
- "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==",
+ "version": "16.12.0",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz",
+ "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==",
"license": "MIT",
"engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
@@ -8143,9 +8044,9 @@
"license": "MIT"
},
"node_modules/i18next": {
- "version": "25.3.0",
- "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.0.tgz",
- "integrity": "sha512-ZSQIiNGfqSG6yoLHaCvrkPp16UejHI8PCDxFYaNG/1qxtmqNmqEg4JlWKlxkrUmrin2sEjsy+Mjy1TRozBhOgw==",
+ "version": "25.7.3",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.3.tgz",
+ "integrity": "sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==",
"funding": [
{
"type": "individual",
@@ -8161,9 +8062,8 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
- "@babel/runtime": "^7.27.6"
+ "@babel/runtime": "^7.28.4"
},
"peerDependencies": {
"typescript": "^5"
@@ -8295,9 +8195,9 @@
"license": "ISC"
},
"node_modules/ipaddr.js": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
- "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz",
+ "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==",
"license": "MIT",
"engines": {
"node": ">= 10"
@@ -8661,6 +8561,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-standalone-pwa": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-standalone-pwa/-/is-standalone-pwa-0.1.1.tgz",
+ "integrity": "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/faisalman"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ua-parser-js"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/faisalman"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -9419,9 +9339,9 @@
"license": "MIT"
},
"node_modules/nice-grpc": {
- "version": "2.1.13",
- "resolved": "https://registry.npmjs.org/nice-grpc/-/nice-grpc-2.1.13.tgz",
- "integrity": "sha512-IkXNok2NFyYh0WKp1aJFwFV3Ue2frBkJ16ojrmgX3Tc9n0g7r0VU+ur3H/leDHPPGsEeVozdMynGxYT30k3D/Q==",
+ "version": "2.1.14",
+ "resolved": "https://registry.npmjs.org/nice-grpc/-/nice-grpc-2.1.14.tgz",
+ "integrity": "sha512-GK9pKNxlvnU5FAdaw7i2FFuR9CqBspcE+if2tqnKXBcE0R8525wj4BZvfcwj7FjvqbssqKxRHt2nwedalbJlww==",
"license": "MIT",
"dependencies": {
"@grpc/grpc-js": "^1.14.0",
@@ -9430,9 +9350,9 @@
}
},
"node_modules/nice-grpc-client-middleware-retry": {
- "version": "3.1.12",
- "resolved": "https://registry.npmjs.org/nice-grpc-client-middleware-retry/-/nice-grpc-client-middleware-retry-3.1.12.tgz",
- "integrity": "sha512-CHKIeHznAePOsT2dLeGwoOFaybQz6LvkIsFfN8SLcyGyTR7AB6vZMaECJjx+QPL8O2qVgaVE167PdeOmQrPuag==",
+ "version": "3.1.13",
+ "resolved": "https://registry.npmjs.org/nice-grpc-client-middleware-retry/-/nice-grpc-client-middleware-retry-3.1.13.tgz",
+ "integrity": "sha512-Q9I/wm5lYkDTveKFirrTHBkBY137yavXZ4xQDXTPIycUp7aLXD8xPTHFhqtAFWUw05aS91uffZZRgdv3HS0y/g==",
"license": "MIT",
"dependencies": {
"abort-controller-x": "^0.4.0",
@@ -9449,9 +9369,9 @@
}
},
"node_modules/nice-grpc-opentelemetry": {
- "version": "0.1.19",
- "resolved": "https://registry.npmjs.org/nice-grpc-opentelemetry/-/nice-grpc-opentelemetry-0.1.19.tgz",
- "integrity": "sha512-rjYVFbmSTE/g3XtxIh37PgEtE1ZOcwFoSHeLBCMRz9GAeGwZd23020QObgElF2UgU6kn1wpH8taWdprNr+3wfg==",
+ "version": "0.1.20",
+ "resolved": "https://registry.npmjs.org/nice-grpc-opentelemetry/-/nice-grpc-opentelemetry-0.1.20.tgz",
+ "integrity": "sha512-dRH6lmm8OgqY21WRo9BP6cHHqIhbG5UT/INFne0qIDSlSseYc6s1+qNTE3Up0z/4zY50V8tVTOH30yyhkwNXTw==",
"license": "MIT",
"dependencies": {
"@opentelemetry/api": "^1.8.0",
@@ -9462,9 +9382,9 @@
}
},
"node_modules/nice-grpc-web": {
- "version": "3.3.8",
- "resolved": "https://registry.npmjs.org/nice-grpc-web/-/nice-grpc-web-3.3.8.tgz",
- "integrity": "sha512-VOVzjcgl90lhoZgjXDg9hG8clIxe6NBk84R3p0P21CGitMesHMTV8g5adnDKQ4i60FJT1uk2EwepSyv1cND8JQ==",
+ "version": "3.3.9",
+ "resolved": "https://registry.npmjs.org/nice-grpc-web/-/nice-grpc-web-3.3.9.tgz",
+ "integrity": "sha512-CiCQLdLTux9D4try8XlHW9tHIP/uLB+aciNKErDNLUM6kzhPFaVh8q+oTkoVGOjxOacEzlOwQRRjwQETAx5lVw==",
"license": "MIT",
"dependencies": {
"abort-controller-x": "^0.4.0",
@@ -9474,9 +9394,9 @@
}
},
"node_modules/nice-grpc/node_modules/@grpc/grpc-js": {
- "version": "1.14.0",
- "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.0.tgz",
- "integrity": "sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg==",
+ "version": "1.14.3",
+ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
+ "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/proto-loader": "^0.8.0",
@@ -11800,9 +11720,9 @@
}
},
"node_modules/ts-proto": {
- "version": "2.7.7",
- "resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-2.7.7.tgz",
- "integrity": "sha512-/OfN9/Yriji2bbpOysZ/Jzc96isOKz+eBTJEcKaIZ0PR6x1TNgVm4Lz0zfbo+J0jwFO7fJjJyssefBPQ0o1V9A==",
+ "version": "2.8.3",
+ "resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-2.8.3.tgz",
+ "integrity": "sha512-TdXInqG+61pj/TvORqITWjvjTTsL1EZxwX49iEj89+xFAcqPT8tjChpAGQXzfcF4MJwvNiuoCEbBOKqVf3ds3g==",
"license": "ISC",
"dependencies": {
"@bufbuild/protobuf": "^2.0.0",
@@ -11932,6 +11852,57 @@
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==",
"license": "MIT"
},
+ "node_modules/ua-is-frozen": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/ua-is-frozen/-/ua-is-frozen-0.1.2.tgz",
+ "integrity": "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/faisalman"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ua-parser-js"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/faisalman"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/ua-parser-js": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.7.tgz",
+ "integrity": "sha512-CFdHVHr+6YfbktNZegH3qbYvYgC7nRNEUm2tk7nSFXSODUu4tDBpaFpP1jdXBUOKKwapVlWRfTtS8bCPzsQ47w==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ua-parser-js"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/faisalman"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/faisalman"
+ }
+ ],
+ "license": "AGPL-3.0-or-later",
+ "dependencies": {
+ "detect-europe-js": "^0.1.2",
+ "is-standalone-pwa": "^0.1.1",
+ "ua-is-frozen": "^0.1.2"
+ },
+ "bin": {
+ "ua-parser-js": "script/cli.js"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/uint8array-tools": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.8.tgz",
@@ -12167,9 +12138,9 @@
}
},
"node_modules/uuidv7": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/uuidv7/-/uuidv7-1.0.2.tgz",
- "integrity": "sha512-8JQkH4ooXnm1JCIhqTMbtmdnYEn6oKukBxHn1Ic9878jMkL7daTI7anTExfY18VRCX7tcdn5quzvCb6EWrR8PA==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/uuidv7/-/uuidv7-1.1.0.tgz",
+ "integrity": "sha512-2VNnOC0+XQlwogChUDzy6pe8GQEys9QFZBGOh54l6qVfwoCUwwRvk7rDTgaIsRgsF5GFa5oiNg8LqXE3jofBBg==",
"license": "Apache-2.0",
"bin": {
"uuidv7": "cli.js"
diff --git a/package.json b/package.json
index 61da2e2..240534e 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "blitz-web-app",
"private": true,
- "version": "0.1.3",
+ "version": "0.1.4",
"type": "module",
"scripts": {
"dev": "vite",
@@ -15,7 +15,7 @@
"dependencies": {
"@bitcoinerlab/secp256k1": "^1.2.0",
"@breeztech/breez-sdk-liquid": "^0.11.2",
- "@buildonspark/spark-sdk": "^0.4.3",
+ "@buildonspark/spark-sdk": "^0.5.2",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.1.1",
@@ -27,7 +27,7 @@
"@vulpemventures/secp256k1-zkp": "^3.2.1",
"bech32": "^2.0.0",
"bip21": "^3.0.0",
- "bitcoin-address-parser": "^1.0.1",
+ "bitcoin-address-parser": "^1.0.3",
"bitcoinjs-lib": "^6.1.7",
"bolt11": "^1.4.1",
"boltz-core": "^3.0.0",
@@ -37,6 +37,7 @@
"events": "^3.3.0",
"firebase": "^11.9.0",
"framer-motion": "^12.10.0",
+ "i18next": "^25.7.3",
"idb": "^8.0.3",
"intl-pluralrules": "^2.0.1",
"js-sha256": "^0.11.1",
diff --git a/src/assets/fonts/Blitzicons1.ttf b/src/assets/fonts/Blitzicons1.ttf
new file mode 100644
index 0000000..848ce09
Binary files /dev/null and b/src/assets/fonts/Blitzicons1.ttf differ
diff --git a/src/assets/icons/BTCMap.png b/src/assets/icons/BTCMap.png
deleted file mode 100644
index 61c7561..0000000
Binary files a/src/assets/icons/BTCMap.png and /dev/null differ
diff --git a/src/assets/icons/SparkAsteriskBlack.png b/src/assets/icons/SparkAsteriskBlack.png
deleted file mode 100644
index 9a58d9f..0000000
Binary files a/src/assets/icons/SparkAsteriskBlack.png and /dev/null differ
diff --git a/src/assets/icons/aboutIcon.png b/src/assets/icons/aboutIcon.png
deleted file mode 100644
index d55705b..0000000
Binary files a/src/assets/icons/aboutIcon.png and /dev/null differ
diff --git a/src/assets/icons/aboutIconWhite.png b/src/assets/icons/aboutIconWhite.png
deleted file mode 100644
index 7d3a3dc..0000000
Binary files a/src/assets/icons/aboutIconWhite.png and /dev/null differ
diff --git a/src/assets/icons/adminHomeWallet.png b/src/assets/icons/adminHomeWallet.png
deleted file mode 100644
index 9059cd3..0000000
Binary files a/src/assets/icons/adminHomeWallet.png and /dev/null differ
diff --git a/src/assets/icons/adminHomeWallet_dark.png b/src/assets/icons/adminHomeWallet_dark.png
deleted file mode 100644
index a9ae67a..0000000
Binary files a/src/assets/icons/adminHomeWallet_dark.png and /dev/null differ
diff --git a/src/assets/icons/adminHomeWallet_white.png b/src/assets/icons/adminHomeWallet_white.png
deleted file mode 100644
index 344a09b..0000000
Binary files a/src/assets/icons/adminHomeWallet_white.png and /dev/null differ
diff --git a/src/assets/icons/appStore.png b/src/assets/icons/appStore.png
deleted file mode 100644
index f1d3a7e..0000000
Binary files a/src/assets/icons/appStore.png and /dev/null differ
diff --git a/src/assets/icons/appStoreFilled.png b/src/assets/icons/appStoreFilled.png
deleted file mode 100644
index e6e076b..0000000
Binary files a/src/assets/icons/appStoreFilled.png and /dev/null differ
diff --git a/src/assets/icons/appStoreFilled_white.png b/src/assets/icons/appStoreFilled_white.png
deleted file mode 100644
index 20ac022..0000000
Binary files a/src/assets/icons/appStoreFilled_white.png and /dev/null differ
diff --git a/src/assets/icons/appStore_white.png b/src/assets/icons/appStore_white.png
deleted file mode 100644
index 6f3f03a..0000000
Binary files a/src/assets/icons/appStore_white.png and /dev/null differ
diff --git a/src/assets/icons/arrow-from-shape-right-white.png b/src/assets/icons/arrow-from-shape-right-white.png
deleted file mode 100644
index d572d11..0000000
Binary files a/src/assets/icons/arrow-from-shape-right-white.png and /dev/null differ
diff --git a/src/assets/icons/arrow-from-shape-right.png b/src/assets/icons/arrow-from-shape-right.png
deleted file mode 100644
index 2de08cd..0000000
Binary files a/src/assets/icons/arrow-from-shape-right.png and /dev/null differ
diff --git a/src/assets/icons/arrow-small-left-white.png b/src/assets/icons/arrow-small-left-white.png
deleted file mode 100644
index 88fad4d..0000000
Binary files a/src/assets/icons/arrow-small-left-white.png and /dev/null differ
diff --git a/src/assets/icons/arrow-small-left.png b/src/assets/icons/arrow-small-left.png
deleted file mode 100644
index f1705ba..0000000
Binary files a/src/assets/icons/arrow-small-left.png and /dev/null differ
diff --git a/src/assets/icons/arrow-to-shape-right-light.png b/src/assets/icons/arrow-to-shape-right-light.png
deleted file mode 100644
index d9cebcd..0000000
Binary files a/src/assets/icons/arrow-to-shape-right-light.png and /dev/null differ
diff --git a/src/assets/icons/arrow-to-shape-right.png b/src/assets/icons/arrow-to-shape-right.png
deleted file mode 100644
index 4b446b5..0000000
Binary files a/src/assets/icons/arrow-to-shape-right.png and /dev/null differ
diff --git a/src/assets/icons/bell.png b/src/assets/icons/bell.png
deleted file mode 100644
index b0803a8..0000000
Binary files a/src/assets/icons/bell.png and /dev/null differ
diff --git a/src/assets/icons/bellWhite.png b/src/assets/icons/bellWhite.png
deleted file mode 100644
index 2025171..0000000
Binary files a/src/assets/icons/bellWhite.png and /dev/null differ
diff --git a/src/assets/icons/blockstreamLiquidBlue.png b/src/assets/icons/blockstreamLiquidBlue.png
deleted file mode 100644
index 81890fb..0000000
Binary files a/src/assets/icons/blockstreamLiquidBlue.png and /dev/null differ
diff --git a/src/assets/icons/cart.png b/src/assets/icons/cart.png
deleted file mode 100644
index 260863a..0000000
Binary files a/src/assets/icons/cart.png and /dev/null differ
diff --git a/src/assets/icons/check.png b/src/assets/icons/check.png
deleted file mode 100644
index 1b763d3..0000000
Binary files a/src/assets/icons/check.png and /dev/null differ
diff --git a/src/assets/icons/check.svg b/src/assets/icons/check.svg
deleted file mode 100644
index 9610907..0000000
--- a/src/assets/icons/check.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
\ No newline at end of file
diff --git a/src/assets/icons/checkMark.png b/src/assets/icons/checkMark.png
deleted file mode 100644
index 840eabf..0000000
Binary files a/src/assets/icons/checkMark.png and /dev/null differ
diff --git a/src/assets/icons/checkWhite.png b/src/assets/icons/checkWhite.png
deleted file mode 100644
index 4e7b57c..0000000
Binary files a/src/assets/icons/checkWhite.png and /dev/null differ
diff --git a/src/assets/icons/clipboardBlue.png b/src/assets/icons/clipboardBlue.png
deleted file mode 100644
index 280c27c..0000000
Binary files a/src/assets/icons/clipboardBlue.png and /dev/null differ
diff --git a/src/assets/icons/clipboardDark.png b/src/assets/icons/clipboardDark.png
deleted file mode 100644
index 8f3c393..0000000
Binary files a/src/assets/icons/clipboardDark.png and /dev/null differ
diff --git a/src/assets/icons/clipboardLight.png b/src/assets/icons/clipboardLight.png
deleted file mode 100644
index 1526ed0..0000000
Binary files a/src/assets/icons/clipboardLight.png and /dev/null differ
diff --git a/src/assets/icons/colorIcon.png b/src/assets/icons/colorIcon.png
deleted file mode 100644
index 11f7b9a..0000000
Binary files a/src/assets/icons/colorIcon.png and /dev/null differ
diff --git a/src/assets/icons/colorIconWhite.png b/src/assets/icons/colorIconWhite.png
deleted file mode 100644
index 0c3a92c..0000000
Binary files a/src/assets/icons/colorIconWhite.png and /dev/null differ
diff --git a/src/assets/icons/contacts.png b/src/assets/icons/contacts.png
deleted file mode 100644
index 0dad2af..0000000
Binary files a/src/assets/icons/contacts.png and /dev/null differ
diff --git a/src/assets/icons/contactsIcon.png b/src/assets/icons/contactsIcon.png
deleted file mode 100644
index 4054d7a..0000000
Binary files a/src/assets/icons/contactsIcon.png and /dev/null differ
diff --git a/src/assets/icons/contactsIconBlue.png b/src/assets/icons/contactsIconBlue.png
deleted file mode 100644
index d4a730e..0000000
Binary files a/src/assets/icons/contactsIconBlue.png and /dev/null differ
diff --git a/src/assets/icons/contactsIconLight.png b/src/assets/icons/contactsIconLight.png
deleted file mode 100644
index 77f824b..0000000
Binary files a/src/assets/icons/contactsIconLight.png and /dev/null differ
diff --git a/src/assets/icons/contactsIconSelected.png b/src/assets/icons/contactsIconSelected.png
deleted file mode 100644
index e3a210c..0000000
Binary files a/src/assets/icons/contactsIconSelected.png and /dev/null differ
diff --git a/src/assets/icons/contactsIconSelectedWhite.png b/src/assets/icons/contactsIconSelectedWhite.png
deleted file mode 100644
index b6a10a3..0000000
Binary files a/src/assets/icons/contactsIconSelectedWhite.png and /dev/null differ
diff --git a/src/assets/icons/contactsIconWhite.png b/src/assets/icons/contactsIconWhite.png
deleted file mode 100644
index b659c41..0000000
Binary files a/src/assets/icons/contactsIconWhite.png and /dev/null differ
diff --git a/src/assets/icons/contactsSelected.png b/src/assets/icons/contactsSelected.png
deleted file mode 100644
index d52081a..0000000
Binary files a/src/assets/icons/contactsSelected.png and /dev/null differ
diff --git a/src/assets/icons/currencyIcon.png b/src/assets/icons/currencyIcon.png
deleted file mode 100644
index 2e1984e..0000000
Binary files a/src/assets/icons/currencyIcon.png and /dev/null differ
diff --git a/src/assets/icons/currencyIconWhite.png b/src/assets/icons/currencyIconWhite.png
deleted file mode 100644
index 3df1bda..0000000
Binary files a/src/assets/icons/currencyIconWhite.png and /dev/null differ
diff --git a/src/assets/icons/darkMode.png b/src/assets/icons/darkMode.png
deleted file mode 100644
index 28a3f1b..0000000
Binary files a/src/assets/icons/darkMode.png and /dev/null differ
diff --git a/src/assets/icons/dotDark.png b/src/assets/icons/dotDark.png
deleted file mode 100644
index 764ea55..0000000
Binary files a/src/assets/icons/dotDark.png and /dev/null differ
diff --git a/src/assets/icons/dotLight.png b/src/assets/icons/dotLight.png
deleted file mode 100644
index 46bb2e3..0000000
Binary files a/src/assets/icons/dotLight.png and /dev/null differ
diff --git a/src/assets/icons/drawerList.png b/src/assets/icons/drawerList.png
deleted file mode 100644
index 46caa4c..0000000
Binary files a/src/assets/icons/drawerList.png and /dev/null differ
diff --git a/src/assets/icons/drawerListWhite.png b/src/assets/icons/drawerListWhite.png
deleted file mode 100644
index 8608ec9..0000000
Binary files a/src/assets/icons/drawerListWhite.png and /dev/null differ
diff --git a/src/assets/icons/edit.png b/src/assets/icons/edit.png
deleted file mode 100644
index 06c32f6..0000000
Binary files a/src/assets/icons/edit.png and /dev/null differ
diff --git a/src/assets/icons/editLight.png b/src/assets/icons/editLight.png
deleted file mode 100644
index 1baf196..0000000
Binary files a/src/assets/icons/editLight.png and /dev/null differ
diff --git a/src/assets/icons/exchangeIcon.png b/src/assets/icons/exchangeIcon.png
deleted file mode 100644
index aa3ef1d..0000000
Binary files a/src/assets/icons/exchangeIcon.png and /dev/null differ
diff --git a/src/assets/icons/exchangeIconWhite.png b/src/assets/icons/exchangeIconWhite.png
deleted file mode 100644
index e228d60..0000000
Binary files a/src/assets/icons/exchangeIconWhite.png and /dev/null differ
diff --git a/src/assets/icons/faceIDIcon.png b/src/assets/icons/faceIDIcon.png
deleted file mode 100644
index cb88cac..0000000
Binary files a/src/assets/icons/faceIDIcon.png and /dev/null differ
diff --git a/src/assets/icons/faceIDIconWhite.png b/src/assets/icons/faceIDIconWhite.png
deleted file mode 100644
index bb87260..0000000
Binary files a/src/assets/icons/faceIDIconWhite.png and /dev/null differ
diff --git a/src/assets/icons/failedTransaction.png b/src/assets/icons/failedTransaction.png
deleted file mode 100644
index abe0a0d..0000000
Binary files a/src/assets/icons/failedTransaction.png and /dev/null differ
diff --git a/src/assets/icons/failedTransactionWhite.png b/src/assets/icons/failedTransactionWhite.png
deleted file mode 100644
index bd1e393..0000000
Binary files a/src/assets/icons/failedTransactionWhite.png and /dev/null differ
diff --git a/src/assets/icons/flashlight.png b/src/assets/icons/flashlight.png
deleted file mode 100644
index 137172d..0000000
Binary files a/src/assets/icons/flashlight.png and /dev/null differ
diff --git a/src/assets/icons/flashlightBlue.png b/src/assets/icons/flashlightBlue.png
deleted file mode 100644
index 13aa0d2..0000000
Binary files a/src/assets/icons/flashlightBlue.png and /dev/null differ
diff --git a/src/assets/icons/flashlightNoFill.png b/src/assets/icons/flashlightNoFill.png
deleted file mode 100644
index 8d50e2f..0000000
Binary files a/src/assets/icons/flashlightNoFill.png and /dev/null differ
diff --git a/src/assets/icons/flashlightNoFillWhite.png b/src/assets/icons/flashlightNoFillWhite.png
deleted file mode 100644
index fb2255c..0000000
Binary files a/src/assets/icons/flashlightNoFillWhite.png and /dev/null differ
diff --git a/src/assets/icons/github.png b/src/assets/icons/github.png
deleted file mode 100644
index 132c186..0000000
Binary files a/src/assets/icons/github.png and /dev/null differ
diff --git a/src/assets/icons/githubWhite.png b/src/assets/icons/githubWhite.png
deleted file mode 100644
index 0507b83..0000000
Binary files a/src/assets/icons/githubWhite.png and /dev/null differ
diff --git a/src/assets/icons/group.png b/src/assets/icons/group.png
deleted file mode 100644
index 83ead21..0000000
Binary files a/src/assets/icons/group.png and /dev/null differ
diff --git a/src/assets/icons/groupWhite.png b/src/assets/icons/groupWhite.png
deleted file mode 100644
index 070d3c4..0000000
Binary files a/src/assets/icons/groupWhite.png and /dev/null differ
diff --git a/src/assets/icons/home.png b/src/assets/icons/home.png
deleted file mode 100644
index 1b5860a..0000000
Binary files a/src/assets/icons/home.png and /dev/null differ
diff --git a/src/assets/icons/images.png b/src/assets/icons/images.png
deleted file mode 100644
index 1c73bbc..0000000
Binary files a/src/assets/icons/images.png and /dev/null differ
diff --git a/src/assets/icons/imagesBlue.png b/src/assets/icons/imagesBlue.png
deleted file mode 100644
index 1f0a0d6..0000000
Binary files a/src/assets/icons/imagesBlue.png and /dev/null differ
diff --git a/src/assets/icons/imagesDark.png b/src/assets/icons/imagesDark.png
deleted file mode 100644
index d6fbd31..0000000
Binary files a/src/assets/icons/imagesDark.png and /dev/null differ
diff --git a/src/assets/icons/keyIcon.png b/src/assets/icons/keyIcon.png
deleted file mode 100644
index 881d3ce..0000000
Binary files a/src/assets/icons/keyIcon.png and /dev/null differ
diff --git a/src/assets/icons/keyIconWhite.png b/src/assets/icons/keyIconWhite.png
deleted file mode 100644
index ea12ef2..0000000
Binary files a/src/assets/icons/keyIconWhite.png and /dev/null differ
diff --git a/src/assets/icons/left-chevron-white.png b/src/assets/icons/left-chevron-white.png
deleted file mode 100644
index 3e4021d..0000000
Binary files a/src/assets/icons/left-chevron-white.png and /dev/null differ
diff --git a/src/assets/icons/left-chevron.png b/src/assets/icons/left-chevron.png
deleted file mode 100644
index dac4a1b..0000000
Binary files a/src/assets/icons/left-chevron.png and /dev/null differ
diff --git a/src/assets/icons/leftCheveronDark.png b/src/assets/icons/leftCheveronDark.png
deleted file mode 100644
index 3177117..0000000
Binary files a/src/assets/icons/leftCheveronDark.png and /dev/null differ
diff --git a/src/assets/icons/leftCheveronLight.png b/src/assets/icons/leftCheveronLight.png
deleted file mode 100644
index 9eb3f8f..0000000
Binary files a/src/assets/icons/leftCheveronLight.png and /dev/null differ
diff --git a/src/assets/icons/lightMode.png b/src/assets/icons/lightMode.png
deleted file mode 100644
index e8e1227..0000000
Binary files a/src/assets/icons/lightMode.png and /dev/null differ
diff --git a/src/assets/icons/lightModeWhite.png b/src/assets/icons/lightModeWhite.png
deleted file mode 100644
index 2c2aff6..0000000
Binary files a/src/assets/icons/lightModeWhite.png and /dev/null differ
diff --git a/src/assets/icons/liquidIcon.png b/src/assets/icons/liquidIcon.png
deleted file mode 100644
index ea952f4..0000000
Binary files a/src/assets/icons/liquidIcon.png and /dev/null differ
diff --git a/src/assets/icons/liquidIconWhite.png b/src/assets/icons/liquidIconWhite.png
deleted file mode 100644
index 7eb34b9..0000000
Binary files a/src/assets/icons/liquidIconWhite.png and /dev/null differ
diff --git a/src/assets/icons/liquidLight.png b/src/assets/icons/liquidLight.png
deleted file mode 100644
index 7b7d57e..0000000
Binary files a/src/assets/icons/liquidLight.png and /dev/null differ
diff --git a/src/assets/icons/liquidReceiveIcon.png b/src/assets/icons/liquidReceiveIcon.png
deleted file mode 100644
index 75f1e42..0000000
Binary files a/src/assets/icons/liquidReceiveIcon.png and /dev/null differ
diff --git a/src/assets/icons/messages-dark.png b/src/assets/icons/messages-dark.png
deleted file mode 100644
index 45c7e1e..0000000
Binary files a/src/assets/icons/messages-dark.png and /dev/null differ
diff --git a/src/assets/icons/messages-light.png b/src/assets/icons/messages-light.png
deleted file mode 100644
index f4da7ba..0000000
Binary files a/src/assets/icons/messages-light.png and /dev/null differ
diff --git a/src/assets/icons/minus.png b/src/assets/icons/minus.png
deleted file mode 100644
index 11aa102..0000000
Binary files a/src/assets/icons/minus.png and /dev/null differ
diff --git a/src/assets/icons/navigation.png b/src/assets/icons/navigation.png
deleted file mode 100644
index 2963692..0000000
Binary files a/src/assets/icons/navigation.png and /dev/null differ
diff --git a/src/assets/icons/navigation_fill.png b/src/assets/icons/navigation_fill.png
deleted file mode 100644
index 8d8cb0c..0000000
Binary files a/src/assets/icons/navigation_fill.png and /dev/null differ
diff --git a/src/assets/icons/navigation_fill_white.png b/src/assets/icons/navigation_fill_white.png
deleted file mode 100644
index 586eacf..0000000
Binary files a/src/assets/icons/navigation_fill_white.png and /dev/null differ
diff --git a/src/assets/icons/navigation_white.png b/src/assets/icons/navigation_white.png
deleted file mode 100644
index 8395982..0000000
Binary files a/src/assets/icons/navigation_white.png and /dev/null differ
diff --git a/src/assets/icons/nodeIcon.png b/src/assets/icons/nodeIcon.png
deleted file mode 100644
index a403da5..0000000
Binary files a/src/assets/icons/nodeIcon.png and /dev/null differ
diff --git a/src/assets/icons/nodeIconWhite.png b/src/assets/icons/nodeIconWhite.png
deleted file mode 100644
index 2942468..0000000
Binary files a/src/assets/icons/nodeIconWhite.png and /dev/null differ
diff --git a/src/assets/icons/notificationsIcon.png b/src/assets/icons/notificationsIcon.png
deleted file mode 100644
index 4116431..0000000
Binary files a/src/assets/icons/notificationsIcon.png and /dev/null differ
diff --git a/src/assets/icons/pendingTx.png b/src/assets/icons/pendingTx.png
deleted file mode 100644
index 30121d4..0000000
Binary files a/src/assets/icons/pendingTx.png and /dev/null differ
diff --git a/src/assets/icons/plus.png b/src/assets/icons/plus.png
deleted file mode 100644
index 8cfc527..0000000
Binary files a/src/assets/icons/plus.png and /dev/null differ
diff --git a/src/assets/icons/posDark.png b/src/assets/icons/posDark.png
deleted file mode 100644
index bd27549..0000000
Binary files a/src/assets/icons/posDark.png and /dev/null differ
diff --git a/src/assets/icons/posLight.png b/src/assets/icons/posLight.png
deleted file mode 100644
index ea59d78..0000000
Binary files a/src/assets/icons/posLight.png and /dev/null differ
diff --git a/src/assets/icons/qrCodeDark.png b/src/assets/icons/qrCodeDark.png
deleted file mode 100644
index dc9eba9..0000000
Binary files a/src/assets/icons/qrCodeDark.png and /dev/null differ
diff --git a/src/assets/icons/qrCodeLight.png b/src/assets/icons/qrCodeLight.png
deleted file mode 100644
index f60833d..0000000
Binary files a/src/assets/icons/qrCodeLight.png and /dev/null differ
diff --git a/src/assets/icons/receipt.png b/src/assets/icons/receipt.png
deleted file mode 100644
index 5a85cac..0000000
Binary files a/src/assets/icons/receipt.png and /dev/null differ
diff --git a/src/assets/icons/receiptWhite.png b/src/assets/icons/receiptWhite.png
deleted file mode 100644
index e27eb0a..0000000
Binary files a/src/assets/icons/receiptWhite.png and /dev/null differ
diff --git a/src/assets/icons/refresh.png b/src/assets/icons/refresh.png
deleted file mode 100644
index e50a834..0000000
Binary files a/src/assets/icons/refresh.png and /dev/null differ
diff --git a/src/assets/icons/refreshWhite.png b/src/assets/icons/refreshWhite.png
deleted file mode 100644
index d2d89f3..0000000
Binary files a/src/assets/icons/refreshWhite.png and /dev/null differ
diff --git a/src/assets/icons/restaurantDark.png b/src/assets/icons/restaurantDark.png
deleted file mode 100644
index ca8be28..0000000
Binary files a/src/assets/icons/restaurantDark.png and /dev/null differ
diff --git a/src/assets/icons/restaurantLight.png b/src/assets/icons/restaurantLight.png
deleted file mode 100644
index 9500c3c..0000000
Binary files a/src/assets/icons/restaurantLight.png and /dev/null differ
diff --git a/src/assets/icons/rootstockLogoBlue.png b/src/assets/icons/rootstockLogoBlue.png
deleted file mode 100644
index 43967a4..0000000
Binary files a/src/assets/icons/rootstockLogoBlue.png and /dev/null differ
diff --git a/src/assets/icons/scanQRCodeBlue.png b/src/assets/icons/scanQRCodeBlue.png
deleted file mode 100644
index ff1c8a7..0000000
Binary files a/src/assets/icons/scanQRCodeBlue.png and /dev/null differ
diff --git a/src/assets/icons/scanQRCodeDark.png b/src/assets/icons/scanQRCodeDark.png
deleted file mode 100644
index 045e8ef..0000000
Binary files a/src/assets/icons/scanQRCodeDark.png and /dev/null differ
diff --git a/src/assets/icons/scanQRCodeLight.png b/src/assets/icons/scanQRCodeLight.png
deleted file mode 100644
index 3d960ad..0000000
Binary files a/src/assets/icons/scanQRCodeLight.png and /dev/null differ
diff --git a/src/assets/icons/settings.png b/src/assets/icons/settings.png
deleted file mode 100644
index 12c06a1..0000000
Binary files a/src/assets/icons/settings.png and /dev/null differ
diff --git a/src/assets/icons/settingsBitcoinIcon.png b/src/assets/icons/settingsBitcoinIcon.png
deleted file mode 100644
index 7760b9d..0000000
Binary files a/src/assets/icons/settingsBitcoinIcon.png and /dev/null differ
diff --git a/src/assets/icons/settingsBitcoinIconWhite.png b/src/assets/icons/settingsBitcoinIconWhite.png
deleted file mode 100644
index 1472bc1..0000000
Binary files a/src/assets/icons/settingsBitcoinIconWhite.png and /dev/null differ
diff --git a/src/assets/icons/settingsWhite.png b/src/assets/icons/settingsWhite.png
deleted file mode 100644
index ece5055..0000000
Binary files a/src/assets/icons/settingsWhite.png and /dev/null differ
diff --git a/src/assets/icons/share.png b/src/assets/icons/share.png
deleted file mode 100644
index 9b3214d..0000000
Binary files a/src/assets/icons/share.png and /dev/null differ
diff --git a/src/assets/icons/shareBlack.png b/src/assets/icons/shareBlack.png
deleted file mode 100644
index 11e5a06..0000000
Binary files a/src/assets/icons/shareBlack.png and /dev/null differ
diff --git a/src/assets/icons/shareWhite.png b/src/assets/icons/shareWhite.png
deleted file mode 100644
index 4e4bd49..0000000
Binary files a/src/assets/icons/shareWhite.png and /dev/null differ
diff --git a/src/assets/icons/sharedBlue.png b/src/assets/icons/sharedBlue.png
deleted file mode 100644
index 5f2f40d..0000000
Binary files a/src/assets/icons/sharedBlue.png and /dev/null differ
diff --git a/src/assets/icons/sharedWhite.png b/src/assets/icons/sharedWhite.png
deleted file mode 100644
index cfc1c9e..0000000
Binary files a/src/assets/icons/sharedWhite.png and /dev/null differ
diff --git a/src/assets/icons/sparkReceiveIcon.png b/src/assets/icons/sparkReceiveIcon.png
deleted file mode 100644
index 3870cd2..0000000
Binary files a/src/assets/icons/sparkReceiveIcon.png and /dev/null differ
diff --git a/src/assets/icons/starBlack.png b/src/assets/icons/starBlack.png
deleted file mode 100644
index 1fbda78..0000000
Binary files a/src/assets/icons/starBlack.png and /dev/null differ
diff --git a/src/assets/icons/starBlue.png b/src/assets/icons/starBlue.png
deleted file mode 100644
index 9a9983d..0000000
Binary files a/src/assets/icons/starBlue.png and /dev/null differ
diff --git a/src/assets/icons/starWhite.png b/src/assets/icons/starWhite.png
deleted file mode 100644
index 4349339..0000000
Binary files a/src/assets/icons/starWhite.png and /dev/null differ
diff --git a/src/assets/icons/telegram.png b/src/assets/icons/telegram.png
deleted file mode 100644
index b606120..0000000
Binary files a/src/assets/icons/telegram.png and /dev/null differ
diff --git a/src/assets/icons/telegramWhite.png b/src/assets/icons/telegramWhite.png
deleted file mode 100644
index 4994e83..0000000
Binary files a/src/assets/icons/telegramWhite.png and /dev/null differ
diff --git a/src/assets/icons/trashIcon.png b/src/assets/icons/trashIcon.png
deleted file mode 100644
index 0399fec..0000000
Binary files a/src/assets/icons/trashIcon.png and /dev/null differ
diff --git a/src/assets/icons/trashIconWhite.png b/src/assets/icons/trashIconWhite.png
deleted file mode 100644
index a9bac4c..0000000
Binary files a/src/assets/icons/trashIconWhite.png and /dev/null differ
diff --git a/src/assets/icons/twitter.png b/src/assets/icons/twitter.png
deleted file mode 100644
index 8dc3c4e..0000000
Binary files a/src/assets/icons/twitter.png and /dev/null differ
diff --git a/src/assets/icons/twitterWhite.png b/src/assets/icons/twitterWhite.png
deleted file mode 100644
index 6d44301..0000000
Binary files a/src/assets/icons/twitterWhite.png and /dev/null differ
diff --git a/src/assets/icons/user_white.png b/src/assets/icons/user_white.png
deleted file mode 100644
index 36805a1..0000000
Binary files a/src/assets/icons/user_white.png and /dev/null differ
diff --git a/src/assets/icons/wallet.png b/src/assets/icons/wallet.png
deleted file mode 100644
index ca593a7..0000000
Binary files a/src/assets/icons/wallet.png and /dev/null differ
diff --git a/src/assets/icons/wallet_blue.png b/src/assets/icons/wallet_blue.png
deleted file mode 100644
index 371249f..0000000
Binary files a/src/assets/icons/wallet_blue.png and /dev/null differ
diff --git a/src/assets/icons/wallet_white.png b/src/assets/icons/wallet_white.png
deleted file mode 100644
index 197984c..0000000
Binary files a/src/assets/icons/wallet_white.png and /dev/null differ
diff --git a/src/assets/icons/x-small-black.png b/src/assets/icons/x-small-black.png
deleted file mode 100644
index 73d414d..0000000
Binary files a/src/assets/icons/x-small-black.png and /dev/null differ
diff --git a/src/assets/icons/x-small-white.png b/src/assets/icons/x-small-white.png
deleted file mode 100644
index 8b09fe5..0000000
Binary files a/src/assets/icons/x-small-white.png and /dev/null differ
diff --git a/src/assets/icons/x-small.png b/src/assets/icons/x-small.png
deleted file mode 100644
index 1e8175c..0000000
Binary files a/src/assets/icons/x-small.png and /dev/null differ
diff --git a/src/assets/scanQRCodeLight.png b/src/assets/scanQRCodeLight.png
deleted file mode 100644
index 3d960ad..0000000
Binary files a/src/assets/scanQRCodeLight.png and /dev/null differ
diff --git a/src/assets/sendRequestImage.png b/src/assets/sendRequestImage.png
deleted file mode 100644
index acbc078..0000000
Binary files a/src/assets/sendRequestImage.png and /dev/null differ
diff --git a/src/components/ThemeImage/themeImage.jsx b/src/components/ThemeImage/themeImage.jsx
index a333fdd..f5ac313 100644
--- a/src/components/ThemeImage/themeImage.jsx
+++ b/src/components/ThemeImage/themeImage.jsx
@@ -1,6 +1,5 @@
import { useMemo } from "react";
import { useThemeContext } from "../../contexts/themeContext";
-import { darkMode } from "../../constants/icons";
export default function ThemeImage({
styles,
@@ -25,7 +24,7 @@ export default function ThemeImage({
: "initial",
...styles,
};
- }, [styles, theme, darkMode, filter]);
+ }, [styles, theme, darkModeType, filter]);
// const imageSource = useMemo(() => {
// return theme
// ? darkModeType
@@ -36,6 +35,7 @@ export default function ThemeImage({
return (
{
if (clickFunction) {
diff --git a/src/components/activityIndicator/activityIndicator.jsx b/src/components/activityIndicator/activityIndicator.jsx
index 7d66962..585d0e0 100644
--- a/src/components/activityIndicator/activityIndicator.jsx
+++ b/src/components/activityIndicator/activityIndicator.jsx
@@ -1,7 +1,6 @@
import { Colors } from "../../constants/theme";
import "./style.css";
export default function ActivityIndicator({ color, size = "small" }) {
- console.log(color);
return (
-
);
diff --git a/src/components/checkCircle/checkCircle.jsx b/src/components/checkCircle/checkCircle.jsx
index 8a954cf..755277e 100644
--- a/src/components/checkCircle/checkCircle.jsx
+++ b/src/components/checkCircle/checkCircle.jsx
@@ -2,7 +2,7 @@ import "./style.css";
import { Colors } from "../../constants/theme";
import useThemeColors from "../../hooks/useThemeColors";
import { useThemeContext } from "../../contexts/themeContext";
-import { checkMark } from "../../constants/icons";
+import { Check } from "lucide-react";
export default function CheckCircle({ isActive, containerSize = 30 }) {
const { theme } = useThemeContext();
@@ -28,7 +28,13 @@ export default function CheckCircle({ isActive, containerSize = 30 }) {
}}
id="customCheckCircle"
>
- {isActive &&
}
+ {isActive && (
+
+ )}
);
}
diff --git a/src/components/checkCircle/style.css b/src/components/checkCircle/style.css
index e82d3e9..14f06ce 100644
--- a/src/components/checkCircle/style.css
+++ b/src/components/checkCircle/style.css
@@ -5,8 +5,3 @@
justify-content: center;
border: 1px solid red;
}
-#customCheckCircle img {
- width: 50%;
- height: 50%;
- filter: invert(1);
-}
diff --git a/src/components/customInput/customInput.jsx b/src/components/customInput/customInput.jsx
index 66e1c7d..19dc99f 100644
--- a/src/components/customInput/customInput.jsx
+++ b/src/components/customInput/customInput.jsx
@@ -11,6 +11,8 @@ export default function CustomInput({
onFocus,
onBlur,
multiline = false,
+ ref,
+ maxLength,
}) {
const commonProps = {
value,
@@ -23,10 +25,12 @@ export default function CustomInput({
...customInputStyles,
resize: "none",
},
+ maxLength,
};
return (
diff --git a/src/components/customInput/style.css b/src/components/customInput/style.css
index c9ca5b9..c41db47 100644
--- a/src/components/customInput/style.css
+++ b/src/components/customInput/style.css
@@ -6,7 +6,7 @@
}
.custom-description-input-container .description-input {
width: 100%;
- padding: 15px;
+ padding: 10px;
border: unset;
border-radius: 8px;
}
diff --git a/src/components/customNumberKeyboard/customNumberKeyboard.jsx b/src/components/customNumberKeyboard/customNumberKeyboard.jsx
index 5620193..b61d1fc 100644
--- a/src/components/customNumberKeyboard/customNumberKeyboard.jsx
+++ b/src/components/customNumberKeyboard/customNumberKeyboard.jsx
@@ -3,9 +3,8 @@ import { useCallback } from "react";
import "./style.css";
import numberConverter from "../../functions/numberConverter";
import { SATSPERBITCOIN } from "../../constants";
-import { leftCheveronDark } from "../../constants/icons";
-import ThemeImage from "../ThemeImage/themeImage";
import useThemeColors from "../../hooks/useThemeColors";
+import { ChevronLeft } from "lucide-react";
function getKeyboardKeys(showDot) {
return KEYBOARD_KEYS.map((key) => {
@@ -89,14 +88,7 @@ export default function CustomNumberKeyboard({
className={`keyboard-key ${keyClassName}`}
onClick={() => addPin(num)}
>
- {num === "back" ? (
-
- ) : (
- num
- )}
+ {num === "back" ? : num}
))}
diff --git a/src/components/customNumberKeyboard/style.css b/src/components/customNumberKeyboard/style.css
index feaa0b8..8e5ed3d 100644
--- a/src/components/customNumberKeyboard/style.css
+++ b/src/components/customNumberKeyboard/style.css
@@ -1,4 +1,5 @@
.keyboard-container {
+ width: 100%;
display: flex;
flex-direction: column;
align-items: center;
diff --git a/src/components/customSettingsNavbar/index.jsx b/src/components/customSettingsNavbar/index.jsx
index 6a1f2cf..621b1c0 100644
--- a/src/components/customSettingsNavbar/index.jsx
+++ b/src/components/customSettingsNavbar/index.jsx
@@ -1,29 +1,48 @@
-import { useNavigate } from "react-router-dom";
-import BackArrow from "../backArrow/backArrow";
import ThemeText from "../themeText/themeText";
+import BackArrow from "../backArrow/backArrow";
+import { useThemeContext } from "../../contexts/themeContext";
+import { Colors } from "../../constants/theme";
+import { useNavigate } from "react-router-dom";
import "./style.css";
-import ThemeImage from "../ThemeImage/themeImage";
-import { settingsIcon } from "../../constants/icons";
-export default function CustomSettingsNavbar({
+// Custom Settings Top Bar Component
+export default function CustomSettingsNavBar({
+ containerStyles = {},
+ textStyles = {},
text = "",
+ showLeftImage = false,
+ leftImageFunction = () => {},
+ LeftImageIcon = null,
+ leftImageStyles = {},
textClassName,
- showWhite,
- showSettings,
- settingLocation,
+ customBackFunction = null,
+ showWhite = false,
}) {
const navigate = useNavigate();
+ const handleBackClick = () => {
+ if (customBackFunction) {
+ customBackFunction();
+ return;
+ }
+ navigate(-1);
+ };
+ const { theme, darkModeType } = useThemeContext();
+
return (
-
-
+
+
+
- {showSettings && (
- navigate(`./${settingLocation}`)}
- className="settingsIcon"
- icon={settingsIcon}
+ {showLeftImage && (
+
)}
diff --git a/src/components/customSettingsNavbar/style.css b/src/components/customSettingsNavbar/style.css
index c1f5098..1e312d6 100644
--- a/src/components/customSettingsNavbar/style.css
+++ b/src/components/customSettingsNavbar/style.css
@@ -11,7 +11,7 @@
z-index: 1;
}
.pageNavBar .pageHeaderText {
- font-size: 1.5rem;
+ font-size: 1.25rem;
text-align: center;
white-space: nowrap; /* Prevent line break */
overflow: hidden; /* Hide overflow */
@@ -27,8 +27,6 @@
right: 0;
z-index: 1;
}
-@media screen and (max-width: 400px) {
- .pageNavBar .pageHeaderText {
- font-size: 20px;
- }
+.pageNavBar .right-action {
+ cursor: pointer;
}
diff --git a/src/components/emojiBar/emojiQuickBar.css b/src/components/emojiBar/emojiQuickBar.css
new file mode 100644
index 0000000..5fb2d39
--- /dev/null
+++ b/src/components/emojiBar/emojiQuickBar.css
@@ -0,0 +1,49 @@
+/* Emoji Quick Bar */
+.emoji-bar {
+ height: 50px;
+ width: 100%;
+ overflow: hidden;
+}
+
+.emoji-scroll-content {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ overflow-x: auto;
+ overflow-y: hidden;
+ height: 100%;
+ padding: 0 8px;
+}
+
+.emoji-scroll-content::-webkit-scrollbar {
+ display: none;
+}
+
+.emoji-scroll-content {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+
+.emoji-button {
+ min-width: 50px;
+ width: 50px;
+ height: 50px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ transition: transform 0.2s ease, opacity 0.2s ease;
+ flex-shrink: 0;
+}
+
+.emoji-button:hover {
+ transform: scale(1.2);
+}
+
+.emoji-button:active {
+ transform: scale(0.95);
+ opacity: 0.7;
+}
diff --git a/src/components/emojiBar/emojiQuickBar.jsx b/src/components/emojiBar/emojiQuickBar.jsx
new file mode 100644
index 0000000..93e8560
--- /dev/null
+++ b/src/components/emojiBar/emojiQuickBar.jsx
@@ -0,0 +1,392 @@
+import { useMemo, useCallback } from "react";
+
+import i18next from "i18next";
+import "./emojiQuickBar.css";
+import useThemeColors from "../../hooks/useThemeColors";
+import ThemeText from "../themeText/themeText";
+
+// Emojis mapped to keywords
+const EMOJI_KEYWORDS = {
+ en: {
+ "🍕": ["pizza", "food", "dinner", "lunch"],
+ "🍔": ["burger", "food", "dinner", "lunch", "mcdonalds", "fast food"],
+ "🌮": ["taco", "food", "dinner", "lunch", "mexican"],
+ "🍜": ["ramen", "noodles", "food", "dinner", "lunch", "soup"],
+ "🍣": ["sushi", "food", "dinner", "lunch", "japanese"],
+ "🍺": ["beer", "drink", "bar", "drinks", "alcohol"],
+ "🍻": ["beers", "drinks", "bar", "cheers", "alcohol"],
+ "☕": ["coffee", "cafe", "starbucks", "drink", "breakfast"],
+ "🍷": ["wine", "drink", "drinks", "alcohol", "dinner"],
+ "🥂": ["champagne", "drinks", "celebrate", "cheers", "alcohol"],
+ "🎉": ["party", "celebrate", "birthday", "celebration"],
+ "🎂": ["cake", "birthday", "dessert", "celebration"],
+ "🎁": ["gift", "present", "birthday", "celebration"],
+ "🎮": ["game", "gaming", "xbox", "playstation", "video game"],
+ "🎬": ["movie", "movies", "film", "cinema", "theater"],
+ "🎵": ["music", "song", "concert", "spotify"],
+ "⚽": ["soccer", "football", "sport", "game"],
+ "🏀": ["basketball", "sport", "game", "nba"],
+ "🎸": ["guitar", "music", "concert", "band"],
+ "🎤": ["karaoke", "singing", "music", "concert"],
+ "✈️": ["flight", "airport", "travel", "trip", "vacation"],
+ "🚗": ["car", "drive", "uber", "lyft", "ride"],
+ "🚕": ["taxi", "cab", "uber", "lyft", "ride"],
+ "🏠": ["home", "house", "rent", "mortgage"],
+ "🏨": ["hotel", "stay", "travel", "vacation"],
+ "⛽": ["gas", "fuel", "gasoline", "petrol"],
+ "🚇": ["subway", "metro", "train", "transit"],
+ "🚲": ["bike", "bicycle", "cycling", "ride"],
+ "💰": ["money", "cash", "payment", "pay"],
+ "💵": ["dollar", "money", "cash", "bill"],
+ "💳": ["card", "credit", "payment", "pay"],
+ "🛒": ["groceries", "shopping", "grocery", "store", "supermarket"],
+ "🎫": ["ticket", "tickets", "concert", "event", "show"],
+ "🏪": ["store", "shop", "shopping", "convenience"],
+ "❤️": ["love", "thanks", "thank you", "heart"],
+ "😂": ["funny", "lol", "haha", "laugh"],
+ "😊": ["happy", "smile", "thanks"],
+ "🙏": ["thanks", "thank you", "please", "grateful"],
+ "👍": ["good", "yes", "ok", "thanks", "great"],
+ "💯": ["perfect", "100", "great", "excellent"],
+ "🔥": ["fire", "hot", "lit", "awesome"],
+ "✨": ["sparkle", "magic", "special", "awesome"],
+ },
+ es: {
+ "🍕": ["pizza", "comida", "cena", "almuerzo"],
+ "🍔": [
+ "hamburguesa",
+ "comida",
+ "cena",
+ "almuerzo",
+ "mcdonalds",
+ "comida rápida",
+ ],
+ "🌮": ["taco", "comida", "cena", "almuerzo", "mexicana"],
+ "🍜": ["ramen", "fideos", "comida", "cena", "almuerzo", "sopa"],
+ "🍣": ["sushi", "comida", "cena", "almuerzo", "japonés"],
+ "🍺": ["cerveza", "bebida", "bar", "alcohol"],
+ "🍻": ["cervezas", "bebidas", "bar", "salud", "alcohol"],
+ "☕": ["café", "cafetería", "starbucks", "bebida", "desayuno"],
+ "🍷": ["vino", "bebida", "alcohol", "cena"],
+ "🥂": ["champán", "brindis", "celebración", "alcohol"],
+ "🎉": ["fiesta", "celebrar", "cumpleaños", "celebración"],
+ "🎂": ["pastel", "cumpleaños", "postre", "celebración"],
+ "🎁": ["regalo", "presente", "cumpleaños", "celebración"],
+ "🎮": ["juego", "gaming", "xbox", "playstation", "videojuego"],
+ "🎬": ["película", "cine", "film", "teatro"],
+ "🎵": ["música", "canción", "concierto", "spotify"],
+ "⚽": ["fútbol", "deporte", "partido"],
+ "🏀": ["baloncesto", "deporte", "nba", "partido"],
+ "🎸": ["guitarra", "música", "concierto", "banda"],
+ "🎤": ["karaoke", "cantar", "música", "concierto"],
+ "✈️": ["vuelo", "aeropuerto", "viaje", "vacaciones"],
+ "🚗": ["auto", "coche", "conducir", "uber", "viaje"],
+ "🚕": ["taxi", "uber", "viaje"],
+ "🏠": ["casa", "hogar", "renta", "alquiler"],
+ "🏨": ["hotel", "estancia", "viaje", "vacaciones"],
+ "⛽": ["gasolina", "combustible"],
+ "🚇": ["metro", "subte", "tren", "transporte"],
+ "🚲": ["bicicleta", "bici", "ciclismo", "viaje"],
+ "💰": ["dinero", "efectivo", "pago", "pagar"],
+ "💵": ["dólar", "dinero", "efectivo", "billete"],
+ "💳": ["tarjeta", "crédito", "pago"],
+ "🛒": ["compras", "supermercado", "tienda"],
+ "🎫": ["ticket", "entrada", "concierto", "evento"],
+ "🏪": ["tienda", "comercio", "supermercado pequeño"],
+ "❤️": ["amor", "gracias", "corazón"],
+ "😂": ["gracioso", "risa", "jajaja"],
+ "😊": ["feliz", "sonrisa", "gracias"],
+ "🙏": ["gracias", "por favor", "agradecido"],
+ "👍": ["bien", "sí", "ok", "gracias", "genial"],
+ "💯": ["perfecto", "excelente", "100"],
+ "🔥": ["fuego", "caliente", "genial"],
+ "✨": ["brillo", "magia", "especial", "genial"],
+ },
+ it: {
+ "🍕": ["pizza", "cibo", "cena", "pranzo"],
+ "🍔": [
+ "burger",
+ "hamburger",
+ "cibo",
+ "cena",
+ "pranzo",
+ "mcdonalds",
+ "fast food",
+ ],
+ "🌮": ["taco", "cibo", "cena", "pranzo", "messicano"],
+ "🍜": ["ramen", "noodles", "cibo", "cena", "pranzo", "zuppa"],
+ "🍣": ["sushi", "cibo", "cena", "pranzo", "giapponese"],
+ "🍺": ["birra", "bevanda", "bar", "alcol"],
+ "🍻": ["birre", "bevande", "brindisi", "alcol"],
+ "☕": ["caffè", "bar", "starbucks", "bevanda", "colazione"],
+ "🍷": ["vino", "bevanda", "alcol", "cena"],
+ "🥂": ["champagne", "brindisi", "celebrare", "alcol"],
+ "🎉": ["festa", "celebrare", "compleanno"],
+ "🎂": ["torta", "compleanno", "dessert"],
+ "🎁": ["regalo", "presente", "compleanno"],
+ "🎮": ["gioco", "gaming", "xbox", "playstation", "videogioco"],
+ "🎬": ["film", "cinema", "teatro"],
+ "🎵": ["musica", "canzone", "concerto", "spotify"],
+ "⚽": ["calcio", "sport", "partita"],
+ "🏀": ["basket", "sport", "nba"],
+ "🎸": ["chitarra", "musica", "concerto", "band"],
+ "🎤": ["karaoke", "cantare", "musica"],
+ "✈️": ["volo", "aeroporto", "viaggio", "vacanza"],
+ "🚗": ["auto", "macchina", "guidare", "uber"],
+ "🚕": ["taxi", "uber", "corsa"],
+ "🏠": ["casa", "abitazione", "affitto", "mutuo"],
+ "🏨": ["hotel", "soggiorno", "viaggio"],
+ "⛽": ["benzina", "carburante"],
+ "🚇": ["metro", "sottopassaggio", "treno", "trasporto"],
+ "🚲": ["bici", "bicicletta", "ciclismo"],
+ "💰": ["soldi", "contanti", "pagamento"],
+ "💵": ["dollaro", "soldi", "contanti"],
+ "💳": ["carta", "credito", "pagamento"],
+ "🛒": ["spesa", "supermercato", "negozio"],
+ "🎫": ["biglietto", "evento", "concerto"],
+ "🏪": ["negozio", "minimarket"],
+ "❤️": ["amore", "grazie", "cuore"],
+ "😂": ["divertente", "risata", "ahah"],
+ "😊": ["felice", "sorriso", "grazie"],
+ "🙏": ["grazie", "per favore", "grato"],
+ "👍": ["bene", "ok", "sì", "grazie"],
+ "💯": ["perfetto", "eccellente"],
+ "🔥": ["fuoco", "caldo", "fantastico"],
+ "✨": ["brillare", "magia", "speciale"],
+ },
+ "pt-BR": {
+ "🍕": ["pizza", "comida", "jantar", "almoço"],
+ "🍔": [
+ "hambúrguer",
+ "comida",
+ "jantar",
+ "almoço",
+ "mcdonalds",
+ "fast food",
+ ],
+ "🌮": ["taco", "comida", "jantar", "almoço", "mexicano"],
+ "🍜": ["lamen", "macarrão", "comida", "jantar", "almoço", "sopa"],
+ "🍣": ["sushi", "comida", "jantar", "almoço", "japonês"],
+ "🍺": ["cerveja", "bebida", "bar", "álcool"],
+ "🍻": ["cervejas", "brinde", "bebidas", "álcool"],
+ "☕": ["café", "cafeteria", "starbucks", "bebida", "café da manhã"],
+ "🍷": ["vinho", "bebida", "álcool", "jantar"],
+ "🥂": ["champanhe", "brinde", "celebrar", "álcool"],
+ "🎉": ["festa", "celebração", "aniversário"],
+ "🎂": ["bolo", "aniversário", "sobremesa"],
+ "🎁": ["presente", "gift", "aniversário"],
+ "🎮": ["jogo", "gaming", "xbox", "playstation", "videogame"],
+ "🎬": ["filme", "cinema", "teatro"],
+ "🎵": ["música", "canção", "show", "spotify"],
+ "⚽": ["futebol", "esporte", "jogo"],
+ "🏀": ["basquete", "esporte", "nba"],
+ "🎸": ["guitarra", "música", "show", "banda"],
+ "🎤": ["karaokê", "cantar", "música"],
+ "✈️": ["voo", "aeroporto", "viagem", "férias"],
+ "🚗": ["carro", "dirigir", "uber"],
+ "🚕": ["táxi", "uber", "corrida"],
+ "🏠": ["casa", "lar", "aluguel"],
+ "🏨": ["hotel", "hospedagem", "viagem"],
+ "⛽": ["gasolina", "combustível"],
+ "🚇": ["metrô", "trem", "transporte"],
+ "🚲": ["bicicleta", "bike", "ciclismo"],
+ "💰": ["dinheiro", "pagamento", "pagar"],
+ "💵": ["dólar", "dinheiro", "nota"],
+ "💳": ["cartão", "crédito", "pagamento"],
+ "🛒": ["mercado", "compras", "supermercado"],
+ "🎫": ["ingresso", "evento", "show"],
+ "🏪": ["loja", "mercadinho", "conveniência"],
+ "❤️": ["amor", "obrigado", "coração"],
+ "😂": ["engraçado", "haha", "risada"],
+ "😊": ["feliz", "sorriso", "obrigado"],
+ "🙏": ["obrigado", "por favor", "gratidão"],
+ "👍": ["bom", "ok", "sim", "obrigado"],
+ "💯": ["perfeito", "excelente"],
+ "🔥": ["fogo", "quente", "incrível"],
+ "✨": ["brilho", "mágico", "especial"],
+ },
+ "de-DE": {
+ "🍕": ["pizza", "essen", "abendessen", "mittagessen"],
+ "🍔": [
+ "burger",
+ "essen",
+ "abendessen",
+ "mittagessen",
+ "mcdonalds",
+ "fast food",
+ ],
+ "🌮": ["taco", "essen", "abendessen", "mittagessen", "mexikanisch"],
+ "🍜": ["ramen", "nudeln", "essen", "suppe"],
+ "🍣": ["sushi", "essen", "japanisch"],
+ "🍺": ["bier", "getränk", "bar", "alkohol"],
+ "🍻": ["biere", "anstoßen", "getränke", "alkohol"],
+ "☕": ["kaffee", "café", "starbucks", "getränk", "frühstück"],
+ "🍷": ["wein", "getränk", "alkohol"],
+ "🥂": ["sekt", "champagner", "anstoßen", " feiern"],
+ "🎉": ["party", "feiern", "geburtstag"],
+ "🎂": ["kuchen", "geburtstag", "dessert"],
+ "🎁": ["geschenk", "präsent", "geburtstag"],
+ "🎮": ["spiel", "gaming", "xbox", "playstation", "videospiel"],
+ "🎬": ["film", "kino", "theater"],
+ "🎵": ["musik", "lied", "konzert", "spotify"],
+ "⚽": ["fußball", "sport", "spiel"],
+ "🏀": ["basketball", "sport", "nba"],
+ "🎸": ["gitarre", "musik", "konzert", "band"],
+ "🎤": ["karaoke", "singen", "musik"],
+ "✈️": ["flug", "reise", "urlaub", "flughafen"],
+ "🚗": ["auto", "fahren", "uber", "fahrt"],
+ "🚕": ["taxi", "fahrt"],
+ "🏠": ["haus", "heim", "miete"],
+ "🏨": ["hotel", "aufenthalt", "reise"],
+ "⛽": ["benzin", "kraftstoff"],
+ "🚇": ["u-bahn", "bahn", "zug", "verkehr"],
+ "🚲": ["fahrrad", "radfahren"],
+ "💰": ["geld", "zahlung"],
+ "💵": ["dollar", "geld", "schein"],
+ "💳": ["karte", "kreditkarte", "zahlung"],
+ "🛒": ["einkauf", "supermarkt", "laden"],
+ "🎫": ["ticket", "eintritt", "event"],
+ "🏪": ["laden", "geschäft", "kiosk"],
+ "❤️": ["liebe", "danke", "herz"],
+ "😂": ["lustig", "lol", "lachen"],
+ "😊": ["glücklich", "lächeln", "danke"],
+ "🙏": ["danke", "bitte", "dankbar"],
+ "👍": ["gut", "ok", "ja", "danke"],
+ "💯": ["perfekt", "super"],
+ "🔥": ["feuer", "heiß", "cool"],
+ "✨": ["glitzer", "magisch", "besonders"],
+ },
+};
+
+const ALL_EMOJIS = [
+ "🍕",
+ "🍔",
+ "☕",
+ "🍺",
+ "🚗",
+ "⛽",
+ "🏠",
+ "💰",
+ "🎉",
+ "❤️",
+ "🌮",
+ "🍜",
+ "🍣",
+ "🍻",
+ "🍷",
+ "🥂",
+ "🎂",
+ "🎁",
+ "🎮",
+ "🎬",
+ "🎵",
+ "⚽",
+ "🏀",
+ "🎸",
+ "🎤",
+ "✈️",
+ "🚕",
+ "🏨",
+ "🚇",
+ "🚲",
+ "💵",
+ "💳",
+ "🛒",
+ "🎫",
+ "🏪",
+ "😂",
+ "😊",
+ "🙏",
+ "👍",
+ "💯",
+ "🔥",
+ "✨",
+];
+
+// Default emoji order (most common first)
+const DEFAULT_EMOJI_ORDER = ["💵", "🏠", "⛽", "🍕", "☕", "🎁", "🎉", "🎫"];
+
+const EmojiQuickBar = ({ description = "", onEmojiSelect }) => {
+ const { backgroundOffset } = useThemeColors();
+
+ const defalutItems = useMemo(() => {
+ return DEFAULT_EMOJI_ORDER.map((item) => ({
+ emoji: item,
+ shouldReplace: false,
+ score: 0,
+ }));
+ }, []);
+
+ const sortedEmojis = useMemo(() => {
+ const splitString = description.split(" ");
+ const currentWord = splitString[splitString.length - 1] || "";
+ if (!currentWord.trim()) {
+ return defalutItems;
+ }
+
+ const lowerDescription = currentWord.toLowerCase();
+ const scored = ALL_EMOJIS.map((emoji) => {
+ const keywords = EMOJI_KEYWORDS[i18next.language][emoji] || [];
+
+ const shouldReplace = keywords[0]
+ ?.toLowerCase()
+ .startsWith(lowerDescription);
+ const score = keywords.reduce((count, keyword) => {
+ return count + (keyword.includes(lowerDescription) ? 1 : 0);
+ }, 0);
+ if (score === 0) return false;
+ return { emoji, score, shouldReplace };
+ }).filter(Boolean);
+
+ if (!scored.length) return defalutItems;
+
+ return scored
+ .sort((a, b) => {
+ if (b.score !== a.score) return b.score - a.score;
+ return ALL_EMOJIS.indexOf(a.emoji) - ALL_EMOJIS.indexOf(b.emoji);
+ })
+ .map((item) => item);
+ }, [description, defalutItems]);
+
+ const createDescription = useCallback(
+ (emoji) => {
+ let newDescription = "";
+ if (emoji.shouldReplace) {
+ let prevDescription = description.split(" ");
+ prevDescription.pop();
+ newDescription = prevDescription.join(" ") + emoji.emoji;
+ } else {
+ newDescription =
+ description.trim() +
+ (description.trim().length ? " " : "") +
+ emoji.emoji;
+ }
+
+ onEmojiSelect(newDescription + " ");
+ },
+ [description, onEmojiSelect]
+ );
+
+ return (
+
+
+ {sortedEmojis.map((emoji, index) => (
+
+ ))}
+
+
+ );
+};
+
+export default EmojiQuickBar;
diff --git a/src/components/formattedBalanceInput/formattedBalanceInput.css b/src/components/formattedBalanceInput/formattedBalanceInput.css
new file mode 100644
index 0000000..62b6d54
--- /dev/null
+++ b/src/components/formattedBalanceInput/formattedBalanceInput.css
@@ -0,0 +1,72 @@
+/* Formatted Balance Input Container */
+.formatted-balance-input-container {
+ width: 90%;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ margin: 0 auto;
+ cursor: pointer;
+}
+.formatted-balance-input-container p {
+ margin: 0;
+ text-transform: uppercase;
+}
+
+.input-wrapper {
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ margin-right: 3px;
+}
+
+.scroll-container {
+ width: 100%;
+ overflow-x: auto;
+ overflow-y: hidden;
+ display: flex;
+}
+
+.scroll-container::-webkit-scrollbar {
+ display: none;
+}
+
+.scroll-container {
+ flex-shrink: 1;
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ overflow-x: auto;
+ overflow-y: hidden;
+ display: flex;
+ white-space: nowrap;
+}
+
+/* Balance Text Input */
+.balance-text-input {
+ font-size: 40px;
+ background: none;
+ border: none;
+ outline: none;
+ padding: 0;
+ margin: 0;
+ pointer-events: none;
+ user-select: none;
+ min-width: fit-content;
+ text-align: center;
+}
+
+.balance-text-input:focus {
+ outline: none;
+}
+
+/* Hidden Text for Measurement */
+.hidden-text {
+ position: absolute;
+ z-index: -1;
+ font-size: 40px;
+ opacity: 0;
+ pointer-events: none;
+ white-space: nowrap;
+ visibility: hidden;
+}
diff --git a/src/components/formattedBalanceInput/formattedBalanceInput.jsx b/src/components/formattedBalanceInput/formattedBalanceInput.jsx
new file mode 100644
index 0000000..eaf6c09
--- /dev/null
+++ b/src/components/formattedBalanceInput/formattedBalanceInput.jsx
@@ -0,0 +1,184 @@
+import { useMemo, useState, useRef, useEffect } from "react";
+import {
+ BITCOIN_SAT_TEXT,
+ BITCOIN_SATS_ICON,
+ HIDDEN_OPACITY,
+} from "../../constants";
+import "./formattedBalanceInput.css";
+import { useNodeContext } from "../../contexts/nodeContext";
+import { formatCurrency } from "../../functions/formatCurrency";
+import ThemeText from "../themeText/themeText";
+import formatBalanceAmount from "../../functions/formatNumber";
+import { useGlobalContextProvider } from "../../contexts/masterInfoObject";
+import useThemeColors from "../../hooks/useThemeColors";
+
+export default function FormattedBalanceInput({
+ amountValue = 0,
+ containerFunction,
+ inputDenomination,
+ customTextInputContainerStyles,
+ customTextInputStyles,
+ activeOpacity = 0.2,
+ maxWidth = 0.95,
+ customCurrencyCode = "",
+}) {
+ const containerRef = useRef(null);
+ const amountRef = useRef(null);
+ const labelRef = useRef(null);
+
+ const { masterInfoObject } = useGlobalContextProvider();
+ const { textColor } = useThemeColors();
+ const { fiatStats } = useNodeContext();
+
+ const currencyText = fiatStats.coin || "USD";
+ const showSymbol = masterInfoObject.satDisplay !== "word";
+
+ const formattedAmount = formatBalanceAmount(
+ amountValue,
+ false,
+ masterInfoObject
+ );
+
+ const currencyInfo = useMemo(
+ () => formatCurrency({ amount: 0, code: currencyText }),
+ [currencyText]
+ );
+
+ const isSymbolInFront = currencyInfo[3];
+ const currencySymbol = currencyInfo[2];
+
+ const showSats =
+ inputDenomination === "sats" || inputDenomination === "hidden";
+
+ const displayText = useMemo(() => {
+ if (customCurrencyCode) return formattedAmount;
+ return formattedAmount;
+ }, [formattedAmount, customCurrencyCode]);
+
+ const fontSize = useAutoScaleCompositeFont({
+ containerRef,
+ amountRef,
+ labelRef,
+ baseFontSize: 40,
+ minFontSize: 24,
+ padding: 12,
+ displayText,
+ });
+
+ return (
+
+ {isSymbolInFront && !showSats && showSymbol && (
+
+ )}
+
+ {showSats && showSymbol && (
+
+ )}
+
+
+
+ {!isSymbolInFront && !showSats && showSymbol && (
+
+ )}
+
+ {!showSymbol && !showSats && (
+
+ )}
+
+ {!showSymbol && showSats && (
+
+ )}
+
+ );
+}
+function useAutoScaleCompositeFont({
+ containerRef,
+ amountRef,
+ labelRef,
+ baseFontSize = 40,
+ minFontSize = 24,
+ padding = 16,
+ displayText,
+}) {
+ const [fontSize, setFontSize] = useState(baseFontSize);
+
+ useEffect(() => {
+ const container = containerRef.current;
+ const amountEl = amountRef.current;
+ const labelEl = labelRef.current;
+
+ if (!container || !amountEl) return;
+
+ const containerWidth = container.offsetWidth;
+ if (!containerWidth) return;
+
+ // Force base size for measurement
+ amountEl.style.fontSize = baseFontSize + "px";
+ if (labelEl) labelEl.style.fontSize = baseFontSize + "px";
+
+ const amountWidth = amountEl.offsetWidth;
+ const labelWidth = labelEl ? labelEl.offsetWidth : 0;
+
+ const totalWidth = amountWidth + labelWidth;
+
+ const available = containerWidth - padding * 2;
+
+ let nextSize = baseFontSize;
+
+ if (totalWidth > available) {
+ const ratio = available / totalWidth;
+ nextSize = Math.max(minFontSize, Math.floor(baseFontSize * ratio));
+ }
+
+ setFontSize(nextSize);
+ }, [
+ containerRef,
+ amountRef,
+ labelRef,
+ baseFontSize,
+ minFontSize,
+ padding,
+ displayText,
+ ]);
+
+ return fontSize;
+}
diff --git a/src/components/formattedSatText/formattedSatText.jsx b/src/components/formattedSatText/formattedSatText.jsx
index 12c666f..886911f 100644
--- a/src/components/formattedSatText/formattedSatText.jsx
+++ b/src/components/formattedSatText/formattedSatText.jsx
@@ -26,6 +26,7 @@ export default function FormattedSatText({
useCustomLabel = false,
customLabel = "",
useMillionDenomination = false,
+ useSizing = false,
}) {
const { masterInfoObject } = useGlobalContextProvider();
const { fiatStats } = useNodeContext();
@@ -80,7 +81,19 @@ export default function FormattedSatText({
+ );
+ const hiddenText = (content, key, extra = {}) => (
+
);
@@ -91,7 +104,36 @@ export default function FormattedSatText({
if (!shouldShowAmount) {
children = [
frontText && renderText(frontText),
- renderText(HIDDEN_BALANCE_TEXT),
+ hiddenText(HIDDEN_BALANCE_TEXT, 1, {
+ fontSize: styles?.fontSize
+ ? `calc(${styles.fontSize} * ${useSizing ? 0.8 : 1})`
+ : `calc(1em * ${useSizing ? 0.8 : 0.65})`,
+ margin: "0 2px",
+ }),
+ hiddenText(HIDDEN_BALANCE_TEXT, 2, {
+ fontSize: styles?.fontSize
+ ? `calc(${styles.fontSize} * ${useSizing ? 0.9 : 1})`
+ : `calc(1em * ${useSizing ? 0.9 : 0.65})`,
+ margin: "0 2px",
+ }),
+ hiddenText(HIDDEN_BALANCE_TEXT, 3, {
+ fontSize: styles?.fontSize
+ ? `calc(${styles.fontSize} * ${useSizing ? 1 : 1})`
+ : `calc(1em * ${useSizing ? 1 : 0.65})`,
+ margin: "0 2px",
+ }),
+ hiddenText(HIDDEN_BALANCE_TEXT, 4, {
+ fontSize: styles?.fontSize
+ ? `calc(${styles.fontSize} * ${useSizing ? 0.9 : 1})`
+ : `calc(1em * ${useSizing ? 0.9 : 0.65})`,
+ margin: "0 2px",
+ }),
+ hiddenText(HIDDEN_BALANCE_TEXT, 5, {
+ fontSize: styles?.fontSize
+ ? `calc(${styles.fontSize} * ${useSizing ? 0.8 : 1})`
+ : `calc(1em * ${useSizing ? 0.8 : 0.65})`,
+ margin: "0 2px",
+ }),
backText && renderText(backText, { marginLeft: 5 }),
];
}
diff --git a/src/components/navBar/navbar.css b/src/components/navBar/navbar.css
index 9368313..ee229f5 100644
--- a/src/components/navBar/navbar.css
+++ b/src/components/navBar/navbar.css
@@ -11,7 +11,7 @@
z-index: 1;
}
.pageNavBar .pageHeaderText {
- font-size: 1.5rem;
+ font-size: 1.25rem;
text-align: center;
white-space: nowrap; /* Prevent line break */
overflow: hidden; /* Hide overflow */
@@ -21,8 +21,3 @@
padding: 0 35px;
margin: 0;
}
-@media screen and (max-width: 400px) {
- .pageNavBar .pageHeaderText {
- font-size: 20px;
- }
-}
diff --git a/src/components/navBar/profileImage.css b/src/components/navBar/profileImage.css
new file mode 100644
index 0000000..374259c
--- /dev/null
+++ b/src/components/navBar/profileImage.css
@@ -0,0 +1,12 @@
+.navbarProfileImageContainer {
+ width: 35px;
+ height: 35px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ position: relative;
+ z-index: 99;
+ overflow: hidden;
+}
diff --git a/src/components/navBar/profileImage.jsx b/src/components/navBar/profileImage.jsx
new file mode 100644
index 0000000..054d82c
--- /dev/null
+++ b/src/components/navBar/profileImage.jsx
@@ -0,0 +1,32 @@
+import { useNavigate } from "react-router-dom";
+import useThemeColors from "../../hooks/useThemeColors";
+
+import { useImageCache } from "../../contexts/imageCacheContext";
+import { useGlobalContextProvider } from "../../contexts/masterInfoObject";
+import { useThemeContext } from "../../contexts/themeContext";
+import ContactProfileImage from "../../pages/contacts/components/profileImage/profileImage";
+import "./profileImage.css";
+
+export default function NavBarProfileImage() {
+ const { backgroundOffset } = useThemeColors();
+ const { cache } = useImageCache();
+ const { masterInfoObject } = useGlobalContextProvider();
+ const { theme, darkModeType } = useThemeContext();
+ const navigate = useNavigate();
+ return (
+
{
+ navigate("/settings");
+ }}
+ >
+
+
+ );
+}
diff --git a/src/components/navBarWithBalance/navBarWithBalance.css b/src/components/navBarWithBalance/navBarWithBalance.css
new file mode 100644
index 0000000..2d532d9
--- /dev/null
+++ b/src/components/navBarWithBalance/navBarWithBalance.css
@@ -0,0 +1,31 @@
+/* Navigation Bar with Balance */
+.nav-bar-top-bar {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ min-height: 40px;
+ position: relative;
+}
+
+.nav-bar-back-arrow {
+ position: absolute;
+ z-index: 99;
+ left: 0;
+ background: none;
+ border: none;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+}
+
+.nav-bar-container {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ flex-grow: 1;
+ padding: 0 35px;
+ justify-content: center;
+}
diff --git a/src/components/navBarWithBalance/navbarWithBalance.jsx b/src/components/navBarWithBalance/navbarWithBalance.jsx
new file mode 100644
index 0000000..a0e9001
--- /dev/null
+++ b/src/components/navBarWithBalance/navbarWithBalance.jsx
@@ -0,0 +1,75 @@
+import { useNavigate } from "react-router-dom";
+import "./navBarWithBalance.css";
+import { ArrowLeft, Wallet2 } from "lucide-react";
+import { useThemeContext } from "../../contexts/themeContext";
+import { Colors } from "../../constants/theme";
+import FormattedSatText from "../formattedSatText/formattedSatText";
+import { useSpark } from "../../contexts/sparkContext";
+import { formatTokensNumber } from "../../functions/lrc20/formatTokensBalance";
+
+export default function NavBarWithBalance({
+ backFunction,
+ seletctedToken,
+ selectedLRC20Asset = "Bitcoin",
+ showBalance = true,
+}) {
+ const { theme, darkModeType } = useThemeContext();
+ const navigate = useNavigate();
+ const { sparkInformation } = useSpark();
+ const balance = seletctedToken?.balance || sparkInformation.balance;
+
+ const formattedTokensBalance =
+ selectedLRC20Asset !== "Bitcoin"
+ ? formatTokensNumber(balance, seletctedToken?.tokenMetadata?.decimals)
+ : balance;
+
+ const handleBack = () => {
+ if (backFunction) {
+ backFunction();
+ } else {
+ navigate(-1);
+ }
+ };
+
+ return (
+
+
+
+ {showBalance && (
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/overlayHost.jsx b/src/components/overlayHost.jsx
new file mode 100644
index 0000000..5b2e678
--- /dev/null
+++ b/src/components/overlayHost.jsx
@@ -0,0 +1,47 @@
+import { useMemo } from "react";
+import { AnimatePresence } from "framer-motion";
+
+import { useOverlay } from "../contexts/overlayContext.jsx";
+import InformationPopup from "../pages/informationPopup/index.jsx";
+import CustomHalfModal from "../pages/customHalfModal/index.jsx";
+import ErrorScreen from "../pages/error/error.jsx";
+import ConfirmActionPage from "./confirmActionPage/confirmActionPage.jsx";
+
+export default function OverlayHost() {
+ const { overlays, closeOverlay, openOverlay } = useOverlay();
+
+ const overlayElements = useMemo(
+ () =>
+ overlays.map((overlay, index) => (
+
+ {overlay.for === "confirm-action" && (
+
+ )}
+
+ {overlay.for === "error" && (
+
+ )}
+
+ {overlay.for === "halfModal" && (
+
+ )}
+
+ {overlay.for === "informationPopup" && (
+
+ )}
+
+ )),
+ [overlays, closeOverlay, openOverlay]
+ );
+
+ return
{overlayElements};
+}
diff --git a/src/components/sendAndRequsetButton/customSendAndRequestBTN.css b/src/components/sendAndRequsetButton/customSendAndRequestBTN.css
new file mode 100644
index 0000000..fa86b7c
--- /dev/null
+++ b/src/components/sendAndRequsetButton/customSendAndRequestBTN.css
@@ -0,0 +1,17 @@
+/* Send and Request Button */
+.send-request-button {
+ background: none;
+ border: none;
+ padding: 0;
+ cursor: pointer;
+}
+
+/* Scan QR Icon Container */
+.scan-qr-icon {
+ width: 70px;
+ height: 70px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 35px;
+}
diff --git a/src/components/sendAndRequsetButton/customSendAndRequsetButton.jsx b/src/components/sendAndRequsetButton/customSendAndRequsetButton.jsx
new file mode 100644
index 0000000..415bb65
--- /dev/null
+++ b/src/components/sendAndRequsetButton/customSendAndRequsetButton.jsx
@@ -0,0 +1,54 @@
+import { ArrowDown, ArrowUp } from "lucide-react";
+import "./customSendAndRequestBTN.css";
+
+import { useThemeContext } from "../../contexts/themeContext";
+import { Colors } from "../../constants/theme";
+
+export default function CustomSendAndRequsetBTN({
+ btnType,
+ btnFunction,
+ arrowColor,
+ containerBackgroundColor,
+ height = 40,
+ width = 40,
+ containerStyles,
+ activeOpacity = 0.2,
+}) {
+ const { theme, darkModeType } = useThemeContext();
+ return (
+
+ );
+}
diff --git a/src/components/tabsIcon/tabsIcon.jsx b/src/components/tabsIcon/tabsIcon.jsx
deleted file mode 100644
index d94a0d3..0000000
--- a/src/components/tabsIcon/tabsIcon.jsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import {
- adminHomeWallet,
- appstore,
- appstoreFilled,
- contactsIconBlue,
- contactsIconBlueSelected,
- walletBlueIcon,
-} from "../../constants/icons";
-import ThemeImage from "../ThemeImage/themeImage";
-
-export default function TabsIcon({ value, icon }) {
- let imgSrc =
- icon === "contacts"
- ? value === 0
- ? contactsIconBlueSelected
- : contactsIconBlue
- : icon === "wallet"
- ? value === 1
- ? walletBlueIcon
- : adminHomeWallet
- : value === 2
- ? appstoreFilled
- : appstore;
- return (
-
- );
-}
diff --git a/src/components/themeText/themeText.jsx b/src/components/themeText/themeText.jsx
index 2651608..240c7a6 100644
--- a/src/components/themeText/themeText.jsx
+++ b/src/components/themeText/themeText.jsx
@@ -9,6 +9,7 @@ export default function ThemeText({
className,
reversed,
clickFunction,
+ ref,
}) {
const { theme } = useThemeContext();
@@ -27,6 +28,7 @@ export default function ThemeText({
);
return (
{
if (clickFunction) {
clickFunction();
diff --git a/src/components/transactionContainer/style.css b/src/components/transactionContainer/style.css
index e67717d..85f1134 100644
--- a/src/components/transactionContainer/style.css
+++ b/src/components/transactionContainer/style.css
@@ -36,6 +36,7 @@
margin: 0;
}
.transactionContainer .transaction .textContainer {
+ width: calc(100% - 45px - 72px - 10px - 10px);
display: flex;
flex-direction: column;
margin-right: 10px;
diff --git a/src/components/transactionContainer/transactionContianer.jsx b/src/components/transactionContainer/transactionContianer.jsx
index ac7baeb..724689e 100644
--- a/src/components/transactionContainer/transactionContianer.jsx
+++ b/src/components/transactionContainer/transactionContianer.jsx
@@ -11,14 +11,13 @@ import {
} from "../../constants";
import ThemeText from "../themeText/themeText";
import { useThemeContext } from "../../contexts/themeContext";
-import { pendingTx, smallArrowLeft } from "../../constants/icons";
-import ThemeImage from "../ThemeImage/themeImage";
import { useTranslation } from "react-i18next";
import { useMemo } from "react";
import { formatTokensNumber } from "../../functions/lrc20/formatTokensBalance";
import { ArrowDown, ArrowUp, Clock } from "lucide-react";
import { Colors } from "../../constants/theme";
import useThemeColors from "../../hooks/useThemeColors";
+import { useAppStatus } from "../../contexts/appStatus";
const TRANSACTION_CONSTANTS = {
VIEW_ALL_PAGE: "viewAllTx",
SPARK_WALLET: "sparkWallet",
@@ -33,6 +32,7 @@ const TRANSACTION_CONSTANTS = {
export default function TransactionContanier({ frompage }) {
const { t } = useTranslation();
+ const { didGetToHomepage } = useAppStatus();
const { sparkInformation } = useSpark();
const { masterInfoObject } = useGlobalContextProvider();
const currentTime = new Date();
@@ -42,7 +42,11 @@ export default function TransactionContanier({ frompage }) {
const userBalanceDenomination = masterInfoObject?.userBalanceDenomination;
const homepageTxPreferance = masterInfoObject?.homepageTxPreferance;
- if (frompage === "home" && sparkInformation.didConnect === null) {
+ if (
+ !sparkInformation.didConnect ||
+ !sparkInformation.identityPubKey ||
+ !didGetToHomepage
+ ) {
return (
@@ -102,14 +106,7 @@ export default function TransactionContanier({ frompage }) {
const transactionPaymentType = currentTransaction.paymentType;
const paymentStatus = currentTransaction.paymentStatus;
- const paymentDetails =
- frompage === TRANSACTION_CONSTANTS.SPARK_WALLET
- ? {
- time: currentTransaction.createdTime,
- direction: currentTransaction.transferDirection,
- amount: currentTransaction.totalValue,
- }
- : JSON.parse(currentTransaction.details);
+ const paymentDetails = JSON.parse(currentTransaction.details);
const isLRC20Payment = paymentDetails.isLRC20Payment;
@@ -162,7 +159,7 @@ export default function TransactionContanier({ frompage }) {
{
+// if (!pendingNavigation) return;
+// if (!didGetToHomepage) {
+// setPendingNavigation(null);
+// return;
+// }
+// if (isNavigating.current) return;
+// crashlyticsLogReport(`Navigating to confirm tx page in roostock listener`);
+// isNavigating.current = true;
+
+// setTimeout(() => {
+// requestAnimationFrame(() => {
+// navigation.navigate("ErrorScreen", {
+// errorMessage: i18next.t("errormessages.receivedRootstock"),
+// });
+// isNavigating.current = false;
+// console.log("cleaning up navigation for rootstock");
+// });
+// }, 100);
+
+// setPendingNavigation(null);
+// }, [pendingNavigation, didGetToHomepage]);
+
+// return null;
+// }
+
+// export function LiquidNavigationListener() {
+// const navigation = useNavigation();
+// const { didGetToHomepage } = useAppStatus();
+// const { pendingLiquidPayment, setPendingLiquidPayment } = useNodeContext();
+// const isNavigating = useRef(false); // Use a ref for local state
+
+// useEffect(() => {
+// if (!pendingLiquidPayment) return;
+// if (!didGetToHomepage) {
+// setPendingLiquidPayment(null);
+// return;
+// }
+// if (isNavigating.current) return;
+// crashlyticsLogReport(`Navigating to confirm tx page in liquid listener `);
+// isNavigating.current = true;
+
+// setTimeout(() => {
+// requestAnimationFrame(() => {
+// navigation.navigate("ErrorScreen", {
+// errorMessage: i18next.t("errormessages.receivedLiquid"),
+// });
+// isNavigating.current = false;
+// console.log("cleaning up navigation for liquid");
+// });
+// }, 100);
+
+// setPendingLiquidPayment(null);
+// }, [pendingLiquidPayment, didGetToHomepage]);
+
+// return null;
+// }
+
+export function SparkNavigationListener() {
+ const navigate = useNavigate();
+ const { didGetToHomepage } = useAppStatus();
+ const { pendingNavigation, setPendingNavigation } = useSpark();
+ const isNavigating = useRef(false); // Use a ref for local state
+ const { showToast } = useToast();
+
+ useEffect(() => {
+ console.log(pendingNavigation, didGetToHomepage, "in navigation");
+ if (!pendingNavigation) return;
+ if (!didGetToHomepage) {
+ setPendingNavigation(null);
+ return;
+ }
+ if (isNavigating.current) return;
+ isNavigating.current = true;
+
+ setTimeout(() => {
+ if (pendingNavigation.showFullAnimation) {
+ navigate("/confirm-page", {
+ state: {
+ for: "paymentsucceed",
+ transaction: pendingNavigation.tx,
+ },
+ replace: true,
+ });
+ } else {
+ showToast({
+ amount: pendingNavigation.amount,
+ LRC20Token: pendingNavigation.LRC20Token,
+ isLRC20Payment: pendingNavigation.isLRC20Payment,
+ duration: 7000,
+ type: "confirmTx",
+ });
+ }
+ console.log("cleaning up navigation for spark");
+ isNavigating.current = false;
+ }, 100);
+
+ setPendingNavigation(null);
+ }, [pendingNavigation, didGetToHomepage]);
+
+ return null;
+}
diff --git a/src/contexts/activeAccount.jsx b/src/contexts/activeAccount.jsx
index 849124d..d115ab0 100644
--- a/src/contexts/activeAccount.jsx
+++ b/src/contexts/activeAccount.jsx
@@ -54,8 +54,8 @@ export const ActiveCustodyAccountProvider = ({ children }) => {
}
}
- console.log("Initializing accounts....");
if (!accountMnemoinc) return;
+ console.log("Initializing accounts....");
initializeAccouts();
}, [accountMnemoinc]);
diff --git a/src/contexts/globalContacts.jsx b/src/contexts/globalContacts.jsx
index 1123648..3559f0a 100644
--- a/src/contexts/globalContacts.jsx
+++ b/src/contexts/globalContacts.jsx
@@ -19,14 +19,17 @@ import {
import {
CONTACTS_TRANSACTION_UPDATE_NAME,
contactsSQLEventEmitter,
+ deleteCachedMessages,
getCachedMessages,
queueSetCashedMessages,
} from "../functions/messaging/cachedMessages";
import { db } from "../../db/initializeFirebase";
import { useKeysContext } from "./keysContext";
import {
+ and,
collection,
onSnapshot,
+ or,
orderBy,
query,
startAfter,
@@ -43,41 +46,55 @@ export const GlobalContactsList = ({ children }) => {
);
const [contactsMessags, setContactsMessagses] = useState({});
const [decodedAddedContacts, setDecodedAddedContacts] = useState([]);
+ const [updateDB, setUpdateDB] = useState(null);
const didTryToUpdate = useRef(false);
- const lookForNewMessages = useRef(false);
+ const lookForNewMessages = useRef(true);
const unsubscribeMessagesRef = useRef(null);
const unsubscribeSentMessagesRef = useRef(null);
const pendingWrite = useRef(null);
+ const globalContactsInformationRef = useRef(globalContactsInformation);
+ const decodedAddedContactsRef = useRef([]);
const addedContacts = globalContactsInformation.addedContacts;
- const toggleGlobalContactsInformation = useCallback((newData, writeToDB) => {
- console.log("WRITING TO DATABASE TWICE (should only see once)");
+ useEffect(() => {
+ globalContactsInformationRef.current = globalContactsInformation;
+ }, [globalContactsInformation]);
+
+ const toggleGlobalContactsInformation = useCallback(
+ (newData, writeToDB) => {
+ setUpdateDB({ newData, writeToDB });
+ },
+ [publicKey]
+ );
- setGlobalContactsInformation((prev) => {
- const newContacts = { ...prev, ...newData };
+ useEffect(() => {
+ if (!updateDB) return;
+ async function handleUpdate() {
+ const { newData, writeToDB } = updateDB;
+ const newContacts = {
+ ...globalContactsInformationRef.current,
+ ...newData,
+ };
+ setGlobalContactsInformation(newContacts);
if (writeToDB) {
- // Store the data we want to write outside the updater
- pendingWrite.current = newContacts;
+ addDataToCollection(
+ { contacts: newContacts },
+ "blitzWalletUsers",
+ publicKey
+ );
}
- return newContacts;
- });
- }, []);
+ setUpdateDB(null);
+ }
+ handleUpdate();
+ }, [updateDB]);
useEffect(() => {
- if (pendingWrite.current) {
- console.log("RUNNING IN WRITE TO DB (should only see once)");
- addDataToCollection(
- { contacts: pendingWrite.current },
- "blitzWalletUsers",
- publicKey
- );
- pendingWrite.current = null;
- }
- }, [globalContactsInformation, publicKey]);
+ decodedAddedContactsRef.current = decodedAddedContacts;
+ }, [decodedAddedContacts]);
useEffect(() => {
if (!publicKey || !addedContacts) return;
@@ -114,7 +131,7 @@ export const GlobalContactsList = ({ children }) => {
.filter((key) => key !== "lastMessageTimestamp")
.filter(
(contact) =>
- !decodedAddedContacts.find(
+ !decodedAddedContactsRef.current.find(
(contactElement) => contactElement.uuid === contact
) && contact !== globalContactsInformation.myProfile.uuid
)
@@ -142,16 +159,16 @@ export const GlobalContactsList = ({ children }) => {
toggleGlobalContactsInformation(
{
myProfile: { ...globalContactsInformation.myProfile },
- addedContacts: encryptMessage(
+ addedContacts: await encryptMessage(
contactsPrivateKey,
globalContactsInformation.myProfile.uuid,
- JSON.stringify(decodedAddedContacts.concat(newContats))
+ JSON.stringify(decodedAddedContactsRef.current.concat(newContats))
),
},
true
);
}
- }, [globalContactsInformation, decodedAddedContacts, contactsPrivateKey]);
+ }, [globalContactsInformation, contactsPrivateKey]);
useEffect(() => {
async function handleUpdate(updateType) {
@@ -174,92 +191,457 @@ export const GlobalContactsList = ({ children }) => {
};
}, [updatedCachedMessagesStateFunction]);
+ const updateContactUniqueName = useCallback(
+ async (newUniqueNames) => {
+ try {
+ if (newUniqueNames.size === 0) {
+ return;
+ }
+ let newValue = null;
+ try {
+ // Validate prerequisites
+ if (!contactsPrivateKey || !publicKey) {
+ console.warn("Missing required data for contact update");
+ return;
+ }
+
+ let currentContacts;
+ try {
+ const decryptedData = await decryptMessage(
+ contactsPrivateKey,
+ publicKey,
+ globalContactsInformationRef.current.addedContacts
+ );
+
+ if (!decryptedData) {
+ console.warn("Decryption returned empty data");
+ return;
+ }
+
+ currentContacts = JSON.parse(decryptedData);
+
+ // Validate parsed data
+ if (!Array.isArray(currentContacts)) {
+ console.warn("Decrypted contacts is not an array");
+ return;
+ }
+ } catch (decryptError) {
+ console.error(
+ "Failed to decode contacts for update:",
+ decryptError
+ );
+ return;
+ }
+
+ let hasChanges = false;
+ const updatedContacts = currentContacts.map((contact) => {
+ try {
+ const newUniqueName = newUniqueNames.get(contact.uuid);
+
+ if (
+ newUniqueName &&
+ typeof newUniqueName === "string" &&
+ newUniqueName.trim() !== "" &&
+ newUniqueName !== contact.uniqueName
+ ) {
+ hasChanges = true;
+ return {
+ ...contact,
+ uniqueName: newUniqueName,
+ };
+ }
+
+ return contact;
+ } catch (mapError) {
+ console.error("Error processing contact:", mapError);
+ return contact;
+ }
+ });
+
+ if (!hasChanges) {
+ return;
+ }
+
+ try {
+ const newEncryptedContacts = await encryptMessage(
+ contactsPrivateKey,
+ publicKey,
+ JSON.stringify(updatedContacts)
+ );
+
+ if (!newEncryptedContacts) {
+ console.error("Encryption failed, aborting update");
+ return;
+ }
+
+ addDataToCollection(
+ {
+ contacts: {
+ ...globalContactsInformationRef.current,
+ addedContacts: newEncryptedContacts,
+ },
+ },
+ "blitzWalletUsers",
+ publicKey
+ ).catch((dbError) => {
+ console.error("Failed to save contacts to database:", dbError);
+ });
+
+ newValue = {
+ ...globalContactsInformationRef.current,
+ addedContacts: newEncryptedContacts,
+ };
+ } catch (encryptError) {
+ console.error("Failed to encrypt updated contacts:", encryptError);
+ return;
+ }
+ } catch (stateError) {
+ console.error("Error in state update function:", stateError);
+ return;
+ }
+
+ if (newValue) {
+ setGlobalContactsInformation(newValue);
+ }
+ } catch (outerError) {
+ console.error("Critical error in updateContactUniqueName:", outerError);
+ }
+ },
+ [contactsPrivateKey, publicKey]
+ );
+
useEffect(() => {
- return;
if (!Object.keys(globalContactsInformation).length) return;
const now = new Date().getTime();
// Unsubscribe from previous listeners before setting new ones
if (unsubscribeMessagesRef.current) {
unsubscribeMessagesRef.current();
}
- if (unsubscribeSentMessagesRef.current) {
- unsubscribeSentMessagesRef.current();
- }
- const inboundMessageQuery = query(
- collection(db, "contactMessages"),
- where("toPubKey", "==", globalContactsInformation.myProfile.uuid),
- orderBy("timestamp"),
- startAfter(now)
- );
- const outbounddMessageQuery = query(
+
+ const combinedMessageQuery = query(
collection(db, "contactMessages"),
- where("fromPubKey", "==", globalContactsInformation.myProfile.uuid),
- orderBy("timestamp"),
- startAfter(now)
+ and(
+ where("timestamp", ">", now),
+ or(
+ where("toPubKey", "==", globalContactsInformation.myProfile.uuid),
+ where("fromPubKey", "==", globalContactsInformation.myProfile.uuid)
+ )
+ ),
+ orderBy("timestamp")
);
+
// Set up the realtime listener
unsubscribeMessagesRef.current = onSnapshot(
- inboundMessageQuery,
- (snapshot) => {
- if (!snapshot?.docChanges()?.length) return;
- snapshot.docChanges().forEach((change) => {
- console.log("received a new message", change.type);
- if (change.type === "added") {
- const newMessage = change.doc.data();
- queueSetCashedMessages({
- newMessagesList: [newMessage],
- myPubKey: globalContactsInformation.myProfile.uuid,
- });
- }
- });
- }
- );
- unsubscribeSentMessagesRef.current = onSnapshot(
- outbounddMessageQuery,
- (snapshot) => {
- if (!snapshot?.docChanges()?.length) return;
- snapshot.docChanges().forEach((change) => {
- console.log("sent a new message", change.type);
- if (change.type === "added") {
+ combinedMessageQuery,
+ async (snapshot) => {
+ const changes = snapshot?.docChanges();
+ if (!changes?.length) return;
+
+ let newMessages = [];
+ let newUniqueIds = new Map();
+
+ await Promise.all(
+ changes.map(async (change) => {
+ if (change.type !== "added") return;
+
const newMessage = change.doc.data();
- queueSetCashedMessages({
- newMessagesList: [newMessage],
- myPubKey: globalContactsInformation.myProfile.uuid,
+ const isReceived =
+ newMessage.toPubKey === globalContactsInformation.myProfile.uuid;
+ console.log(
+ `${isReceived ? "received" : "sent"} a new message`,
+ newMessage
+ );
+
+ if (typeof newMessage.message !== "string") {
+ newMessages.push(newMessage);
+ return;
+ }
+
+ const sendersPubkey = isReceived
+ ? newMessage.fromPubKey
+ : newMessage.toPubKey;
+
+ const decoded = await decryptMessage(
+ contactsPrivateKey,
+ sendersPubkey,
+ newMessage.message
+ );
+
+ if (!decoded) return;
+
+ let parsedMessage;
+ try {
+ parsedMessage = JSON.parse(decoded);
+ } catch {
+ return;
+ }
+
+ if (parsedMessage?.senderProfileSnapshot && isReceived) {
+ newUniqueIds.set(
+ sendersPubkey,
+ parsedMessage.senderProfileSnapshot.uniqueName
+ );
+ }
+
+ newMessages.push({
+ ...newMessage,
+ message: parsedMessage,
+ sendersPubkey,
+ isReceived,
});
- }
- });
+ })
+ );
+
+ if (newUniqueIds.size) {
+ updateContactUniqueName(newUniqueIds);
+ }
+
+ if (newMessages.length) {
+ queueSetCashedMessages({
+ newMessagesList: newMessages,
+ myPubKey: globalContactsInformation.myProfile.uuid,
+ });
+ }
}
);
+
return () => {
if (unsubscribeMessagesRef.current) {
unsubscribeMessagesRef.current();
}
- if (unsubscribeSentMessagesRef.current) {
- unsubscribeSentMessagesRef.current();
- }
};
- }, [globalContactsInformation?.myProfile?.uuid]);
+ }, [globalContactsInformation?.myProfile?.uuid, contactsPrivateKey]);
+
+ const addContact = useCallback(
+ async (contact) => {
+ try {
+ const newContact = {
+ name: contact.name || "",
+ nameLower: contact.nameLower || "",
+ bio: contact.bio,
+ unlookedTransactions: 0,
+ isLNURL: contact.isLNURL,
+ uniqueName: contact.uniqueName || "",
+ uuid: contact.uuid,
+ isAdded: true,
+ isFavorite: false,
+ profileImage: contact.profileImage,
+ receiveAddress: contact.receiveAddress,
+ transactions: [],
+ };
+
+ let newAddedContacts = JSON.parse(JSON.stringify(decodedAddedContacts));
+
+ const isContactInAddedContacts = newAddedContacts.filter(
+ (addedContact) => addedContact.uuid === newContact.uuid
+ ).length;
+
+ if (isContactInAddedContacts) {
+ newAddedContacts = newAddedContacts.map((addedContact) => {
+ if (addedContact.uuid === newContact.uuid) {
+ return {
+ ...addedContact,
+ name: newContact.name,
+ nameLower: newContact.nameLower,
+ bio: newContact.bio,
+ unlookedTransactions: 0,
+ isAdded: true,
+ };
+ } else return addedContact;
+ });
+ } else newAddedContacts.push(newContact);
+
+ toggleGlobalContactsInformation(
+ {
+ myProfile: {
+ ...globalContactsInformation.myProfile,
+ didEditProfile: true,
+ },
+ addedContacts: await encryptMessage(
+ contactsPrivateKey,
+ publicKey,
+ JSON.stringify(newAddedContacts)
+ ),
+ },
+ true
+ );
+ } catch (err) {
+ console.log("Error adding contact", err);
+ }
+ },
+ [
+ decodedAddedContacts,
+ contactsPrivateKey,
+ publicKey,
+ globalContactsInformation,
+ ]
+ );
+
+ const deleteContact = useCallback(
+ async (contact) => {
+ try {
+ const newAddedContacts = decodedAddedContacts
+ .map((savedContacts) => {
+ if (savedContacts.uuid === contact.uuid) {
+ return null;
+ } else return savedContacts;
+ })
+ .filter((contact) => contact);
+
+ await deleteCachedMessages(contact.uuid);
+
+ toggleGlobalContactsInformation(
+ {
+ addedContacts: await encryptMessage(
+ contactsPrivateKey,
+ publicKey,
+ JSON.stringify(newAddedContacts)
+ ),
+ myProfile: { ...globalContactsInformation.myProfile },
+ },
+ true
+ );
+ } catch (err) {
+ console.log("Error deleating contact", err);
+ }
+ },
+ [
+ decodedAddedContacts,
+ contactsPrivateKey,
+ publicKey,
+ globalContactsInformation,
+ ]
+ );
useEffect(() => {
if (!Object.keys(globalContactsInformation).length) return;
- if (lookForNewMessages.current) return;
- lookForNewMessages.current = true;
- // syncDatabasePayment(
- // globalContactsInformation.myProfile.uuid,
- // updatedCachedMessagesStateFunction
- // );
- }, [globalContactsInformation, updatedCachedMessagesStateFunction]);
+ if (!contactsPrivateKey) return;
+ if (!addedContacts) return;
+
+ if (lookForNewMessages.current) {
+ lookForNewMessages.current = false;
+ async function handleOfflineMessageSync() {
+ console.log("RUNNING SYNC DATABAE");
+ const restoredPayments = await syncDatabasePayment(
+ globalContactsInformation.myProfile.uuid,
+ contactsPrivateKey
+ );
+
+ if (restoredPayments.length === 0) {
+ updatedCachedMessagesStateFunction();
+ }
+
+ const contactDataMap = new Map(
+ restoredPayments
+ .filter(
+ (item) =>
+ item.isReceived &&
+ item?.message?.senderProfileSnapshot?.uniqueName
+ )
+ .map((item) => [
+ item.sendersPubkey,
+ item.message.senderProfileSnapshot.uniqueName,
+ ])
+ );
+ updateContactUniqueName(contactDataMap);
+
+ queueSetCashedMessages({
+ newMessagesList: restoredPayments,
+ myPubKey: globalContactsInformation.myProfile.uuid,
+ });
+ }
+ handleOfflineMessageSync();
+ }
+ }, [
+ globalContactsInformation,
+ updatedCachedMessagesStateFunction,
+ contactsPrivateKey,
+ decodedAddedContacts,
+ addedContacts,
+ ]);
+
+ const giftCardsList = useMemo(() => {
+ if (!contactsMessags) return [];
+
+ const actualContacts = Object.keys(contactsMessags);
+ const lastMessageTimestampIndex = actualContacts.indexOf(
+ "lastMessageTimestamp"
+ );
+
+ // Remove lastMessageTimestamp efficiently
+ if (lastMessageTimestampIndex > -1) {
+ actualContacts.splice(lastMessageTimestampIndex, 1);
+ }
+
+ if (actualContacts.length === 0) return [];
+
+ const giftCards = [];
+
+ // Process contacts efficiently
+ for (const contact of actualContacts) {
+ const contactData = contactsMessags[contact];
+ if (!contactData?.messages?.length) continue;
+
+ // Use for loop for better performance than filter + push
+ const messages = contactData.messages;
+ for (let i = 0; i < messages.length; i++) {
+ const message = messages[i];
+ if (message.message?.giftCardInfo && !message.message.didSend) {
+ giftCards.push(message);
+ }
+ }
+ }
+
+ // Sort in-place for memory efficiency
+ giftCards.sort((a, b) => {
+ const timeA = a.serverTimestamp || a.timestamp;
+ const timeB = b.serverTimestamp || b.timestamp;
+ return timeB - timeA;
+ });
+
+ return giftCards;
+ }, [contactsMessags]);
+
+ const hasUnlookedTransactions = useMemo(() => {
+ return Object.keys(contactsMessags).some((contactUUID) => {
+ if (
+ contactUUID === "lastMessageTimestamp" ||
+ contactUUID === globalContactsInformation?.myProfile?.uuid
+ ) {
+ return false;
+ }
+ const messages = contactsMessags[contactUUID]?.messages;
+ return messages?.some((message) => !message.message.wasSeen) || false;
+ });
+ }, [contactsMessags, globalContactsInformation?.myProfile?.uuid]);
+
+ const contextValue = useMemo(
+ () => ({
+ decodedAddedContacts,
+ globalContactsInformation,
+ toggleGlobalContactsInformation,
+ contactsMessags,
+ updatedCachedMessagesStateFunction,
+ giftCardsList,
+ hasUnlookedTransactions,
+ deleteContact,
+ addContact,
+ }),
+ [
+ decodedAddedContacts,
+ globalContactsInformation,
+ toggleGlobalContactsInformation,
+ contactsMessags,
+ updatedCachedMessagesStateFunction,
+ giftCardsList,
+ hasUnlookedTransactions,
+ deleteContact,
+ addContact,
+ ]
+ );
return (
-
+
{children}
);
diff --git a/src/contexts/imageCacheContext.jsx b/src/contexts/imageCacheContext.jsx
index 2dceae2..99b1383 100644
--- a/src/contexts/imageCacheContext.jsx
+++ b/src/contexts/imageCacheContext.jsx
@@ -4,129 +4,231 @@ import React, {
useState,
useEffect,
useRef,
+ useCallback,
+ useMemo,
} from "react";
-import { getStorage } from "firebase/storage";
+import { getDownloadURL, getMetadata, getStorage, ref } from "firebase/storage";
import { useGlobalContacts } from "./globalContacts";
import { useAppStatus } from "./appStatus";
import { BLITZ_PROFILE_IMG_STORAGE_REF } from "../constants";
-import { ImageCacheDB } from "../functions/contacts/imageCacheDb";
+import {
+ clearAllCachedImages,
+ deleteCachedImage,
+ downloadAndCacheImage,
+ getAllImageKeys,
+ getCachedImage,
+} from "../functions/images/storage";
+import { useGlobalContextProvider } from "./masterInfoObject";
const ImageCacheContext = createContext();
export function ImageCacheProvider({ children }) {
const [cache, setCache] = useState({});
+ const { masterInfoObject } = useGlobalContextProvider();
const { didGetToHomepage } = useAppStatus();
const { decodedAddedContacts } = useGlobalContacts();
const didRunContextCacheCheck = useRef(null);
+ const cachedImagesRef = useRef(cache);
- useEffect(() => {
- (async () => {
- try {
- const keys = await ImageCacheDB.getAllKeys();
- const initialCache = {};
-
- for (const key of keys) {
- const value = await ImageCacheDB.getItem(key);
- if (value && value.blob) {
- const uuid = key.replace(BLITZ_PROFILE_IMG_STORAGE_REF + "/", "");
- const blobUrl = URL.createObjectURL(value.blob);
- initialCache[uuid] = { ...value, uri: blobUrl };
- }
- }
+ const inFlightRequests = useRef(new Map());
- setCache(initialCache);
- } catch (e) {
- console.error("Error loading image cache from IndexedDB", e);
+ const refreshCacheObject = useCallback(async () => {
+ try {
+ const keys = await getAllImageKeys();
+
+ const initialCache = {};
+
+ for (const key of keys) {
+ const value = await getCachedImage(key);
+ if (value) {
+ const uuid = key.replace(BLITZ_PROFILE_IMG_STORAGE_REF + "/", "");
+ initialCache[uuid] = { ...value, localUri: value.uri };
+ }
}
- })();
+
+ setCache(initialCache);
+ } catch (e) {
+ console.error("Error loading image cache from storage", e);
+ }
}, []);
useEffect(() => {
- return;
- if (!didGetToHomepage) return;
- if (didRunContextCacheCheck.current) return;
- didRunContextCacheCheck.current = true;
- console.log(decodedAddedContacts, "DECIN FUNC");
- async function refreshContactsImages() {
- for (let index = 0; index < decodedAddedContacts.length; index++) {
- const element = decodedAddedContacts[index];
- await refreshCache(element.uuid);
+ cachedImagesRef.current = cache;
+ }, [cache]);
+
+ useEffect(() => {
+ refreshCacheObject();
+ }, [decodedAddedContacts, refreshCacheObject]); //rerun the cache when adding or removing contacts
+
+ const refreshCache = useCallback(
+ async (uuid, hasdownloadURL, skipCacheUpdate = false) => {
+ if (inFlightRequests.current.has(uuid)) {
+ console.log("Request already in flight for", uuid);
+ return inFlightRequests.current.get(uuid);
}
- }
- refreshContactsImages();
- }, [decodedAddedContacts, didGetToHomepage]);
+ const requestPromise = (async () => {
+ try {
+ console.log("Refreshing image for", uuid);
+ const key = `${BLITZ_PROFILE_IMG_STORAGE_REF}/${uuid}`;
+ let url;
+ let metadata;
+ let updated;
+
+ if (!hasdownloadURL) {
+ const storageInstance = getStorage();
+
+ const reference = ref(
+ storageInstance,
+ `${BLITZ_PROFILE_IMG_STORAGE_REF}/${uuid}.jpg`
+ );
+
+ metadata = await getMetadata(reference);
+ updated = metadata.updated;
+
+ const cached = cache[uuid];
+ if (cached && cached.updated === updated) {
+ const cachedImage = await getCachedImage(key);
+ if (cachedImage) {
+ return {
+ uri: cachedImage.uri,
+ localUri: cachedImage.uri,
+ updated: cachedImage.metadata,
+ isObjectURL: cachedImage.isObjectURL,
+ };
+ }
+ }
+
+ url = await getDownloadURL(reference);
+ } else {
+ url = hasdownloadURL;
+ updated = new Date().toISOString();
+ }
- async function refreshCache(uuid, hasDownloadURL) {
- try {
- const key = `${BLITZ_PROFILE_IMG_STORAGE_REF}/${uuid}`;
- let url;
- let metadata;
- let updated;
-
- if (!hasDownloadURL) {
- const reference = getStorage().ref(
- `${BLITZ_PROFILE_IMG_STORAGE_REF}/${uuid}.jpg`
- );
- metadata = await reference.getMetadata();
- updated = metadata.updated;
-
- const cached = await ImageCacheDB.getItem(key);
- if (cached && cached.updated === updated) {
- const blobUrl = URL.createObjectURL(cached.blob);
- const newCache = { ...cached, uri: blobUrl };
- setCache((prev) => ({ ...prev, [uuid]: newCache }));
- return newCache;
- }
+ const blob = await downloadAndCacheImage(key, url, { updated });
- url = await reference.getDownloadURL();
- } else {
- url = hasDownloadURL;
- updated = new Date().toISOString();
- }
+ if (!blob) {
+ throw new Error("Failed to download image");
+ }
- const response = await fetch(url);
- const blob = await response.blob();
- const blobUrl = URL.createObjectURL(blob);
+ // Get the newly cached image
+ const cachedImage = await getCachedImage(key);
- const newCacheEntry = {
- updated,
- blob,
- };
+ if (!cachedImage) {
+ throw new Error("Failed to retrieve cached image");
+ }
- await ImageCacheDB.setItem(key, newCacheEntry);
+ const newCacheEntry = {
+ uri: cachedImage.uri,
+ localUri: cachedImage.uri,
+ updated,
+ isObjectURL: cachedImage.isObjectURL,
+ };
- const newDisplayEntry = {
- ...newCacheEntry,
- uri: blobUrl,
- };
+ if (!skipCacheUpdate) {
+ setCache((prev) => ({ ...prev, [uuid]: newCacheEntry }));
+ }
- setCache((prev) => ({ ...prev, [uuid]: newDisplayEntry }));
+ return newCacheEntry;
+ } catch (err) {
+ console.log("Error refreshing image cache", err);
+ throw err;
+ } finally {
+ inFlightRequests.current.delete(uuid);
+ }
+ })();
- return newDisplayEntry;
- } catch (err) {
- console.error("Error refreshing image cache", err);
- }
- }
+ inFlightRequests.current.set(uuid, requestPromise);
- async function removeProfileImageFromCache(uuid) {
+ return requestPromise;
+ },
+ [cache]
+ );
+
+ const removeProfileImageFromCache = useCallback(async (uuid) => {
try {
+ console.log("Deleting profile image", uuid);
const key = `${BLITZ_PROFILE_IMG_STORAGE_REF}/${uuid}`;
- await ImageCacheDB.deleteItem(key);
+
const newCacheEntry = {
uri: null,
- updated: new Date().toISOString(),
+ localUri: null,
+ updated: new Date().getTime(),
};
+
+ await deleteCachedImage(key);
setCache((prev) => ({ ...prev, [uuid]: newCacheEntry }));
return newCacheEntry;
} catch (err) {
- console.error("Error deleting image from cache", err);
+ console.log("Error refreshing image cache", err);
}
- }
+ }, []);
+
+ useEffect(() => {
+ if (!didGetToHomepage) return;
+ if (didRunContextCacheCheck.current) return;
+ if (!masterInfoObject.uuid) return;
+ didRunContextCacheCheck.current = true;
+
+ async function refreshContactsImages() {
+ // allways check all images, will return cahced image if its already cached. But this prevents against stale images
+ let refreshArray = [
+ ...decodedAddedContacts,
+ { uuid: masterInfoObject.uuid },
+ ];
+
+ const validContacts = refreshArray.filter((element) => !element.isLNURL);
+
+ const results = await Promise.allSettled(
+ validContacts.map((element) => refreshCache(element.uuid, null, true))
+ );
+
+ const cacheUpdates = {};
+
+ results.forEach((result, index) => {
+ if (result.status === "fulfilled" && result.value) {
+ try {
+ const uuid = validContacts[index].uuid;
+ const newEntry = result.value;
+ const existingEntry = cachedImagesRef.current[uuid];
+
+ // Only add to updates if the entry is new or has changed
+ if (
+ !existingEntry ||
+ existingEntry.updated !== newEntry.updated ||
+ existingEntry.localUri !== newEntry.localUri
+ ) {
+ cacheUpdates[uuid] = newEntry;
+ }
+ } catch (err) {
+ console.error("Error updating response", err);
+ }
+ }
+ });
+
+ if (Object.keys(cacheUpdates).length > 0) {
+ setCache((prev) => ({ ...prev, ...cacheUpdates }));
+ }
+ }
+ refreshContactsImages();
+ }, [
+ decodedAddedContacts,
+ didGetToHomepage,
+ masterInfoObject?.uuid,
+ refreshCache,
+ ]);
+
+ const contextValue = useMemo(
+ () => ({
+ cache,
+ refreshCache,
+ removeProfileImageFromCache,
+ refreshCacheObject,
+ }),
+ [cache, refreshCache, removeProfileImageFromCache, refreshCacheObject]
+ );
return (
-
+
{children}
);
diff --git a/src/contexts/masterInfoObject.jsx b/src/contexts/masterInfoObject.jsx
index a15a337..8be9124 100644
--- a/src/contexts/masterInfoObject.jsx
+++ b/src/contexts/masterInfoObject.jsx
@@ -9,6 +9,8 @@ import {
import { useTranslation } from "react-i18next";
import { sendDataToDB } from "../../db/interactionManager";
import { useKeysContext } from "./keysContext";
+import { getDataFromCollection } from "../../db";
+import { auth } from "../../db/initializeFirebase";
// Initiate context
const GlobalContextManger = createContext(null);
@@ -18,6 +20,11 @@ const GlobalContextProvider = ({ children }) => {
const [masterInfoObject, setMasterInfoObject] = useState({});
+ const [preloadedUserData, setPreLoadedUserData] = useState({
+ isLoading: true,
+ data: null,
+ });
+
const { i18n } = useTranslation();
const toggleMasterInfoObject = useCallback(
@@ -33,13 +40,40 @@ const GlobalContextProvider = ({ children }) => {
[i18n, publicKey]
);
+ useEffect(() => {
+ async function preloadUserData() {
+ try {
+ if (auth.currentUser) {
+ const collectionData = await getDataFromCollection(
+ "blitzWalletUsers",
+ auth.currentUser.uid
+ );
+ if (!collectionData) throw new Error("No data returened");
+ setPreLoadedUserData({ isLoading: true, data: collectionData });
+ } else throw new Error("No user logged in");
+ } catch (err) {
+ console.log("Error preloading user data");
+ setPreLoadedUserData({ isLoading: false, data: null });
+ }
+ }
+ preloadUserData();
+ }, []);
+
const contextValue = useMemo(
() => ({
toggleMasterInfoObject,
setMasterInfoObject,
masterInfoObject,
+ preloadedUserData,
+ setPreLoadedUserData,
}),
- [toggleMasterInfoObject, masterInfoObject, setMasterInfoObject]
+ [
+ toggleMasterInfoObject,
+ masterInfoObject,
+ setMasterInfoObject,
+ preloadedUserData,
+ setPreLoadedUserData,
+ ]
);
return (
diff --git a/src/contexts/navigationLogger.jsx b/src/contexts/navigationLogger.jsx
index f58d912..7189135 100644
--- a/src/contexts/navigationLogger.jsx
+++ b/src/contexts/navigationLogger.jsx
@@ -9,8 +9,6 @@ export function NavigationStackProvider({ children }) {
const [stack, setStack] = useState([]);
const initialized = useRef(false);
- console.log(stack);
-
useEffect(() => {
if (!initialized.current) {
initialized.current = true;
diff --git a/src/contexts/nodeContext.jsx b/src/contexts/nodeContext.jsx
index 5de1c0e..7732d16 100644
--- a/src/contexts/nodeContext.jsx
+++ b/src/contexts/nodeContext.jsx
@@ -12,13 +12,18 @@ import { useAppStatus } from "./appStatus";
import loadNewFiatData from "../functions/saveAndUpdateFiatData";
import { connectToLiquidNode } from "../functions/connectToLiquid";
import { useGlobalContextProvider } from "./masterInfoObject";
+import { useSpark } from "./sparkContext";
+import { useGlobalContacts } from "./globalContacts";
+import liquidToSparkSwap from "../functions/spark/liquidToSparkSwap";
// Initiate context
const NodeContextManager = createContext(null);
const GLobalNodeContextProider = ({ children }) => {
+ const { sparkInformation } = useSpark();
const { contactsPrivateKey, publicKey, accountMnemoinc } = useKeysContext();
- const { didGetToHomepage } = useAppStatus();
+ const { didGetToHomepage, minMaxLiquidSwapAmounts } = useAppStatus();
+ const { globalContactsInformation } = useGlobalContacts();
const { masterInfoObject } = useGlobalContextProvider();
const [nodeInformation, setNodeInformation] = useState({
didConnectToNode: null,
@@ -35,6 +40,7 @@ const GLobalNodeContextProider = ({ children }) => {
transactions: [],
userBalance: 0,
});
+ const [pendingLiquidPayment, setPendingLiquidPayment] = useState(null);
const [fiatStats, setFiatStats] = useState({});
const toggleFiatStats = useCallback((newInfo) => {
setFiatStats((prev) => ({ ...prev, ...newInfo }));
@@ -97,6 +103,33 @@ const GLobalNodeContextProider = ({ children }) => {
connectToLiquid();
}, [contactsPrivateKey, publicKey, didGetToHomepage, accountMnemoinc]);
+ // This function checks to see if there are any liquid funds that need to be sent to spark
+ useEffect(() => {
+ async function swapLiquidToSpark() {
+ try {
+ if (liquidNodeInformation.userBalance > minMaxLiquidSwapAmounts.min) {
+ setPendingLiquidPayment(true);
+ await liquidToSparkSwap(
+ globalContactsInformation.myProfile.uniqueName
+ );
+ }
+ } catch (err) {
+ console.log("transfering liquid to spark error", err);
+ }
+ }
+ if (!didGetToHomepage) return;
+ if (!sparkInformation.didConnect) return;
+ if (!sparkInformation.identityPubKey) return;
+ swapLiquidToSpark();
+ }, [
+ didGetToHomepage,
+ liquidNodeInformation.userBalance,
+ minMaxLiquidSwapAmounts,
+ sparkInformation.didConnect,
+ sparkInformation.identityPubKey,
+ globalContactsInformation?.myProfile?.uniqueName,
+ ]);
+
const contextValue = useMemo(
() => ({
nodeInformation,
@@ -104,6 +137,8 @@ const GLobalNodeContextProider = ({ children }) => {
toggleLiquidNodeInformation,
toggleFiatStats,
fiatStats,
+ pendingLiquidPayment,
+ setPendingLiquidPayment,
}),
[
nodeInformation,
@@ -111,6 +146,8 @@ const GLobalNodeContextProider = ({ children }) => {
fiatStats,
toggleFiatStats,
toggleLiquidNodeInformation,
+ pendingLiquidPayment,
+ setPendingLiquidPayment,
]
);
diff --git a/src/contexts/overlayContext.jsx b/src/contexts/overlayContext.jsx
new file mode 100644
index 0000000..ffb5e88
--- /dev/null
+++ b/src/contexts/overlayContext.jsx
@@ -0,0 +1,45 @@
+import { createContext, useCallback, useContext, useState } from "react";
+
+const OverlayContext = createContext(null);
+
+export function OverlayProvider({ children }) {
+ const [overlays, setOverlays] = useState([]);
+
+ const openOverlay = useCallback((overlay) => {
+ setOverlays((prev) => {
+ if (overlay.for === "halfModal") {
+ return prev.slice(0, -1).concat(overlay);
+ }
+ return [...prev, overlay];
+ });
+ }, []);
+
+ const closeOverlay = useCallback(() => {
+ setOverlays((prev) => prev.slice(0, -1));
+ }, []);
+
+ const clearOverlays = useCallback(() => {
+ setOverlays([]);
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useOverlay() {
+ const ctx = useContext(OverlayContext);
+ if (!ctx) {
+ throw new Error("useOverlay must be used inside OverlayProvider");
+ }
+ return ctx;
+}
diff --git a/src/contexts/sparkContext.jsx b/src/contexts/sparkContext.jsx
index dce8f24..845a34e 100644
--- a/src/contexts/sparkContext.jsx
+++ b/src/contexts/sparkContext.jsx
@@ -7,11 +7,10 @@ import {
useRef,
useCallback,
} from "react";
-
-import { sparkReceivePaymentWrapper } from "../functions/spark/payments";
-import { breezLiquidPaymentWrapper } from "../functions/breezLiquid";
import {
claimnSparkStaticDepositAddress,
+ getCachedSparkTransactions,
+ getSingleTxDetails,
getSparkBalance,
getSparkLightningPaymentStatus,
getSparkStaticBitcoinL1AddressQuote,
@@ -24,13 +23,13 @@ import {
addSingleSparkTransaction,
bulkUpdateSparkTransactions,
deleteUnpaidSparkLightningTransaction,
+ getAllSparkContactInvoices,
getAllSparkTransactions,
getAllUnpaidSparkLightningInvoices,
SPARK_TX_UPDATE_ENVENT_NAME,
sparkTransactionsEventEmitter,
} from "../functions/spark/transactions";
import {
- findSignleTxFromHistory,
fullRestoreSparkState,
restoreSparkTxState,
updateSparkTxStatus,
@@ -52,21 +51,40 @@ import liquidToSparkSwap from "../functions/spark/liquidToSparkSwap";
import { useActiveCustodyAccount } from "./activeAccount";
import sha256Hash from "../functions/hash";
import { getLRC20Transactions } from "../functions/lrc20";
+import { useGlobalContextProvider } from "./masterInfoObject";
+import handleBalanceCache from "../functions/spark/handleBalanceCache";
+import {
+ createBalancePoller,
+ createRestorePoller,
+} from "../functions/pollingManager";
+import i18next from "i18next";
+import { useLocation } from "react-router-dom";
export const isSendingPayingEventEmiiter = new EventEmitter();
export const SENDING_PAYMENT_EVENT_NAME = "SENDING_PAYMENT_EVENT";
+if (!globalThis.blitzWalletSparkIntervalState) {
+ globalThis.blitzWalletSparkIntervalState = {
+ intervalTracker: new Map(),
+ listenerLock: new Map(),
+ allIntervalIds: new Set(),
+ depositIntervalIds: new Set(),
+ };
+}
+const { intervalTracker, listenerLock, allIntervalIds, depositIntervalIds } =
+ globalThis.blitzWalletSparkIntervalState;
+
// Initiate context
const SparkWalletManager = createContext(null);
const sessionTime = new Date().getTime();
const SparkWalletProvider = ({ children, navigate }) => {
+ const location = useLocation();
+ const { masterInfoObject } = useGlobalContextProvider();
const { accountMnemoinc, contactsPrivateKey, publicKey } = useKeysContext();
const { currentWalletMnemoinc } = useActiveCustodyAccount();
const { didGetToHomepage, minMaxLiquidSwapAmounts, appState } =
useAppStatus();
- const { liquidNodeInformation } = useNodeContext();
- const [isSendingPayment, setIsSendingPayment] = useState(false);
const { toggleGlobalContactsInformation, globalContactsInformation } =
useGlobalContacts();
const prevAccountMnemoincRef = useRef(null);
@@ -78,128 +96,253 @@ const SparkWalletProvider = ({ children, navigate }) => {
sparkAddress: "",
didConnect: null,
});
+ const [tokensImageCache, setTokensImageCache] = useState({});
const [pendingNavigation, setPendingNavigation] = useState(null);
- const [pendingLiquidPayment, setPendingLiquidPayment] = useState(null);
+ const hasRestoreCompleted = useRef(false);
+ const [restoreCompleted, setRestoreCompleted] = useState(false);
+ const [reloadNewestPaymentTimestamp, setReloadNewestPaymentTimestamp] =
+ useState(0);
const depositAddressIntervalRef = useRef(null);
const sparkDBaddress = useRef(null);
const updatePendingPaymentsIntervalRef = useRef(null);
const isInitialRestore = useRef(true);
const isInitialLRC20Run = useRef(true);
+ const sparkInfoRef = useRef({
+ balance: 0,
+ tokens: {},
+ identityPubKey: "",
+ sparkAddress: "",
+ });
+ const sessionTimeRef = useRef(Date.now());
+ const newestPaymentTimeRef = useRef(Date.now());
+ const handledTransfers = useRef(new Set());
+ const usedSavedTxIds = useRef(new Set());
+ const prevAccountId = useRef(null);
+ const isSendingPaymentRef = useRef(false);
+ const balancePollingTimeoutRef = useRef(null);
+ const balancePollingAbortControllerRef = useRef(null);
+ const txPollingTimeoutRef = useRef(null);
+ const txPollingAbortControllerRef = useRef(null);
+ const currentPollingMnemonicRef = useRef(null);
+ const isInitialRender = useRef(true);
const didInitializeSendingPaymentEvent = useRef(false);
const initialBitcoinIntervalRun = useRef(null);
- const currentWalletMnemoincRef = useRef(currentWalletMnemoinc);
- const prevAppState = useRef(null);
+ const prevAppState = useRef(appState);
const prevListenerType = useRef(null);
- const [numberOfCachedTxs, setNumberOfCachedTxs] = useState(0);
- // dont need any more velow
+
+ const showTokensInformation =
+ masterInfoObject.enabledBTKNTokens === null
+ ? !!Object.keys(sparkInformation.tokens || {}).length
+ : masterInfoObject.enabledBTKNTokens;
+
+ const didRunInitialRestore = useRef(false);
+
+ const handledNavigatedTxs = useRef(new Set());
+
+ const [didRunNormalConnection, setDidRunNormalConnection] = useState(false);
+ const [normalConnectionTimeout, setNormalConnectionTimeout] = useState(false);
+ const shouldRunNormalConnection =
+ didRunNormalConnection || normalConnectionTimeout;
+ const currentMnemonicRef = useRef(currentWalletMnemoinc);
const [numberOfIncomingLNURLPayments, setNumberOfIncomingLNURLPayments] =
useState(0);
const [numberOfConnectionTries, setNumberOfConnectionTries] = useState(0);
- const [startConnectingToSpark, setStartConnectingToSpark] = useState(false);
+
+ const cleanStatusAndLRC20Intervals = () => {
+ try {
+ for (const intervalId of allIntervalIds) {
+ console.log("Clearing stored interval ID:", intervalId);
+ clearInterval(intervalId);
+ }
+
+ intervalTracker.clear();
+ allIntervalIds.clear();
+ } catch (err) {
+ console.log("Error cleaning lrc20 intervals", err);
+ }
+ };
+
+ const clearAllDepositIntervals = () => {
+ console.log(
+ "Clearing all deposit address intervals. Counts:",
+ depositIntervalIds.size
+ );
+
+ for (const intervalId of depositIntervalIds) {
+ console.log("Clearing deposit interval ID:", intervalId);
+ clearInterval(intervalId);
+ }
+
+ depositIntervalIds.clear();
+ console.log("All deposit intervals cleared");
+ };
+
+ useEffect(() => {
+ sparkInfoRef.current = {
+ balance: sparkInformation.balance,
+ tokens: sparkInformation.tokens,
+ identityPubKey: sparkInformation.identityPubKey,
+ sparkAddress: sparkInformation.sparkAddress,
+ };
+ }, [
+ sparkInformation.balance,
+ sparkInformation.tokens,
+ sparkInformation.identityPubKey,
+ sparkInformation.sparkAddress,
+ ]);
const sessionTime = useMemo(() => {
- console.log("Updating wallet session time");
+ console.log("Updating wallet session time", currentWalletMnemoinc);
return Date.now();
}, [currentWalletMnemoinc]);
useEffect(() => {
- currentWalletMnemoincRef.current = currentWalletMnemoinc;
+ currentMnemonicRef.current = currentWalletMnemoinc;
+ }, [currentWalletMnemoinc]);
+
+ useEffect(() => {
+ // Fixing race condition with new preloaded txs
+ sessionTimeRef.current = Date.now() + 5 * 1000;
}, [currentWalletMnemoinc]);
+ useEffect(() => {
+ newestPaymentTimeRef.current = Date.now();
+ }, [reloadNewestPaymentTimestamp]);
+
+ useEffect(() => {
+ if (!sparkInfoRef.current?.tokens) return;
+
+ async function updateTokensImageCache() {
+ const availableAssets = Object.entries(sparkInfoRef.current.tokens);
+ const extensions = ["jpg", "png"];
+ const newCache = {};
+
+ for (const [tokenId] of availableAssets) {
+ newCache[tokenId] = null;
+
+ for (const ext of extensions) {
+ const url = `https://tokens.sparkscan.io/${tokenId}.${ext}`;
+ try {
+ const response = await fetch(url, { method: "HEAD" });
+ if (response.ok) {
+ newCache[tokenId] = url;
+ break;
+ }
+ } catch (err) {
+ console.log("Image fetch error:", tokenId, err);
+ }
+ }
+ }
+
+ setTokensImageCache(newCache);
+ }
+
+ updateTokensImageCache();
+ }, [Object.keys(sparkInformation.tokens || {}).length]);
+
// Debounce refs
const debounceTimeoutRef = useRef(null);
const pendingTransferIds = useRef(new Set());
- const toggleIsSendingPayment = (isSending) => {
- setIsSendingPayment(isSending);
+ const toggleIsSendingPayment = useCallback((isSending) => {
+ console.log("Setting is sending payment", isSending);
+ if (isSending) {
+ if (txPollingAbortControllerRef.current) {
+ txPollingAbortControllerRef.current.abort();
+ txPollingAbortControllerRef.current = null;
+ }
+ }
+ isSendingPaymentRef.current = isSending;
+ }, []);
+
+ const toggleNewestPaymentTimestamp = () => {
+ setReloadNewestPaymentTimestamp((prev) => prev + 1);
};
- useEffect(() => {
- if (didInitializeSendingPaymentEvent.current) return;
- didInitializeSendingPaymentEvent.current = true;
- isSendingPayingEventEmiiter.addListener(
- SENDING_PAYMENT_EVENT_NAME,
- toggleIsSendingPayment
- );
- }, []);
+ useEffect(() => {
+ if (
+ !isSendingPayingEventEmiiter.listenerCount(SENDING_PAYMENT_EVENT_NAME)
+ ) {
+ isSendingPayingEventEmiiter.addListener(
+ SENDING_PAYMENT_EVENT_NAME,
+ toggleIsSendingPayment
+ );
+ }
- // This is a function that handles incoming transactions and formmataes it to reqirued formation
- const handleTransactionUpdate = async (
- recevedTxId,
- transactions,
- balance
- ) => {
- try {
- // First we need to get recent spark transfers
- if (!transactions)
- throw new Error("Unable to get transactions from spark");
- const { transfers } = transactions;
- let selectedSparkTransaction = transfers.find(
- (tx) => tx.id === recevedTxId
+ return () => {
+ console.log("clearning up toggle send pament");
+ isSendingPayingEventEmiiter.removeListener(
+ SENDING_PAYMENT_EVENT_NAME,
+ toggleIsSendingPayment
);
+ };
+ }, [toggleIsSendingPayment]);
+
+ const debouncedHandleIncomingPayment = useCallback(async (balance) => {
+ if (pendingTransferIds.current.size === 0) return;
- if (!selectedSparkTransaction) {
- console.log("Running full history sweep");
- const singleTxResponse = await findSignleTxFromHistory(
- recevedTxId,
- 50,
- currentWalletMnemoincRef.current
+ const transferIdsToProcess = Array.from(pendingTransferIds.current);
+ pendingTransferIds.current.clear();
+
+ console.log(
+ "Processing debounced incoming payments:",
+ transferIdsToProcess
+ );
+ // let transfersOffset = 0;
+ let cachedTransfers = [];
+
+ for (const transferId of transferIdsToProcess) {
+ try {
+ const transfer = await getSingleTxDetails(
+ currentMnemonicRef.current,
+ transferId
);
- if (!singleTxResponse.tx)
- throw new Error("Unable to find tx in all of history");
- selectedSparkTransaction = singleTxResponse.tx;
+
+ if (!transfer) continue;
+ cachedTransfers.push(transfer);
+ } catch (error) {
+ console.error("Error processing incoming payment:", transferId, error);
}
+ }
- console.log(
- selectedSparkTransaction,
- "received transaction from spark tx list"
- );
- if (!selectedSparkTransaction)
- throw new Error("Not able to get recent transfer");
+ const paymentObjects = [];
+
+ const [unpaidInvoices, unpaidContactInvoices] = await Promise.all([
+ getAllUnpaidSparkLightningInvoices(),
+ getAllSparkContactInvoices(),
+ ]);
- const unpaidInvoices = await getAllUnpaidSparkLightningInvoices();
- const paymentObject = await transformTxToPaymentObject(
- selectedSparkTransaction,
- sparkInformation.sparkAddress,
+ for (const transferId of transferIdsToProcess) {
+ const tx = cachedTransfers.find((t) => t.id === transferId);
+ if (!tx) continue;
+
+ // Skip UTXO_SWAP handling here — old logic kept
+ if (tx.type === "UTXO_SWAP") continue;
+
+ const paymentObj = await transformTxToPaymentObject(
+ tx,
+ sparkInfoRef.current.sparkAddress,
undefined,
false,
unpaidInvoices,
- sparkInformation.identityPubKey
+ sparkInfoRef.current.identityPubKey,
+ 1,
+ undefined,
+ unpaidContactInvoices
);
- if (paymentObject) {
- await bulkUpdateSparkTransactions(
- [paymentObject],
- "incomingPayment",
- 0,
- balance
- );
+ if (paymentObj) {
+ paymentObjects.push(paymentObj);
}
- const savedTxs = await getAllSparkTransactions(
- 5,
- sparkInformation.identityPubKey
- );
- return {
- txs: savedTxs,
- paymentObject: paymentObject || {},
- paymentCreatedTime: new Date(
- selectedSparkTransaction.createdTime
- ).getTime(),
- };
- } catch (err) {
- console.log("Handle incoming transaction error", err);
}
- };
- console.log(sparkInformation);
- const handleIncomingPayment = async (transferId, transactions, balance) => {
- let storedTransaction = await handleTransactionUpdate(
- transferId,
- transactions,
- balance
- );
- if (!storedTransaction) {
+ if (!paymentObjects.length) {
+ handleBalanceCache({
+ isCheck: false,
+ passedBalance: balance,
+ mnemonic: currentMnemonicRef.current,
+ });
setSparkInformation((prev) => ({
...prev,
balance: balance,
@@ -207,135 +350,273 @@ const SparkWalletProvider = ({ children, navigate }) => {
return;
}
- const selectedStoredPayment = storedTransaction.txs.find(
- (tx) => tx.sparkID === transferId
- );
- if (!selectedStoredPayment) return;
- console.log(selectedStoredPayment, "seleceted stored transaction");
-
- const details = JSON.parse(selectedStoredPayment.details);
-
- if (details?.shouldNavigate && !details.isLNURL) return;
- if (details.isLNURL && !details.isBlitzContactPayment) return;
- if (details.isRestore) return;
- if (storedTransaction.paymentCreatedTime < sessionTime) return;
- // Handle confirm animation here
- navigate("/confirm-page", {
- state: {
- for: "invoicePaid",
- transaction: { ...selectedStoredPayment, details },
- },
- });
- };
+ try {
+ await bulkUpdateSparkTransactions(
+ paymentObjects,
+ "incomingPayment",
+ 0,
+ balance
+ );
+ } catch (error) {
+ console.error("bulkUpdateSparkTransactions failed:", error);
+ }
+ }, []);
- const debouncedHandleIncomingPayment = useCallback(
- async (balance) => {
- if (pendingTransferIds.current.size === 0) return;
+ const handleUpdate = useCallback(
+ async (...args) => {
+ try {
+ const [updateType = "transactions", fee = 0, passedBalance = 0] = args;
+ const mnemonic = currentMnemonicRef.current;
+ const { identityPubKey, balance: prevBalance } = sparkInfoRef.current;
- const transferIdsToProcess = Array.from(pendingTransferIds.current);
- pendingTransferIds.current.clear();
+ console.log(
+ "running update in spark context from db changes",
+ updateType
+ );
- console.log(
- "Processing debounced incoming payments:",
- transferIdsToProcess
- );
- const transactions = await getSparkTransactions(
- 1,
- undefined,
- currentWalletMnemoincRef.current
- );
- // Process all pending transfer IDs
- for (const transferId of transferIdsToProcess) {
- try {
- await handleIncomingPayment(transferId, transactions, balance);
- } catch (error) {
- console.error(
- "Error processing incoming payment:",
- transferId,
- error
+ if (!identityPubKey) {
+ console.warn(
+ "handleUpdate called but identityPubKey is not available yet"
);
+ return;
}
- }
- },
- [sparkInformation.identityPubKey]
- );
- const handleUpdate = async (...args) => {
- try {
- const [updateType = "transactions", fee = 0, passedBalance = 0] = args;
- console.log(
- "running update in spark context from db changes",
- updateType
- );
+ const txs = await getCachedSparkTransactions(null, identityPubKey);
- const txs = await getAllSparkTransactions(
- null,
- sparkInformation.identityPubKey
- );
- const balance = await getSparkBalance(currentWalletMnemoincRef.current);
-
- setSparkInformation((prev) => {
- return {
- ...prev,
- transactions: txs || prev.transactions,
- balance: Math.round(
- (passedBalance
- ? passedBalance
- : balance.didWork
- ? Number(balance.balance)
- : prev.balance) - fee
- ),
- tokens: balance.tokensObj,
- };
- });
- return;
- if (
- updateType === "supportTx" ||
- updateType === "restoreTxs" ||
- updateType === "transactions"
- ) {
- setSparkInformation((prev) => ({
- ...prev,
- transactions: txs || prev.transactions,
- }));
- return;
- }
- if (updateType === "incomingPayment") {
- setSparkInformation((prev) => ({
- ...prev,
- transactions: txs || prev.transactions,
- balance: Number(passedBalance),
- }));
- return;
- }
-
- if (updateType === "paymentWrapperTx") {
- setSparkInformation((prev) => {
- return {
+ if (
+ updateType === "lrc20Payments" ||
+ updateType === "txStatusUpdate" ||
+ updateType === "transactions"
+ ) {
+ setSparkInformation((prev) => ({
...prev,
transactions: txs || prev.transactions,
- balance: Math.round(
- (balance.didWork ? Number(balance.balance) : prev.balance) - fee
- ),
- tokens: balance.tokensObj,
- };
- });
- } else if (updateType === "fullUpdate") {
- setSparkInformation((prev) => {
- return {
+ }));
+ } else if (updateType === "incomingPayment") {
+ handleBalanceCache({
+ isCheck: false,
+ passedBalance: Number(passedBalance),
+ mnemonic,
+ });
+ setSparkInformation((prev) => ({
+ ...prev,
+ transactions: txs || prev.transactions,
+ balance: Number(passedBalance),
+ }));
+ } else if (updateType === "fullUpdate-waitBalance") {
+ if (balancePollingAbortControllerRef.current) {
+ balancePollingAbortControllerRef.current.abort();
+ }
+
+ balancePollingAbortControllerRef.current = new AbortController();
+ currentPollingMnemonicRef.current = mnemonic;
+
+ const pollingMnemonic = currentPollingMnemonicRef.current;
+
+ setSparkInformation((prev) => ({
...prev,
- balance: balance.didWork ? Number(balance.balance) : prev.balance,
transactions: txs || prev.transactions,
- tokens: balance.tokensObj,
- };
+ }));
+
+ const poller = createBalancePoller(
+ mnemonic,
+ currentMnemonicRef,
+ balancePollingAbortControllerRef.current,
+ (newBalance) => {
+ setSparkInformation((prev) => {
+ if (pollingMnemonic !== currentMnemonicRef.current) {
+ return prev;
+ }
+ handleBalanceCache({
+ isCheck: false,
+ passedBalance: newBalance,
+ mnemonic: pollingMnemonic,
+ });
+ return {
+ ...prev,
+ balance: newBalance,
+ };
+ });
+ },
+ prevBalance
+ );
+
+ balancePollingTimeoutRef.current = poller;
+ poller.start();
+ } else {
+ const balanceResponse = await getSparkBalance(mnemonic);
+
+ const newBalance = balanceResponse.didWork
+ ? Number(balanceResponse.balance)
+ : prevBalance;
+
+ if (updateType === "paymentWrapperTx") {
+ const updatedBalance = Math.round(newBalance - fee);
+
+ handleBalanceCache({
+ isCheck: false,
+ passedBalance: updatedBalance,
+ mnemonic,
+ });
+
+ setSparkInformation((prev) => ({
+ ...prev,
+ transactions: txs || prev.transactions,
+ balance: updatedBalance,
+ tokens: balanceResponse.didWork
+ ? balanceResponse.tokensObj
+ : prev.tokens,
+ }));
+ } else if (updateType === "fullUpdate-tokens") {
+ setSparkInformation((prev) => ({
+ ...prev,
+ transactions: txs || prev.transactions,
+ tokens: balanceResponse.didWork
+ ? balanceResponse.tokensObj
+ : prev.tokens,
+ }));
+ } else if (updateType === "fullUpdate") {
+ handleBalanceCache({
+ isCheck: false,
+ passedBalance: newBalance,
+ mnemonic,
+ });
+
+ setSparkInformation((prev) => ({
+ ...prev,
+ balance: newBalance,
+ transactions: txs || prev.transactions,
+ tokens: balanceResponse.didWork
+ ? balanceResponse.tokensObj
+ : prev.tokens,
+ }));
+ }
+ }
+
+ if (
+ updateType === "paymentWrapperTx" ||
+ updateType === "transactions" ||
+ updateType === "txStatusUpdate" ||
+ updateType === "lrc20Payments"
+ ) {
+ console.log(
+ "Payment type is send payment, transaction, lrc20 first render, or txstatus update, skipping confirm tx page navigation"
+ );
+ return;
+ }
+ const [lastAddedTx] = await getCachedSparkTransactions(
+ 1,
+ identityPubKey
+ );
+
+ console.log(lastAddedTx, "testing");
+
+ if (!lastAddedTx) {
+ console.log(
+ "No transaction found, skipping confirm tx page navigation"
+ );
+
+ return;
+ }
+
+ const parsedTx = {
+ ...lastAddedTx,
+ details: JSON.parse(lastAddedTx.details),
+ };
+
+ if (handledNavigatedTxs.current.has(parsedTx.sparkID)) {
+ console.log(
+ "Already handled transaction, skipping confirm tx page navigation"
+ );
+ return;
+ }
+ handledNavigatedTxs.current.add(parsedTx.sparkID);
+
+ const details = parsedTx?.details;
+
+ if (new Date(details.time).getTime() < sessionTimeRef.current) {
+ console.log(
+ "created before session time was set, skipping confirm tx page navigation"
+ );
+ return;
+ }
+
+ if (isSendingPaymentRef.current) {
+ console.log(
+ "Is sending payment, skipping confirm tx page navigation"
+ );
+ return;
+ }
+
+ if (details.direction === "OUTGOING") {
+ console.log(
+ "Only incoming payments navigate here, skipping confirm tx page navigation"
+ );
+ return;
+ }
+
+ const isOnReceivePage = location.pathname === "/receive";
+
+ const isNewestPayment =
+ !!details?.createdTime || !!details?.time
+ ? new Date(details.createdTime || details?.time).getTime() >
+ newestPaymentTimeRef.current
+ : false;
+
+ let shouldShowConfirm = false;
+
+ if (
+ (lastAddedTx.paymentType?.toLowerCase() === "lightning" &&
+ !details.isLNURL &&
+ !details?.shouldNavigate &&
+ isOnReceivePage &&
+ isNewestPayment) ||
+ (lastAddedTx.paymentType?.toLowerCase() === "spark" &&
+ !details.isLRC20Payment &&
+ isOnReceivePage &&
+ isNewestPayment)
+ ) {
+ if (lastAddedTx.paymentType?.toLowerCase() === "spark") {
+ const upaidLNInvoices = await getAllUnpaidSparkLightningInvoices();
+ const lastMatch = upaidLNInvoices.findLast((invoice) => {
+ const savedInvoiceDetails = JSON.parse(invoice.details);
+ return (
+ !savedInvoiceDetails.sendingUUID &&
+ !savedInvoiceDetails.isLNURL &&
+ invoice.amount === details.amount
+ );
+ });
+
+ if (lastMatch && !usedSavedTxIds.current.has(lastMatch.id)) {
+ usedSavedTxIds.current.add(lastMatch.id);
+ const lastInvoiceDetails = JSON.parse(lastMatch.details);
+ if (details.time - lastInvoiceDetails.createdTime < 60 * 1000) {
+ shouldShowConfirm = true;
+ }
+ }
+ } else {
+ shouldShowConfirm = true;
+ }
+ }
+
+ // Handle confirm animation here
+ setPendingNavigation({
+ tx: parsedTx,
+ amount: details.amount,
+ LRC20Token: details.LRC20Token,
+ isLRC20Payment: !!details.LRC20Token,
+ showFullAnimation: shouldShowConfirm,
});
+ } catch (err) {
+ console.log("error in spark handle db update function", err);
}
- } catch (err) {
- console.error("Error in handleUpdate:", err);
- }
- };
+ },
+ [location]
+ );
- const transferHandler = (transferId, balance) => {
+ const transferHandler = useCallback((transferId, balance) => {
+ if (handledTransfers.current.has(transferId)) return;
+ handledTransfers.current.add(transferId);
console.log(`Transfer ${transferId} claimed. New balance: ${balance}`);
// Add transferId to pending set
@@ -350,93 +631,150 @@ const SparkWalletProvider = ({ children, navigate }) => {
debounceTimeoutRef.current = setTimeout(() => {
debouncedHandleIncomingPayment(balance);
}, 500);
- };
+ }, []);
+
+ useEffect(() => {
+ if (!sparkInformation.identityPubKey) {
+ console.log("Skipping listener setup - no identity pub key yet");
+ return;
+ }
+
+ console.log("adding web view listeners");
+
+ sparkTransactionsEventEmitter.removeAllListeners(
+ SPARK_TX_UPDATE_ENVENT_NAME
+ );
+
+ sparkTransactionsEventEmitter.on(SPARK_TX_UPDATE_ENVENT_NAME, handleUpdate);
+
+ return () => {
+ console.log("Cleaning up spark event listeners");
+ sparkTransactionsEventEmitter.removeListener(
+ SPARK_TX_UPDATE_ENVENT_NAME,
+ handleUpdate
+ );
+ };
+ }, [sparkInformation.identityPubKey, handleUpdate, transferHandler]);
const addListeners = async (mode) => {
console.log("Adding Spark listeners...");
- const walletHash = sha256Hash(currentWalletMnemoincRef.current);
- if (mode === "full") {
- if (!sparkTransactionsEventEmitter.listenerCount()) {
- sparkTransactionsEventEmitter.on(
- SPARK_TX_UPDATE_ENVENT_NAME,
- handleUpdate
- );
- }
- if (!sparkWallet[walletHash].listenerCount()) {
- sparkWallet[walletHash].on("transfer:claimed", transferHandler);
- }
+ const walletHash = sha256Hash(currentMnemonicRef.current);
- if (isInitialRestore.current) {
- isInitialRestore.current = false;
- }
+ try {
+ if (mode === "full") {
+ if (!sparkWallet[walletHash]?.listenerCount()) {
+ sparkWallet[walletHash].on("transfer:claimed", transferHandler);
+ }
- await fullRestoreSparkState({
- sparkAddress: sparkInformation.sparkAddress,
- batchSize: isInitialRestore.current ? 15 : 5,
- isSendingPayment: isSendingPayment,
- mnemonic: currentWalletMnemoincRef.current,
- identityPubKey: sparkInformation.identityPubKey,
- });
- await updateSparkTxStatus(
- currentWalletMnemoincRef.current,
- sparkInformation.identityPubKey
- );
+ if (!isInitialRestore.current) {
+ if (txPollingAbortControllerRef.current) {
+ txPollingAbortControllerRef.current.abort();
+ }
- if (updatePendingPaymentsIntervalRef.current) {
- console.log("BLOCKING TRYING TO SET INTERVAL AGAIN");
- clearInterval(updatePendingPaymentsIntervalRef.current);
- }
- updatePendingPaymentsIntervalRef.current = setInterval(async () => {
- try {
- await updateSparkTxStatus(
- currentWalletMnemoincRef.current,
- sparkInformation.identityPubKey
+ txPollingAbortControllerRef.current = new AbortController();
+ const restorePoller = createRestorePoller(
+ currentMnemonicRef.current,
+ isSendingPaymentRef.current,
+ currentMnemonicRef,
+ txPollingAbortControllerRef.current,
+ (result) => {
+ console.log("RESTORE COMPLETE");
+ },
+ sparkInfoRef.current
);
- await getLRC20Transactions({
- ownerPublicKeys: [sparkInformation.identityPubKey],
- sparkAddress: sparkInformation.sparkAddress,
- isInitialRun: isInitialLRC20Run.current,
- mnemonic: currentWalletMnemoincRef.current,
- });
- } catch (err) {
- console.error("Error during periodic restore:", err);
+
+ restorePoller.start();
}
- }, 10 * 1000);
+
+ updateSparkTxStatus(
+ currentMnemonicRef.current,
+ sparkInfoRef.current.identityPubKey
+ );
+
+ if (updatePendingPaymentsIntervalRef.current) {
+ console.log("BLOCKING TRYING TO SET INTERVAL AGAIN");
+ clearInterval(updatePendingPaymentsIntervalRef.current);
+ updatePendingPaymentsIntervalRef.current = null;
+ }
+
+ const capturedMnemonic = currentMnemonicRef.current;
+ const capturedWalletHash = walletHash;
+
+ const intervalId = setInterval(async () => {
+ try {
+ if (capturedMnemonic !== currentMnemonicRef.current) {
+ console.log("Mnemonic changed. Aborting interval.");
+ clearInterval(intervalId);
+ intervalTracker.delete(capturedWalletHash);
+ allIntervalIds.delete(intervalId);
+ return;
+ }
+
+ await updateSparkTxStatus(
+ currentMnemonicRef.current,
+ sparkInfoRef.current.identityPubKey
+ );
+
+ if (capturedMnemonic !== currentMnemonicRef.current) {
+ console.log(
+ "Context changed during updateSparkTxStatus. Aborting getLRC20Transactions."
+ );
+ clearInterval(intervalId);
+ intervalTracker.delete(capturedWalletHash);
+ allIntervalIds.delete(intervalId);
+ return;
+ }
+
+ await getLRC20Transactions({
+ ownerPublicKeys: [sparkInfoRef.current.identityPubKey],
+ sparkAddress: sparkInfoRef.current.sparkAddress,
+ isInitialRun: isInitialLRC20Run.current,
+ mnemonic: currentMnemonicRef.current,
+ });
+ if (isInitialLRC20Run.current) {
+ isInitialLRC20Run.current = false;
+ }
+ } catch (err) {
+ console.error("Error during periodic restore:", err);
+ }
+ }, 10 * 1000);
+
+ if (isInitialRestore.current) {
+ isInitialRestore.current = false;
+ }
+
+ updatePendingPaymentsIntervalRef.current = intervalId;
+ intervalTracker.set(walletHash, intervalId);
+ allIntervalIds.add(intervalId);
+ }
+ } catch (error) {
+ console.error("Error in addListeners:", error);
+ } finally {
+ listenerLock.set(walletHash, false);
+ console.log("Lock released for wallet:", walletHash);
}
};
- const removeListeners = () => {
+ const removeListeners = (onlyClearIntervals = false) => {
console.log("Removing spark listeners");
- console.log(
- sparkTransactionsEventEmitter.listenerCount(SPARK_TX_UPDATE_ENVENT_NAME),
- "Nymber of event emiitter litsenrs"
- );
- console.log(
- sparkWallet[sha256Hash(prevAccountMnemoincRef.current)]?.listenerCount(
- "transfer:claimed"
- ),
- "number of spark wallet listenre"
- );
- if (
- sparkTransactionsEventEmitter.listenerCount(SPARK_TX_UPDATE_ENVENT_NAME)
- ) {
- sparkTransactionsEventEmitter.removeAllListeners(
- SPARK_TX_UPDATE_ENVENT_NAME
- );
- }
- if (
- sparkWallet[sha256Hash(prevAccountMnemoincRef.current)]?.listenerCount(
- "transfer:claimed"
- )
- ) {
- sparkWallet[
- sha256Hash(prevAccountMnemoincRef.current)
- ]?.removeAllListeners("transfer:claimed");
- }
+ cleanStatusAndLRC20Intervals();
+ if (!onlyClearIntervals) {
+ if (!prevAccountMnemoincRef.current) {
+ prevAccountMnemoincRef.current = currentMnemonicRef.current;
+ return;
+ }
+ const hashedMnemonic = sha256Hash(prevAccountMnemoincRef.current);
+
+ if (
+ prevAccountMnemoincRef.current &&
+ sparkWallet[hashedMnemonic]?.listenerCount("transfer:claimed")
+ ) {
+ sparkWallet[hashedMnemonic]?.removeAllListeners("transfer:claimed");
+ }
- prevAccountMnemoincRef.current = currentWalletMnemoincRef.current;
- // sparkWallet?.removeAllListeners('deposit:confirmed');
+ prevAccountMnemoincRef.current = currentMnemonicRef.current;
+ }
// Clear debounce timeout when removing listeners
if (debounceTimeoutRef.current) {
@@ -451,34 +789,56 @@ const SparkWalletProvider = ({ children, navigate }) => {
clearInterval(updatePendingPaymentsIntervalRef.current);
updatePendingPaymentsIntervalRef.current = null;
}
+ //Clear balance polling
+ if (balancePollingTimeoutRef.current) {
+ clearTimeout(balancePollingTimeoutRef.current);
+ balancePollingTimeoutRef.current = null;
+ }
+ if (balancePollingAbortControllerRef.current) {
+ balancePollingAbortControllerRef.current.abort();
+ balancePollingAbortControllerRef.current = null;
+ }
+ if (txPollingTimeoutRef.current) {
+ clearTimeout(txPollingTimeoutRef.current);
+ txPollingTimeoutRef.current = null;
+ }
+ if (txPollingAbortControllerRef.current) {
+ txPollingAbortControllerRef.current.abort();
+ txPollingAbortControllerRef.current = null;
+ }
+ currentPollingMnemonicRef.current = null;
};
// Add event listeners to listen for bitcoin and lightning or spark transfers when receiving does not handle sending
useEffect(() => {
+ if (prevAppState.current !== appState && appState === "background") {
+ console.log("App moved to background — clearing listener type");
+ prevListenerType.current = null;
+ }
+
const timeoutId = setTimeout(async () => {
if (!didGetToHomepage) return;
- if (!sparkInformation.didConnect) return;
+ if (!sparkInfoRef.current.identityPubKey) return;
const getListenerType = () => {
- if (appState === "active" && !isSendingPayment) return "full";
- if (appState === "active" && isSendingPayment) return "sparkOnly";
- return null; // No listeners in background
+ if (appState === "active") return "full";
+ return null;
};
const newType = getListenerType();
const prevType = prevListenerType.current;
+ const prevId = prevAccountId.current;
- console.log(prevAppState.current, appState, "APP STATE CHANGE");
-
- if (prevAppState.current !== appState && appState === "background") {
- removeListeners();
- prevListenerType.current = null;
- }
-
- if (newType !== prevType && appState === "active") {
+ // Only reconfigure listeners when becoming active
+ if (
+ (newType !== prevType ||
+ prevId !== sparkInfoRef.current.identityPubKey) &&
+ appState === "active"
+ ) {
removeListeners();
- if (newType) addListeners(newType);
+ if (newType) await addListeners(newType);
prevListenerType.current = newType;
+ prevAccountId.current = sparkInfoRef.current.identityPubKey;
}
prevAppState.current = appState;
@@ -488,24 +848,28 @@ const SparkWalletProvider = ({ children, navigate }) => {
}, [
appState,
sparkInformation.didConnect,
+ sparkInformation.identityPubKey,
didGetToHomepage,
- isSendingPayment,
]);
useEffect(() => {
if (!didGetToHomepage) return;
if (!sparkInformation.didConnect) return;
+ if (!sparkInformation.identityPubKey) return;
+
// Interval to check deposit addresses to see if they were paid
const handleDepositAddressCheck = async () => {
try {
console.log("l1Deposit check running....");
+ if (isSendingPaymentRef.current) return;
+ if (!currentMnemonicRef.current) return;
const allTxs = await getAllSparkTransactions(
null,
sparkInformation.identityPubKey
);
const savedTxMap = new Map(allTxs.map((tx) => [tx.sparkID, tx]));
const depoistAddress = await queryAllStaticDepositAddresses(
- currentWalletMnemoincRef.current
+ currentMnemonicRef.current
);
// Loop through deposit addresses and check if they have been paid
@@ -521,16 +885,11 @@ const SparkWalletProvider = ({ children, navigate }) => {
);
console.log("Deposit address txids:", txids);
if (!txids || !txids.length) continue;
+
const unpaidTxids = txids.filter((txid) => !txid.didClaim);
let claimedTxs = Storage.getItem("claimedBitcoinTxs") || [];
for (const txid of unpaidTxids) {
- // get quote for the txid
- const { didwork, quote, error } =
- await getSparkStaticBitcoinL1AddressQuote(
- txid.txid,
- currentWalletMnemoincRef.current
- );
const hasAlreadySaved = savedTxMap.has(txid.txid);
if (!txid.isConfirmed) {
@@ -541,19 +900,36 @@ const SparkWalletProvider = ({ children, navigate }) => {
creditAmountSats: txid.amount - txid.fee,
},
address,
- sparkInformation
+ sparkInfoRef.current
);
}
+ continue;
}
+ // get quote for the txid
+ const {
+ didwork: quoteDidWorkResponse,
+ quote,
+ error,
+ } = await getSparkStaticBitcoinL1AddressQuote(
+ txid.txid,
+ currentMnemonicRef.current
+ );
- console.log("Deposit address quote:", quote);
-
- if (!didwork || !quote) {
+ if (!quoteDidWorkResponse || !quote) {
console.log(error, "Error getting deposit address quote");
if (
error.includes("UTXO is already claimed by the current user.")
) {
handleTxIdState(txid, true, address);
+ } else if (!hasAlreadySaved) {
+ await addPendingTransaction(
+ {
+ transactionId: txid.txid,
+ creditAmountSats: txid.amount - txid.fee,
+ },
+ address,
+ sparkInfoRef.current
+ );
}
continue;
}
@@ -569,9 +945,14 @@ const SparkWalletProvider = ({ children, navigate }) => {
} = await claimnSparkStaticDepositAddress({
...quote,
sspSignature: quote.signature,
- mnemonic: currentWalletMnemoincRef.current,
+ mnemonic: currentMnemonicRef.current,
});
+ // Add pending transaction if not already saved (after successful claim)
+ if (!hasAlreadySaved) {
+ await addPendingTransaction(quote, address, sparkInfoRef.current);
+ }
+
if (!claimTx || !didWork) {
console.log("Claim static deposit address error", claimError);
if (
@@ -583,10 +964,6 @@ const SparkWalletProvider = ({ children, navigate }) => {
continue;
}
- if (!hasAlreadySaved) {
- await addPendingTransaction(quote, address, sparkInformation);
- }
-
console.log("Claimed deposit address transaction:", claimTx);
if (!claimedTxs?.includes(quote.signature)) {
@@ -597,53 +974,56 @@ const SparkWalletProvider = ({ children, navigate }) => {
await new Promise((res) => setTimeout(res, 2000));
- const findBitcoinTxResponse = await findSignleTxFromHistory(
- claimTx.transferId,
- 5,
- currentWalletMnemoincRef.current
+ const bitcoinTransfer = await getSingleTxDetails(
+ currentMnemonicRef.current,
+ claimTx.transferId
);
let updatedTx = {};
- if (!findBitcoinTxResponse.tx) {
+ if (!bitcoinTransfer) {
updatedTx = {
useTempId: true,
id: claimTx.transferId,
tempId: quote.transactionId,
paymentStatus: "pending",
paymentType: "bitcoin",
- accountId: sparkInformation.identityPubKey,
+ accountId: sparkInfoRef.current.identityPubKey,
};
} else {
- const { tx: bitcoinTransfer } = findBitcoinTxResponse;
- if (!bitcoinTransfer) {
- updatedTx = {
- useTempId: true,
- id: claimTx.transferId,
- tempId: quote.transactionId,
- paymentStatus: "pending",
- paymentType: "bitcoin",
- accountId: sparkInformation.identityPubKey,
- };
- } else {
- updatedTx = {
- useTempId: true,
- tempId: quote.transactionId,
- id: bitcoinTransfer.id,
- paymentStatus: "completed",
- paymentType: "bitcoin",
- accountId: sparkInformation.identityPubKey,
- details: {
- amount: bitcoinTransfer.totalValue,
- fee: Math.abs(
- quote.creditAmountSats - bitcoinTransfer.totalValue
- ),
- },
- };
- }
+ updatedTx = {
+ useTempId: true,
+ tempId: quote.transactionId,
+ id: bitcoinTransfer.id,
+ paymentStatus: "completed",
+ paymentType: "bitcoin",
+ accountId: sparkInfoRef.current.identityPubKey,
+ details: {
+ amount: bitcoinTransfer.totalValue,
+ fee: Math.abs(
+ (txid.amount || quote.creditAmountSats) -
+ bitcoinTransfer.totalValue
+ ),
+ totalFee: Math.abs(
+ (txid.amount || quote.creditAmountSats) -
+ bitcoinTransfer.totalValue
+ ),
+ supportFee: 0,
+ },
+ };
}
-
- await bulkUpdateSparkTransactions([updatedTx], "fullUpdate");
console.log("Updated bitcoin transaction:", updatedTx);
+ await bulkUpdateSparkTransactions([updatedTx], "fullUpdate");
+ // If no details are provided do not show confirm screen
+ // Navigate here, since bulkUpdateSparkTransactions will default to transactions and get blocked in other path
+ if (updatedTx.details) {
+ if (handledNavigatedTxs.current.has(updatedTx.id)) return;
+ handledNavigatedTxs.current.add(updatedTx.id);
+ setPendingNavigation({
+ tx: updatedTx,
+ amount: updatedTx.details.amount,
+ showFullAnimation: false,
+ });
+ }
}
}
} catch (err) {
@@ -663,7 +1043,7 @@ const SparkWalletProvider = ({ children, navigate }) => {
address: address,
time: new Date().getTime(),
direction: "INCOMING",
- description: "Deposit address payment",
+ description: i18next.t("contexts.spark.depositLabel"),
onChainTxid: quote.transactionId,
isRestore: true, // This is a restore payment
},
@@ -671,19 +1051,78 @@ const SparkWalletProvider = ({ children, navigate }) => {
await addSingleSparkTransaction(pendingTx);
};
+ clearAllDepositIntervals();
if (depositAddressIntervalRef.current) {
clearInterval(depositAddressIntervalRef.current);
+ depositAddressIntervalRef.current = null;
}
- if (isSendingPayment) return;
+
if (!initialBitcoinIntervalRun.current) {
setTimeout(handleDepositAddressCheck, 1_000 * 5);
initialBitcoinIntervalRun.current = true;
}
- depositAddressIntervalRef.current = setInterval(
+
+ const depositIntervalId = setInterval(
handleDepositAddressCheck,
1_000 * 60
);
- }, [didGetToHomepage, sparkInformation.didConnect, isSendingPayment]);
+
+ depositAddressIntervalRef.current = depositIntervalId;
+ depositIntervalIds.add(depositIntervalId);
+
+ return () => {
+ console.log("Cleaning up deposit interval on unmount/dependency change");
+ if (depositIntervalId) {
+ clearInterval(depositIntervalId);
+ depositIntervalIds.delete(depositIntervalId);
+ }
+ if (depositAddressIntervalRef.current) {
+ clearInterval(depositAddressIntervalRef.current);
+ depositAddressIntervalRef.current = null;
+ }
+ };
+ }, [didGetToHomepage, sparkInformation.didConnect]);
+
+ // Run fullRestore when didConnect becomes true
+ useEffect(() => {
+ if (!sparkInformation.didConnect) return;
+ if (!sparkInformation.identityPubKey) return;
+ if (didRunInitialRestore.current) return;
+ didRunInitialRestore.current = true;
+
+ async function runRestore() {
+ const restoreResponse = await fullRestoreSparkState({
+ sparkAddress: sparkInfoRef.current.sparkAddress,
+ batchSize: isInitialRestore.current ? 5 : 2,
+ isSendingPayment: isSendingPaymentRef.current,
+ mnemonic: currentMnemonicRef.current,
+ identityPubKey: sparkInfoRef.current.identityPubKey,
+ isInitialRestore: isInitialRestore.current,
+ });
+
+ if (!restoreResponse) {
+ setRestoreCompleted(true); // This will get the transactions for the session
+ }
+ }
+
+ runRestore();
+ }, [sparkInformation.didConnect, sparkInformation.identityPubKey]);
+
+ // Run transactions after BOTH restore completes
+ useEffect(() => {
+ if (!restoreCompleted) return;
+
+ async function fetchTransactions() {
+ const transactions = await getCachedSparkTransactions(
+ null,
+ sparkInfoRef.current.identityPubKey
+ );
+ setSparkInformation((prev) => ({ ...prev, transactions }));
+ hasRestoreCompleted.current = true;
+ }
+
+ fetchTransactions();
+ }, [restoreCompleted]);
// This function connects to the spark node and sets the session up
@@ -694,6 +1133,7 @@ const SparkWalletProvider = ({ children, navigate }) => {
// toggleGlobalContactsInformation,
// globalContactsInformation,
mnemonic: accountMnemoinc,
+ hasRestoreCompleted: hasRestoreCompleted.current,
});
console.log(didWork, "did Connect to spark");
@@ -709,6 +1149,8 @@ const SparkWalletProvider = ({ children, navigate }) => {
useEffect(() => {
if (!sparkInformation.didConnect) return;
if (!globalContactsInformation?.myProfile) return;
+ if (!sparkInformation.identityPubKey) return;
+ if (!sparkInformation.sparkAddress) return;
if (sparkDBaddress.current) return;
sparkDBaddress.current = true;
@@ -728,34 +1170,23 @@ const SparkWalletProvider = ({ children, navigate }) => {
true
);
}
- }, [globalContactsInformation.myProfile, sparkInformation]);
-
- // This function checks to see if there are any liquid funds that need to be sent to spark
- useEffect(() => {
- async function swapLiquidToSpark() {
- try {
- if (liquidNodeInformation.userBalance > minMaxLiquidSwapAmounts.min) {
- setPendingLiquidPayment(true);
- await liquidToSparkSwap(
- globalContactsInformation.myProfile.uniqueName
- );
- }
- } catch (err) {
- console.log("transfering liquid to spark error", err);
- }
- }
-
- if (!didGetToHomepage) return;
- if (!sparkInformation.didConnect) return;
- swapLiquidToSpark();
}, [
- didGetToHomepage,
- liquidNodeInformation,
- minMaxLiquidSwapAmounts,
+ globalContactsInformation.myProfile,
sparkInformation.didConnect,
- globalContactsInformation?.myProfile?.uniqueName,
+ sparkInformation.identityPubKey,
+ sparkInformation.sparkAddress,
]);
+ // Cleanup on unmount
+ useEffect(() => {
+ return () => {
+ if (debounceTimeoutRef.current) {
+ clearTimeout(debounceTimeoutRef.current);
+ }
+ pendingTransferIds.current.clear();
+ };
+ }, []);
+
const contextValue = useMemo(
() => ({
sparkInformation,
@@ -765,10 +1196,9 @@ const SparkWalletProvider = ({ children, navigate }) => {
numberOfIncomingLNURLPayments,
setNumberOfIncomingLNURLPayments,
numberOfConnectionTries,
- numberOfCachedTxs,
- setNumberOfCachedTxs,
- setStartConnectingToSpark,
+
connectToSparkWallet,
+ isSendingPaymentRef,
}),
[
sparkInformation,
@@ -778,10 +1208,9 @@ const SparkWalletProvider = ({ children, navigate }) => {
numberOfIncomingLNURLPayments,
setNumberOfIncomingLNURLPayments,
numberOfConnectionTries,
- numberOfCachedTxs,
- setNumberOfCachedTxs,
- setStartConnectingToSpark,
+
connectToSparkWallet,
+ isSendingPaymentRef,
]
);
diff --git a/src/contexts/toastManager.css b/src/contexts/toastManager.css
new file mode 100644
index 0000000..baf6f74
--- /dev/null
+++ b/src/contexts/toastManager.css
@@ -0,0 +1,105 @@
+.toast-container {
+ position: fixed;
+ top: 20px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 95%;
+ max-width: 500px;
+ z-index: 1000;
+ pointer-events: none;
+}
+
+.toast {
+ border-radius: 12px;
+ padding: 16px 20px;
+ margin-bottom: 12px;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ opacity: 0;
+ transform: translateY(-20px);
+ transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+ pointer-events: all;
+ cursor: grab;
+ user-select: none;
+}
+
+.toast:active {
+ cursor: grabbing;
+}
+
+.toast-visible {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+.toast-clipboard {
+ background: var(--dmt);
+}
+.toast-confirmTx {
+ background: var(--dmt);
+}
+
+.toast-error {
+ background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
+ border-color: #ef5350;
+}
+
+.toast-warning {
+ background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
+ border-color: #ffa726;
+}
+
+.toast-info {
+ background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
+ border-color: #42a5f5;
+}
+
+.toast-success {
+ background: linear-gradient(135deg, #4caf50 0%, #388e3c 100%);
+ border-color: #66bb6a;
+}
+
+.toast-content {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.toast-icon {
+ font-size: 24px;
+ flex-shrink: 0;
+}
+
+.toast-text {
+ flex: 1;
+ min-width: 0;
+}
+
+.toast-title {
+ color: white;
+ font-size: 15px;
+ font-weight: 500;
+ display: block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.toast-message {
+ margin: 0;
+ font-size: 0.8em;
+}
+
+@media (max-width: 640px) {
+ .demo-title {
+ font-size: 2rem;
+ }
+
+ .button-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .toast-container {
+ width: 90%;
+ }
+}
diff --git a/src/contexts/toastManager.jsx b/src/contexts/toastManager.jsx
new file mode 100644
index 0000000..79895d6
--- /dev/null
+++ b/src/contexts/toastManager.jsx
@@ -0,0 +1,316 @@
+import { Clipboard, HelpCircle } from "lucide-react";
+import React, {
+ createContext,
+ useContext,
+ useReducer,
+ useCallback,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
+import { Colors } from "../constants/theme";
+import "./toastManager.css";
+import ThemeText from "../components/themeText/themeText";
+import { useTranslation } from "react-i18next";
+import displayCorrectDenomination from "../functions/displayCorrectDenomination";
+import { useSpark } from "./sparkContext";
+import { useNodeContext } from "./nodeContext";
+import { useGlobalContextProvider } from "./masterInfoObject";
+import { formatTokensNumber } from "../functions/lrc20/formatTokensBalance";
+
+// Toast Context
+const ToastContext = createContext();
+
+const initialState = {
+ toasts: [],
+};
+
+const toastReducer = (state, action) => {
+ switch (action.type) {
+ case "ADD_TOAST":
+ return {
+ ...state,
+ toasts: [...state.toasts, action.payload],
+ };
+ case "REMOVE_TOAST":
+ return {
+ ...state,
+ toasts: state.toasts.filter((toast) => toast.id !== action.payload),
+ };
+ case "CLEAR_TOASTS":
+ return {
+ ...state,
+ toasts: [],
+ };
+ default:
+ return state;
+ }
+};
+
+// Toast Provider Component
+export const ToastProvider = ({ children }) => {
+ const [state, dispatch] = useReducer(toastReducer, initialState);
+
+ const showToast = useCallback((toast) => {
+ const id = Date.now() + Math.random();
+ const toastWithId = {
+ id,
+ type: "success",
+ duration: 3000,
+ position: "top",
+ ...toast,
+ };
+
+ dispatch({ type: "ADD_TOAST", payload: toastWithId });
+
+ if (toastWithId.duration > 0) {
+ setTimeout(() => {
+ dispatch({ type: "REMOVE_TOAST", payload: id });
+ }, toastWithId.duration);
+ }
+
+ return id;
+ }, []);
+
+ const hideToast = useCallback((id) => {
+ dispatch({ type: "REMOVE_TOAST", payload: id });
+ }, []);
+
+ const clearToasts = useCallback(() => {
+ dispatch({ type: "CLEAR_TOASTS" });
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+};
+
+// Custom hook to use toast
+export const useToast = () => {
+ const context = useContext(ToastContext);
+ if (!context) {
+ throw new Error("useToast must be used within a ToastProvider");
+ }
+ return context;
+};
+
+// Individual Toast Component
+const Toast = ({
+ toast,
+ onHide,
+ fiatStats,
+ sparkInformation,
+ masterInfoObject,
+}) => {
+ const [isVisible, setIsVisible] = useState(false);
+ const [isDragging, setIsDragging] = useState(false);
+ const [dragY, setDragY] = useState(0);
+ const toastRef = useRef(null);
+ const startY = useRef(0);
+ const { t } = useTranslation();
+
+ useEffect(() => {
+ // Animate in
+ setTimeout(() => setIsVisible(true), 10);
+ }, []);
+
+ const handleAnimateOut = useCallback(() => {
+ setIsVisible(false);
+ setTimeout(() => {
+ onHide();
+ }, 300);
+ }, [onHide]);
+
+ const handleMouseDown = (e) => {
+ setIsDragging(true);
+ startY.current = e.clientY;
+ };
+
+ const handleTouchStart = (e) => {
+ setIsDragging(true);
+ startY.current = e.touches[0].clientY;
+ };
+
+ const handleMouseMove = (e) => {
+ if (!isDragging) return;
+ const currentY = e.clientY;
+ const diff = currentY - startY.current;
+ if (diff < 0) {
+ setDragY(diff);
+ }
+ };
+
+ const handleTouchMove = (e) => {
+ if (!isDragging) return;
+ const currentY = e.touches[0].clientY;
+ const diff = currentY - startY.current;
+ if (diff < 0) {
+ setDragY(diff);
+ }
+ };
+
+ const handleDragEnd = () => {
+ setIsDragging(false);
+ if (dragY < -20) {
+ handleAnimateOut();
+ } else {
+ setDragY(0);
+ }
+ };
+
+ useEffect(() => {
+ if (isDragging) {
+ document.addEventListener("mousemove", handleMouseMove);
+ document.addEventListener("mouseup", handleDragEnd);
+ document.addEventListener("touchmove", handleTouchMove);
+ document.addEventListener("touchend", handleDragEnd);
+ }
+ return () => {
+ document.removeEventListener("mousemove", handleMouseMove);
+ document.removeEventListener("mouseup", handleDragEnd);
+ document.removeEventListener("touchmove", handleTouchMove);
+ document.removeEventListener("touchend", handleDragEnd);
+ };
+ }, [isDragging, dragY]);
+
+ const getToastClass = () => {
+ const classes = ["toast"];
+ switch (toast.type) {
+ case "clipboard":
+ classes.push("toast-clipboard");
+ break;
+ case "confirmTx":
+ classes.push("toast-clipboard");
+ break;
+ case "error":
+ classes.push("toast-error");
+ break;
+ case "warning":
+ classes.push("toast-warning");
+ break;
+ case "info":
+ classes.push("toast-info");
+ break;
+ default:
+ classes.push("toast-default");
+ }
+ if (isVisible) classes.push("toast-visible");
+ return classes.join(" ");
+ };
+
+ const getIconForType = () => {
+ switch (toast.type) {
+ case "clipboard":
+ return "📋";
+ case "confirmTx":
+ return "ℹ️";
+ case "error":
+ return "✕";
+ case "warning":
+ return "⚠️";
+ case "info":
+ return "ℹ️";
+ case "success":
+ return "✓";
+ default:
+ return "•";
+ }
+ };
+ const token = toast.isLRC20Payment
+ ? sparkInformation.tokens?.[toast?.LRC20Token]
+ : "";
+
+ const formattedTokensBalance =
+ toast.type === "confirmTx" && !!token
+ ? formatTokensNumber(toast.amount, token?.tokenMetadata?.decimals)
+ : 0;
+
+ const toastStyle = {
+ transform: `translateY(${dragY}px)`,
+ transition: isDragging ? "none" : "transform 0.3s ease",
+ };
+
+ return (
+
+
+ {toast.type === "clipboard" ? (
+
+ ) : toast.type === "confirmTx" ? (
+
+ ) : (
+
+ )}
+
+ {toast.type === "confirmTx" ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
+
+
+
+ );
+};
+
+// Toast Container Component
+export const ToastContainer = () => {
+ const { toasts, hideToast } = useToast();
+ const { sparkInformation } = useSpark();
+ const { fiatStats } = useNodeContext();
+ const { masterInfoObject } = useGlobalContextProvider();
+
+ return (
+
+ {toasts.map((toast) => (
+ hideToast(toast.id)}
+ sparkInformation={sparkInformation}
+ fiatStats={fiatStats}
+ masterInfoObject={masterInfoObject}
+ />
+ ))}
+
+ );
+};
diff --git a/src/fonts.css b/src/fonts.css
index fe9b6d3..d9c7e5f 100644
--- a/src/fonts.css
+++ b/src/fonts.css
@@ -1,3 +1,10 @@
+@font-face {
+ font-family: "Blitzicons1";
+ src: url("./assets/fonts/Blitzicons1.ttf") format("truetype");
+ font-weight: normal;
+ font-style: normal;
+}
+
/* 100 Thin */
@font-face {
font-family: "Poppins";
diff --git a/src/functions/apps/giftCardPurchaseTracker.js b/src/functions/apps/giftCardPurchaseTracker.js
new file mode 100644
index 0000000..c3dbd11
--- /dev/null
+++ b/src/functions/apps/giftCardPurchaseTracker.js
@@ -0,0 +1,57 @@
+import i18next from "i18next";
+import { SATSPERBITCOIN } from "../../constants";
+import { isMoreThanADayOld } from "../rotateAddressDateChecker";
+import Storage from "../localStorage";
+
+export default async function giftCardPurchaseAmountTracker({
+ sendingAmountSat,
+ USDBTCValue,
+ testOnly = false,
+}) {
+ const currentTime = new Date();
+ try {
+ Storage.getItem;
+ const dailyPurchaseAmount = Storage.getItem("dailyPurchaeAmount");
+
+ if (dailyPurchaseAmount) {
+ if (isMoreThanADayOld(dailyPurchaseAmount.date)) {
+ if (!testOnly) {
+ Storage.setItem("dailyPurchaeAmount", {
+ date: currentTime,
+ amount: sendingAmountSat,
+ });
+ }
+ } else {
+ const totalPurchaseAmount = Math.round(
+ ((dailyPurchaseAmount.amount + sendingAmountSat) / SATSPERBITCOIN) *
+ USDBTCValue.value
+ );
+
+ if (totalPurchaseAmount > 9000)
+ throw new Error(
+ i18next.t(
+ "apps.giftCards.expandedGiftCardPage.dailyPurchaseAmountError"
+ )
+ );
+
+ if (!testOnly) {
+ Storage.setItem("dailyPurchaeAmount", {
+ date: dailyPurchaseAmount.date,
+ amount: dailyPurchaseAmount.amount + sendingAmountSat,
+ });
+ }
+ }
+ } else {
+ if (!testOnly) {
+ Storage.setItem("dailyPurchaeAmount", {
+ date: currentTime,
+ amount: sendingAmountSat,
+ });
+ }
+ }
+ return { shouldBlock: false };
+ } catch (err) {
+ console.log(err);
+ return { shouldBlock: true, reason: err.message };
+ }
+}
diff --git a/src/functions/cachedImage.js b/src/functions/cachedImage.js
new file mode 100644
index 0000000..1e8e8cb
--- /dev/null
+++ b/src/functions/cachedImage.js
@@ -0,0 +1,126 @@
+/**
+ * getCachedProfileImage.js
+ * Browser-compatible profile image caching using storage.js
+ */
+
+import { getStorage, ref, getDownloadURL, getMetadata } from "firebase/storage";
+import {
+ deleteCachedImage,
+ downloadAndCacheImage,
+ getCachedImage,
+ getCachedImageMetadata,
+} from "./images/storage";
+import { BLITZ_PROFILE_IMG_STORAGE_REF } from "../constants";
+
+// Helper to generate cache key
+const CACHE_KEY = (uuid) => `${BLITZ_PROFILE_IMG_STORAGE_REF}/${uuid}`;
+
+/**
+ * Get cached profile image from Firebase Storage
+ * Checks cache first, validates against Firebase metadata, downloads if needed
+ *
+ * @param {string} uuid - User unique identifier
+ * @param {Object} storage - Firebase storage instance (optional, will use default if not provided)
+ * @param {boolean} asDataURL - If true, returns data URL; if false, returns object URL
+ * @returns {Promise<{localUri: string, updated: string, isObjectURL?: boolean}|null>}
+ */
+export async function getCachedProfileImage(
+ uuid,
+ storage = null,
+ asDataURL = true
+) {
+ try {
+ // Use provided storage or get default
+ const storageInstance = storage || getStorage();
+
+ const key = CACHE_KEY(uuid);
+
+ // Get Firebase Storage reference
+ const reference = ref(
+ storageInstance,
+ `${BLITZ_PROFILE_IMG_STORAGE_REF}/${uuid}.jpg`
+ );
+
+ // Get metadata from Firebase to check if image has been updated
+ const metadata = await getMetadata(reference);
+ const updated = metadata.updated;
+
+ // Check for cached image and its metadata
+ const cachedMetadata = await getCachedImageMetadata(key);
+
+ // If cached version matches current version, return cached image
+ if (cachedMetadata?.updated === updated) {
+ const cachedImage = await getCachedImage(key, asDataURL);
+
+ if (cachedImage) {
+ return {
+ localUri: cachedImage.uri,
+ updated: cachedMetadata.updated,
+ isObjectURL: cachedImage.isObjectURL,
+ };
+ }
+ }
+
+ // Download new image if cache is stale or doesn't exist
+ const url = await getDownloadURL(reference);
+
+ // Download and cache the image with metadata
+ const blob = await downloadAndCacheImage(key, url, { updated });
+
+ if (!blob) {
+ throw new Error("Failed to download image");
+ }
+
+ // Get the newly cached image
+ const cachedImage = await getCachedImage(key, asDataURL);
+
+ if (!cachedImage) {
+ throw new Error("Failed to retrieve cached image");
+ }
+
+ return {
+ localUri: cachedImage.uri,
+ updated,
+ isObjectURL: cachedImage.isObjectURL,
+ };
+ } catch (e) {
+ console.log("Error caching profile image:", e);
+ return null;
+ }
+}
+
+/**
+ * Preload multiple profile images into cache
+ * Useful for loading images in bulk (e.g., contact list)
+ *
+ * @param {string[]} uuids - Array of user UUIDs
+ * @param {Object} storage - Firebase storage instance
+ * @returns {Promise
-
+
-
{
+ setValue(1);
navigate("/wallet", { replace: true });
}, [navigate]);
return (
diff --git a/src/pages/contacts/components/EditProfileTextInput/EditProfileTextInput.jsx b/src/pages/contacts/components/EditProfileTextInput/EditProfileTextInput.jsx
new file mode 100644
index 0000000..24b40f8
--- /dev/null
+++ b/src/pages/contacts/components/EditProfileTextInput/EditProfileTextInput.jsx
@@ -0,0 +1,115 @@
+import React, { useState, useRef } from "react";
+import { Info } from "lucide-react";
+import "./editProfileTextInput.css";
+import { useThemeContext } from "../../../../contexts/themeContext";
+import { Colors } from "../../../../constants/theme";
+import CustomInput from "../../../../components/customInput/customInput";
+import ThemeText from "../../../../components/themeText/themeText";
+
+/**
+ * Reusable text input component for edit profile forms
+ */
+export function EditProfileTextInput({
+ label,
+ placeholder,
+ value = "",
+ onChangeText,
+ onFocus,
+ onBlur,
+ inputRef,
+ maxLength = 30,
+ multiline = false,
+ minHeight,
+ maxHeight,
+ isDarkMode = false,
+ showInfoIcon = false,
+ onInfoPress,
+ containerStyle = {},
+}) {
+ const { theme, darkModeType } = useThemeContext();
+ const isOverLimit = value.length >= maxLength;
+
+ const handleChange = (e) => {
+ console.log(e);
+ onChangeText?.(e);
+ };
+
+ const handleContainerClick = () => {
+ inputRef?.current?.focus();
+ };
+
+ return (
+
+ {showInfoIcon ? (
+
+
+
+
+ ) : (
+
+ )}
+
+ {/* {multiline ? (
+
+ ) : (
+
+ )} */}
+
+
+ );
+}
diff --git a/src/pages/contacts/components/EditProfileTextInput/editProfileTextInput.css b/src/pages/contacts/components/EditProfileTextInput/editProfileTextInput.css
new file mode 100644
index 0000000..c8a5d88
--- /dev/null
+++ b/src/pages/contacts/components/EditProfileTextInput/editProfileTextInput.css
@@ -0,0 +1,188 @@
+.text-input-container {
+ width: 100%;
+ margin-bottom: 20px;
+ cursor: text;
+}
+
+.text-input-container:last-child {
+ margin-bottom: 0;
+}
+
+.label-row {
+ display: flex;
+ align-items: center;
+ margin-bottom: 8px;
+}
+
+.input-label {
+ display: block;
+ cursor: text;
+ margin-bottom: 8px;
+}
+.label-row .input-label {
+ margin-bottom: 0;
+}
+.info-button {
+ background: none;
+ border: none;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+}
+
+.text-input {
+ width: 100%;
+ max-width: unset;
+ border: none;
+ resize: vertical;
+ margin-bottom: 8px;
+}
+
+.text-input.over-limit {
+ border-color: #ef4444;
+ color: #ef4444;
+}
+
+.text-input.over-limit:focus {
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
+}
+
+.character-count {
+ text-align: right;
+}
+
+.dark .character-count {
+ color: #a0aec0;
+}
+
+.character-count.over-limit {
+ color: #ef4444;
+ font-weight: 600;
+}
+
+.submit-button {
+ width: 100%;
+ padding: 16px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border: none;
+ border-radius: 12px;
+ color: white;
+ font-size: 18px;
+ font-weight: 700;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+.submit-button:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
+}
+
+.submit-button:active {
+ transform: translateY(0);
+}
+
+.info-modal {
+ position: fixed;
+ top: 20px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: #667eea;
+ color: white;
+ padding: 16px 24px;
+ border-radius: 12px;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ font-weight: 600;
+ z-index: 1000;
+ animation: slideIn 0.3s ease;
+ max-width: 90%;
+}
+
+@keyframes slideIn {
+ from {
+ transform: translate(-50%, -20px);
+ opacity: 0;
+ }
+ to {
+ transform: translate(-50%, 0);
+ opacity: 1;
+ }
+}
+
+.features-card {
+ background: white;
+ border-radius: 24px;
+ padding: 32px;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+}
+
+.dark-mode .features-card {
+ background: rgba(45, 55, 72, 0.95);
+}
+
+.features-title {
+ font-size: 22px;
+ font-weight: 700;
+ color: #2d3748;
+ margin-bottom: 20px;
+ text-align: center;
+}
+
+.dark-mode .features-title {
+ color: #e2e8f0;
+}
+
+.features-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 12px;
+}
+
+.feature {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 12px 16px;
+ background: #f7fafc;
+ border-radius: 10px;
+ color: #4a5568;
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.dark-mode .feature {
+ background: #2d3748;
+ color: #cbd5e0;
+}
+
+.feature-icon {
+ color: #48bb78;
+ font-weight: 700;
+ font-size: 16px;
+}
+
+@media (max-width: 640px) {
+ .title {
+ font-size: 2rem;
+ }
+
+ .header {
+ flex-direction: column;
+ gap: 16px;
+ align-items: flex-start;
+ }
+
+ .profile-form,
+ .features-card {
+ padding: 24px;
+ border-radius: 16px;
+ }
+
+ .features-grid {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/src/pages/contacts/components/ExpandedContactsPage/expandedContactsPage.css b/src/pages/contacts/components/ExpandedContactsPage/expandedContactsPage.css
new file mode 100644
index 0000000..3388319
--- /dev/null
+++ b/src/pages/contacts/components/ExpandedContactsPage/expandedContactsPage.css
@@ -0,0 +1,148 @@
+/* Flex Container */
+.flex-container {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+/* Top Bar Navigation */
+.top-bar {
+ width: 100%;
+ min-height: 40px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ margin-bottom: 15px;
+}
+
+.back-button-container {
+ margin-right: auto;
+ background: none;
+ border: none;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+}
+
+/* Profile Image */
+.profile-image-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: none;
+ border: none;
+ padding: 0;
+ position: relative;
+
+ margin: 0 auto;
+}
+
+.profile-image-button.active {
+ cursor: pointer;
+}
+
+.profile-image-button.active:hover {
+ opacity: 0.9;
+}
+
+.profile-image-button.inactive {
+ cursor: default;
+}
+
+.profile-image {
+ width: 150px;
+ height: 150px;
+ border-radius: 125px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 10px;
+ overflow: hidden;
+}
+
+.select-from-photos {
+ width: 30px;
+ height: 30px;
+ border-radius: 20px;
+ background-color: var(--dark-mode-text, #ffffff);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ right: 12.5px;
+ bottom: 12.5px;
+ z-index: 2;
+}
+
+/* Button Container */
+.button-global-container {
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ justify-content: center;
+ margin-bottom: 10px;
+}
+
+/* Bio Container */
+.bio-container {
+ width: 90%;
+ min-height: 60px;
+ max-height: 80px;
+ border-radius: 8px;
+ padding: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 0 auto;
+}
+
+.bio-scroll {
+ display: flex;
+ align-items: center;
+ flex-grow: 1;
+ overflow-y: auto;
+ max-height: 100%;
+}
+
+.bio-scroll::-webkit-scrollbar {
+ display: none;
+}
+
+.bio-scroll {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+
+/* Contacts List Container */
+.contacts-list-container {
+ flex: 1;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+}
+
+.contacts-list-container::-webkit-scrollbar {
+ width: 8px;
+}
+
+.contacts-list-container::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.contacts-list-container::-webkit-scrollbar-thumb {
+ background: rgba(136, 136, 136, 0.3);
+ border-radius: 4px;
+}
+
+.contacts-list-container::-webkit-scrollbar-thumb:hover {
+ background: rgba(136, 136, 136, 0.5);
+}
+
+/* Transactions List */
+.transactions-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
diff --git a/src/pages/contacts/components/ExpandedContactsPage/expandedContactsPage.jsx b/src/pages/contacts/components/ExpandedContactsPage/expandedContactsPage.jsx
new file mode 100644
index 0000000..c524a63
--- /dev/null
+++ b/src/pages/contacts/components/ExpandedContactsPage/expandedContactsPage.jsx
@@ -0,0 +1,419 @@
+import { useLocation, useNavigate } from "react-router-dom";
+import { useCallback, useEffect, useMemo } from "react";
+
+import { useTranslation } from "react-i18next";
+
+import ContactsTransactionItem from "../../internalComponents/contactTransactions/contactsTransactions";
+import ThemeText from "../../../../components/themeText/themeText";
+import { useAppStatus } from "../../../../contexts/appStatus";
+import { useThemeContext } from "../../../../contexts/themeContext";
+import useThemeColors from "../../../../hooks/useThemeColors";
+import { queueSetCashedMessages } from "../../../../functions/messaging/cachedMessages";
+import FullLoadingScreen from "../../../../components/fullLoadingScreen/fullLoadingScreen";
+import CustomSendAndRequsetBTN from "../../../../components/sendAndRequsetButton/customSendAndRequsetButton";
+import ContactProfileImage from "../profileImage/profileImage";
+import { useImageCache } from "../../../../contexts/imageCacheContext";
+import { useServerTimeOnly } from "../../../../contexts/serverTime";
+import { useExpandedNavbar } from "../../utils/useExpandedNavbar";
+import "./expandedContactsPage.css";
+import { useGlobalContacts } from "../../../../contexts/globalContacts";
+import { ArrowLeft, Settings, Share, Star } from "lucide-react";
+import {
+ Colors,
+ HIDDEN_OPACITY,
+ INSET_WINDOW_WIDTH,
+} from "../../../../constants/theme";
+import { useOverlay } from "../../../../contexts/overlayContext";
+
+export default function ExpandedContactsPage({
+ uuid,
+ hideProfileImage: globalHide,
+}) {
+ const { openOverlay } = useOverlay();
+ const navigate = useNavigate();
+ const location = useLocation();
+ const props = location.state;
+ const { isConnectedToTheInternet } = useAppStatus();
+ const { theme, darkModeType } = useThemeContext();
+ const {
+ backgroundOffset,
+ backgroundColor,
+ textInputColor,
+ textInputBackground,
+ } = useThemeColors();
+ const { decodedAddedContacts, globalContactsInformation, contactsMessags } =
+ useGlobalContacts();
+ const { cache } = useImageCache();
+ const getServerTime = useServerTimeOnly();
+ const currentTime = getServerTime();
+ const { t } = useTranslation();
+ const { handleFavortie, handleSettings } = useExpandedNavbar();
+ const selectedUUID = uuid || props?.uuid;
+ const myProfile = globalContactsInformation?.myProfile;
+ const hideProfileImage = globalHide || props?.hideProfileImage;
+ console.log(decodedAddedContacts, props, "hide profile image");
+ const [selectedContact] = useMemo(
+ () =>
+ decodedAddedContacts.filter((contact) => contact?.uuid === selectedUUID),
+ [decodedAddedContacts, selectedUUID]
+ );
+ console.log(selectedContact);
+ const imageData = cache[selectedContact.uuid];
+ const contactTransactions = contactsMessags[selectedUUID]?.messages || [];
+
+ useEffect(() => {
+ //listening for messages when you're on the contact
+ async function updateSeenTransactions() {
+ const newMessagesList = [];
+ let consecutiveSeenCount = 0;
+ const REQUIRED_CONSECUTIVE_SEEN = 100;
+
+ for (let i = 0; i < contactTransactions.length; i++) {
+ const msg = contactTransactions[i];
+
+ if (msg.message.wasSeen) {
+ consecutiveSeenCount++;
+ if (consecutiveSeenCount >= REQUIRED_CONSECUTIVE_SEEN) {
+ break;
+ }
+ } else {
+ consecutiveSeenCount = 0;
+ newMessagesList.push({
+ ...msg,
+ message: { ...msg.message, wasSeen: true },
+ });
+ }
+ }
+
+ if (!newMessagesList.length) return;
+
+ queueSetCashedMessages({
+ newMessagesList,
+ myPubKey: globalContactsInformation.myProfile.uuid,
+ });
+ }
+
+ updateSeenTransactions();
+ }, [contactTransactions]);
+
+ console.log(contactTransactions);
+ const handleShare = () => {
+ if (selectedContact?.isLNURL || !selectedContact?.uniqueName) return;
+
+ const shareText = `${t("share.contact")}\nhttps://blitzwalletapp.com/u/${
+ selectedContact?.uniqueName
+ }`;
+
+ if (navigator.share) {
+ navigator
+ .share({
+ text: shareText,
+ })
+ .catch((err) => console.log("Share failed:", err));
+ } else {
+ navigator.clipboard.writeText(shareText);
+ // Optionally show a toast notification
+ }
+ };
+
+ // Header component for the list
+ const ListHeaderComponent = useCallback(
+ () => (
+ <>
+ {!hideProfileImage && (
+
+ )}
+
+
+
+ {selectedContact.uniqueName && (
+
+ )}
+
+
+ {
+ if (!isConnectedToTheInternet) {
+ openOverlay({
+ for: "error",
+ errorMessage: t("errormessages.nointernet"),
+ });
+ return;
+ }
+ navigate("/sendAndRequestPage", {
+ state: {
+ selectedContact: selectedContact,
+ paymentType: "send",
+ imageData,
+ },
+ });
+ }}
+ arrowColor={
+ theme
+ ? darkModeType
+ ? Colors.lightsout.background
+ : Colors.dark.background
+ : Colors.constants.blue
+ }
+ containerBackgroundColor={Colors.dark.text}
+ containerStyles={{ marginRight: 30 }}
+ />
+
+ {
+ if (selectedContact.isLNURL) {
+ openOverlay({
+ for: "error",
+ errorMessage: t(
+ "contacts.expandedContactPage.requestLNURLError"
+ ),
+ });
+ return;
+ }
+ if (!isConnectedToTheInternet) {
+ openOverlay({
+ for: "error",
+ errorMessage: t("errormessages.nointernet"),
+ });
+ return;
+ }
+ navigate("/sendAndRequestPage", {
+ state: {
+ selectedContact: selectedContact,
+ paymentType: "request",
+ imageData,
+ },
+ });
+ }}
+ arrowColor={
+ theme
+ ? darkModeType
+ ? Colors.lightsout.background
+ : Colors.dark.background
+ : Colors.constants.blue
+ }
+ containerBackgroundColor={Colors.dark.text}
+ containerStyles={{
+ opacity: selectedContact.isLNURL ? HIDDEN_OPACITY : 1,
+ }}
+ />
+
+
+ {!!selectedContact?.bio?.trim() && (
+
+ )}
+ >
+ ),
+ [
+ theme,
+ darkModeType,
+ selectedContact?.name,
+ selectedContact?.uniqueName,
+ selectedContact?.bio,
+ selectedContact?.isLNURL,
+ imageData?.updated,
+ imageData?.localUri,
+ isConnectedToTheInternet,
+ hideProfileImage,
+ contactTransactions.length,
+ ]
+ );
+
+ if (hideProfileImage) {
+ return (
+
+ {!selectedContact ? (
+
+ ) : contactTransactions.length !== 0 ? (
+ <>
+
+
+ {contactTransactions.slice(0, 50).map((item, index) => (
+
+ ))}
+
+ >
+ ) : (
+
+
+
+
+ )}
+
+ );
+ }
+
+ return (
+
+
+
+ {selectedContact && (
+
handleFavortie({ selectedContact })}
+ style={{ marginRight: "10px", cursor: "pointer" }}
+ fill={
+ selectedContact?.isFavorite
+ ? theme && darkModeType
+ ? Colors.dark.text
+ : Colors.constants.blue
+ : "transparent"
+ }
+ color={
+ theme && darkModeType ? Colors.dark.text : Colors.constants.blue
+ }
+ size={30}
+ />
+ )}
+ {selectedContact && (
+ handleSettings({ selectedContact })}
+ color={
+ theme && darkModeType ? Colors.dark.text : Colors.constants.blue
+ }
+ size={30}
+ />
+ )}
+
+
+ {!selectedContact ? (
+
+ ) : contactTransactions.length !== 0 ? (
+
+
+
+ {contactTransactions.slice(0, 50).map((item, index) => (
+
+ ))}
+
+
+ ) : (
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/pages/contacts/components/addContactPage/addContactPage.jsx b/src/pages/contacts/components/addContactPage/addContactPage.jsx
new file mode 100644
index 0000000..c01e3e2
--- /dev/null
+++ b/src/pages/contacts/components/addContactPage/addContactPage.jsx
@@ -0,0 +1,55 @@
+import React from "react";
+import "./style.css";
+import useThemeColors from "../../../../hooks/useThemeColors";
+import { useGlobalContacts } from "../../../../contexts/globalContacts";
+import CustomButton from "../../../../components/customButton/customButton";
+import { useTranslation } from "react-i18next";
+import ThemeText from "../../../../components/themeText/themeText";
+
+export default function AddContactPage({ selectedContact }) {
+ const newContact = selectedContact;
+
+ const { textInputBackground, textInputColor } = useThemeColors();
+ const { addContact } = useGlobalContacts();
+ const { t } = useTranslation();
+
+ const name = newContact?.name?.trim() || t("constants.annonName");
+ const username = newContact?.uniqueName;
+ const lnurl = newContact?.isLNURL ? newContact?.receiveAddress : null;
+ const bio = newContact?.bio?.trim() || t("constants.noBioSet");
+
+ return (
+
+
+
+ {!!username && (
+
+ )}
+
+ {!!lnurl && (
+
+
+
+
+ )}
+
+
+
+
+
+
{
+ addContact(newContact);
+ }}
+ buttonStyles={{ marginTop: "auto" }}
+ textContent={t("contacts.editMyProfilePage.addContactBTN")}
+ />
+
+ );
+}
diff --git a/src/pages/contacts/components/addContactPage/style.css b/src/pages/contacts/components/addContactPage/style.css
new file mode 100644
index 0000000..2271408
--- /dev/null
+++ b/src/pages/contacts/components/addContactPage/style.css
@@ -0,0 +1,78 @@
+.container {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.name-text {
+ text-align: center;
+ opacity: 0.6;
+ margin-top: 10px;
+ margin-bottom: 5px;
+}
+
+.username-text {
+ text-align: center;
+ margin-top: 0px;
+ margin-bottom: 20px;
+}
+
+.info-container {
+ width: 90%;
+ max-width: 500px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-bottom: 12px;
+}
+
+.info-label {
+ font-size: 16px;
+ opacity: 0.6;
+ margin-bottom: 4px;
+}
+
+.info-value {
+ margin-bottom: 10px;
+ word-break: break-all;
+ text-align: center;
+}
+
+.bio-container {
+ width: 90%;
+ max-width: 500px;
+ min-height: 60px;
+ max-height: 80px;
+ border-radius: 8px;
+ padding: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: auto;
+ margin-bottom: 20px;
+}
+
+.bio-text {
+ text-align: center;
+}
+
+.custom-button {
+ padding: 12px 24px;
+ background-color: #007aff;
+ color: white;
+ border: none;
+ border-radius: 8px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.custom-button:hover {
+ background-color: #0056b3;
+}
+
+.custom-button:active {
+ background-color: #004494;
+}
diff --git a/src/pages/contacts/components/addContactsHalfModal/addContactsHalfModal.jsx b/src/pages/contacts/components/addContactsHalfModal/addContactsHalfModal.jsx
new file mode 100644
index 0000000..732ebe0
--- /dev/null
+++ b/src/pages/contacts/components/addContactsHalfModal/addContactsHalfModal.jsx
@@ -0,0 +1,256 @@
+import { useState, useEffect, useRef, useCallback } from "react";
+import "./style.css";
+import customUUID from "../../../../functions/customUUID";
+import useDebounce from "../../../../hooks/useDebounce";
+import { EMAIL_REGEX, VALID_USERNAME_REGEX } from "../../../../constants";
+import { searchUsers } from "../../../../../db";
+import { getCachedProfileImage } from "../../../../functions/cachedImage";
+import ContactProfileImage from "../profileImage/profileImage";
+import { useThemeContext } from "../../../../contexts/themeContext";
+import useThemeColors from "../../../../hooks/useThemeColors";
+import ThemeText from "../../../../components/themeText/themeText";
+import { useNavigate } from "react-router-dom";
+import CustomButton from "../../../../components/customButton/customButton";
+import { useTranslation } from "react-i18next";
+import CustomInput from "../../../../components/customInput/customInput";
+import FullLoadingScreen from "../../../../components/fullLoadingScreen/fullLoadingScreen";
+
+// Main Component
+export default function AddContactsModal({ onClose, params }) {
+ const navigate = useNavigate();
+ const [searchInput, setSearchInput] = useState(
+ params.startingSearchValue || ""
+ );
+ const { t } = useTranslation();
+ const [users, setUsers] = useState([]);
+ const [isSearching, setIsSearching] = useState(false);
+ const [isKeyboardActive, setIsKeyboardActive] = useState(false);
+ const searchInputRef = useRef(null);
+ const searchTrackerRef = useRef(null);
+ const didClickCamera = useRef(null);
+ const { theme, darkModeType } = useThemeContext();
+
+ // Mock context values
+ const globalContactsInformation = {
+ myProfile: { uniqueName: "currentUser" },
+ };
+
+ const isUsingLNURL =
+ searchInput?.includes("@") && searchInput?.indexOf("@") !== 0;
+
+ useEffect(() => {
+ if (params.startingSearchValue) {
+ handleSearch(params.startingSearchValue);
+ }
+ }, []);
+
+ useEffect(() => {
+ searchInputRef.current?.focus();
+ }, []);
+
+ const handleSearchTrackerRef = () => {
+ const requestUUID = customUUID();
+ searchTrackerRef.current = requestUUID;
+ return requestUUID;
+ };
+
+ const debouncedSearch = useDebounce(async (term, requestUUID) => {
+ if (searchTrackerRef.current !== requestUUID) {
+ return;
+ }
+
+ const searchTerm = term.replace(/@/g, "");
+ if (searchTerm && VALID_USERNAME_REGEX.test(searchTerm)) {
+ const results = await searchUsers(searchTerm);
+ const newUsers = (
+ await Promise.all(
+ results.map(async (savedContact) => {
+ if (!savedContact) return false;
+ if (
+ savedContact.uniqueName ===
+ globalContactsInformation.myProfile.uniqueName
+ )
+ return false;
+ if (!savedContact?.uuid) return false;
+
+ let responseData;
+ if (
+ savedContact.hasProfileImage ||
+ typeof savedContact.hasProfileImage === "boolean"
+ ) {
+ responseData = await getCachedProfileImage(savedContact.uuid);
+ console.log(responseData);
+ }
+
+ if (!responseData) return savedContact;
+ else return { ...savedContact, ...responseData };
+ })
+ )
+ ).filter(Boolean);
+
+ setIsSearching(false);
+ setUsers(newUsers);
+ } else {
+ setIsSearching(false);
+ }
+ }, 650);
+
+ const handleSearch = (term) => {
+ setSearchInput(term);
+
+ if (isUsingLNURL) {
+ searchTrackerRef.current = null;
+ setIsSearching(false);
+ return;
+ }
+
+ if (term.length === 0 || term === "@") {
+ searchTrackerRef.current = null;
+ setUsers([]);
+ setIsSearching(false);
+ return;
+ }
+
+ if (term.length > 0) {
+ const requestUUID = handleSearchTrackerRef();
+ setIsSearching(true);
+ debouncedSearch(term, requestUUID);
+ }
+ };
+
+ const clearHalfModalForLNURL = () => {
+ if (!EMAIL_REGEX.test(searchInput)) return;
+
+ const newContact = {
+ name: searchInput.split("@")[0],
+ bio: "",
+ uniqueName: "",
+ isFavorite: false,
+ transactions: [],
+ unlookedTransactions: 0,
+ receiveAddress: searchInput,
+ isAdded: true,
+ isLNURL: true,
+ profileImage: "",
+ uuid: customUUID(),
+ };
+
+ console.log("Navigate to expanded page with LNURL:", newContact);
+
+ if (onClose) {
+ onClose();
+ }
+
+ navigate("/expandedAddContactsPage", {
+ state: newContact,
+ });
+ };
+
+ return (
+
+
+
+ {isSearching && (
+
+ )}
+
+
+
+
+ {isUsingLNURL ? (
+
+
+
+
+
+ ) : (
+
+ {users.length > 0 ? (
+ users.map((item) => (
+
+ ))
+ ) : (
+
+ {isSearching && searchInput.length > 0
+ ? ""
+ : searchInput.length > 0 && searchInput !== "@"
+ ? "No profiles found"
+ : "Search by LNURL (e.g. name@service.com) or Blitz username"}
+
+ )}
+
+ )}
+
+ );
+}
+
+function ContactListItem({ savedContact, theme, darkModeType, onClose }) {
+ const { backgroundOffset } = useThemeColors();
+ const navigate = useNavigate();
+ const newContact = {
+ ...savedContact,
+ isFavorite: false,
+ transactions: [],
+ unlookedTransactions: 0,
+ isAdded: true,
+ };
+
+ const handleClick = () => {
+ console.log("Navigate to expanded page with:", newContact);
+ if (onClose) {
+ onClose();
+ }
+
+ navigate("/expandedAddContactsPage", {
+ state: newContact,
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/src/pages/contacts/components/addContactsHalfModal/style.css b/src/pages/contacts/components/addContactsHalfModal/style.css
new file mode 100644
index 0000000..d15f933
--- /dev/null
+++ b/src/pages/contacts/components/addContactsHalfModal/style.css
@@ -0,0 +1,156 @@
+.modal-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 90%;
+ margin: 10px auto 0;
+}
+
+.title-container {
+ display: flex;
+ align-items: center;
+ margin-bottom: 10px;
+ width: 100%;
+ margin-top: 10px;
+}
+
+.title-text {
+ font-size: 20px;
+ font-weight: 400;
+ margin: 0;
+ margin-right: 10px;
+}
+
+.search-container {
+ position: relative;
+ width: 100%;
+}
+
+.search-input {
+ width: 100%;
+ padding: 12px 48px 12px 16px;
+ border: 1px solid #d1d5db;
+ border-radius: 8px;
+ font-size: 16px;
+ outline: none;
+ transition: border-color 0.2s;
+}
+
+.search-input:focus {
+ border-color: #3b82f6;
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
+}
+
+.camera-button {
+ position: absolute;
+ right: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ background: none;
+ border: none;
+ color: #3b82f6;
+ cursor: pointer;
+ padding: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.camera-icon {
+ width: 24px;
+ height: 24px;
+}
+
+.lnurl-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+}
+
+.lnurl-text {
+ text-align: center;
+ margin-bottom: 8px;
+ opacity: 0.6;
+}
+
+.lnurl-address {
+ text-align: center;
+ margin: 0;
+ margin-bottom: 24px;
+}
+
+.continue-button {
+ padding: 10px 24px;
+ background-color: #3b82f6;
+ color: white;
+ border: none;
+ border-radius: 8px;
+ font-size: 16px;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.users-list {
+ width: 100%;
+ max-height: 384px;
+ overflow-y: auto;
+}
+
+.empty-message {
+ text-align: center;
+ color: #6b7280;
+ margin-top: 16px;
+}
+
+.contact-item {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ padding: 8px 0;
+ background: none;
+ border: none;
+ cursor: pointer;
+ border-radius: 8px;
+ margin-bottom: 8px;
+}
+
+.contact-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 12px;
+ flex-shrink: 0;
+ overflow: hidden;
+}
+
+.contact-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.contact-initial {
+ color: white;
+ font-weight: 600;
+ font-size: 18px;
+}
+
+.contact-info {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+}
+
+.contact-username {
+ margin: 0;
+}
+
+.contact-name {
+ font-size: 14px;
+ opacity: 0.6;
+ margin: 0;
+}
diff --git a/src/pages/contacts/components/contactElement/contactElement.jsx b/src/pages/contacts/components/contactElement/contactElement.jsx
deleted file mode 100644
index f5a44ea..0000000
--- a/src/pages/contacts/components/contactElement/contactElement.jsx
+++ /dev/null
@@ -1,184 +0,0 @@
-import { useNavigate } from "react-router-dom";
-import { useAppStatus } from "../../../../contexts/appStatus";
-import { useGlobalContacts } from "../../../../contexts/globalContacts";
-import { useKeysContext } from "../../../../contexts/keysContext";
-import ThemeText from "../../../../components/themeText/themeText";
-import navigateToExpandedContact from "../../../../functions/contacts/navigateToExpandedContact";
-import {
- createFormattedDate,
- formatMessage,
-} from "../../../../functions/contacts/formats";
-
-export function ContactElement(props) {
- const { contactsPrivateKey, publicKey } = useKeysContext();
- const { isConnectedToTheInternet } = useAppStatus();
- // const { theme, darkModeType } = useGlobalThemeContext();
- // const { backgroundOffset } = GetThemeColors();
- const {
- decodedAddedContacts,
- globalContactsInformation,
- toggleGlobalContactsInformation,
- contactsMessags,
- } = useGlobalContacts();
-
- const contact = props.contact;
- const navigate = useNavigate();
- const hasUnlookedTransaction =
- contactsMessags[contact.uuid]?.messages.length &&
- contactsMessags[contact.uuid]?.messages.filter(
- (savedMessage) => !savedMessage.message.wasSeen
- ).length > 0;
-
- return (
- {
- // if (!contact.isAdded) return;
- // if (!isConnectedToTheInternet) {
- // navigate(".ErrorScreen", {
- // errorMessage:
- // "Please reconnect to the internet to use this feature",
- // });
- // return;
- // }
- // navigate("ContactsPageLongPressActions", {
- // contact: contact,
- // });
- }}
- key={contact.uuid}
- onPress={() =>
- navigateToExpandedContact(
- contact,
- decodedAddedContacts,
- globalContactsInformation,
- toggleGlobalContactsInformation,
- contactsPrivateKey,
- publicKey,
- navigate
- )
- }
- >
-
-
-
- {/* */}
-
-
-
-
- {hasUnlookedTransaction && (
-
- )}
-
-
- {/* */}
-
-
-
-
- {!contact.isAdded && (
-
- )}
-
-
-
-
-
- );
-}
diff --git a/src/pages/contacts/components/contactsList/contactsList.css b/src/pages/contacts/components/contactsList/contactsList.css
new file mode 100644
index 0000000..9a10d9b
--- /dev/null
+++ b/src/pages/contacts/components/contactsList/contactsList.css
@@ -0,0 +1,71 @@
+.contact-picker-container {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.scroll-container {
+ flex: 1;
+ overflow-y: auto;
+ padding-top: 10px;
+}
+
+.contacts-list {
+ width: 95%;
+ margin: 20px auto 0;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.contact-row {
+ display: flex;
+ align-items: center;
+ padding: 6px 0;
+ border-radius: 12px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.contact-image-container {
+ width: 45px;
+ height: 45px;
+ border-radius: 50%;
+ overflow: hidden;
+ margin-right: 12px;
+ background-color: #f3f4f6;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.profile-image-wrapper {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.profile-image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.profile-image-placeholder {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #9ca3af;
+}
+
+.name-container {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
diff --git a/src/pages/contacts/components/contactsList/contactsList.jsx b/src/pages/contacts/components/contactsList/contactsList.jsx
new file mode 100644
index 0000000..546be04
--- /dev/null
+++ b/src/pages/contacts/components/contactsList/contactsList.jsx
@@ -0,0 +1,104 @@
+import { useState, useMemo, useCallback } from "react";
+import "./contactsList.css";
+import { useGlobalContacts } from "../../../../contexts/globalContacts";
+import { useImageCache } from "../../../../contexts/imageCacheContext";
+import ContactProfileImage from "../profileImage/profileImage";
+import { ArrowLeft } from "lucide-react";
+import CustomSettingsNavBar from "../../../../components/customSettingsNavbar";
+import { useTranslation } from "react-i18next";
+import { useThemeContext } from "../../../../contexts/themeContext";
+import { useAppStatus } from "../../../../contexts/appStatus";
+import CustomInput from "../../../../components/customInput/customInput";
+import ThemeText from "../../../../components/themeText/themeText";
+import { useNavigate } from "react-router-dom";
+
+const formatDisplayName = (contact) => {
+ return contact.name || contact.uniqueName || "";
+};
+
+export default function ChooseContactListPage() {
+ const navigate = useNavigate();
+ const [inputText, setInputText] = useState("");
+ const { t } = useTranslation();
+ const { theme, darkModeType } = useThemeContext();
+ const { isConnectedToTheInternet } = useAppStatus();
+ const { decodedAddedContacts } = useGlobalContacts();
+ const { cache } = useImageCache();
+
+ const navigateToExpandedContact = useCallback((contact, imageData) => {
+ console.log("Navigate to contact:", contact, imageData);
+ navigate("/sendAndRequestPage", {
+ replace: true,
+ state: {
+ selectedContact: contact,
+ paymentType: "send",
+ imageData,
+ },
+ });
+ }, []);
+
+ const sortedContacts = useMemo(() => {
+ return [...decodedAddedContacts].sort((contactA, contactB) => {
+ const nameA = contactA?.name || contactA?.uniqueName || "";
+ const nameB = contactB?.name || contactB?.uniqueName || "";
+ return nameA.localeCompare(nameB);
+ });
+ }, [decodedAddedContacts]);
+
+ const filteredContacts = useMemo(() => {
+ return sortedContacts.filter((contact) => {
+ return (
+ contact.name.toLowerCase().startsWith(inputText.toLowerCase()) ||
+ (!contact.isLNURL &&
+ contact.uniqueName
+ ?.toLowerCase()
+ ?.startsWith(inputText.toLowerCase()))
+ );
+ });
+ }, [sortedContacts, inputText]);
+
+ return (
+
+
+
+
+
+
+ {filteredContacts.map((contact) => (
+
+ navigateToExpandedContact(contact, cache[contact.uuid])
+ }
+ >
+
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/pages/contacts/components/expandedAddContactPage/expandedAddContactPage.jsx b/src/pages/contacts/components/expandedAddContactPage/expandedAddContactPage.jsx
new file mode 100644
index 0000000..6da95dd
--- /dev/null
+++ b/src/pages/contacts/components/expandedAddContactPage/expandedAddContactPage.jsx
@@ -0,0 +1,179 @@
+import React, { memo, useEffect, useMemo, useState } from "react";
+import "./style.css";
+import { ArrowLeft, Settings, Star } from "lucide-react";
+import { Colors } from "../../../../constants/theme";
+import { useThemeContext } from "../../../../contexts/themeContext";
+import useThemeColors from "../../../../hooks/useThemeColors";
+import { useImageCache } from "../../../../contexts/imageCacheContext";
+import { useGlobalContacts } from "../../../../contexts/globalContacts";
+import { useExpandedNavbar } from "../../utils/useExpandedNavbar";
+import ContactProfileImage from "../profileImage/profileImage";
+import { useLocation, useNavigate } from "react-router-dom";
+import AddContactPage from "../addContactPage/addContactPage";
+import ExpandedContactsPage from "../ExpandedContactsPage/ExpandedContactsPage";
+
+// Memoized shared header component
+const SharedHeader = memo(
+ ({
+ selectedContact,
+ imageData,
+ theme,
+ darkModeType,
+ backgroundOffset,
+ isContactAdded,
+ isEditingMyProfile,
+ navigate,
+ }) => {
+ return (
+
+ );
+ }
+);
+
+// Memoized navbar
+const MemoizedNavBar = memo(
+ ({
+ onBack,
+ theme,
+ darkModeType,
+ selectedContact,
+ backgroundColor,
+ isContactAdded,
+ handleFavortie,
+ handleSettings,
+ }) => {
+ return (
+
+
+ {selectedContact && isContactAdded && (
+
handleFavortie({ selectedContact })}
+ style={{ marginRight: "10px" }}
+ fill={
+ selectedContact?.isFavorite
+ ? theme && darkModeType
+ ? Colors.dark.text
+ : Colors.constants.blue
+ : "transparent"
+ }
+ color={
+ theme && darkModeType ? Colors.dark.text : Colors.constants.blue
+ }
+ size={30}
+ />
+ )}
+ {selectedContact && isContactAdded && (
+ handleSettings({ selectedContact })}
+ color={
+ theme && darkModeType ? Colors.dark.text : Colors.constants.blue
+ }
+ size={30}
+ />
+ )}
+
+ );
+ }
+);
+
+export default function ExpandedAddContactsPage({ route }) {
+ const { decodedAddedContacts, globalContactsInformation, contactsMessags } =
+ useGlobalContacts();
+ const { theme, darkModeType } = useThemeContext();
+ const { backgroundOffset, backgroundColor } = useThemeColors();
+ const { cache, refreshCacheObject } = useImageCache();
+ const { handleFavortie, handleSettings } = useExpandedNavbar();
+
+ const navigate = useNavigate();
+ const location = useLocation();
+ const props = location.state;
+ console.log(props, location);
+ const newContact = props;
+
+ useEffect(() => {
+ refreshCacheObject();
+ }, []);
+
+ // Memoize contact lookup
+ const selectedContact = useMemo(() => {
+ return decodedAddedContacts.find(
+ (contact) =>
+ (contact.uuid === newContact?.uuid && contact.isAdded) ||
+ (contact.isLNURL &&
+ contact.receiveAddress.toLowerCase() ===
+ newContact.receiveAddress?.toLowerCase())
+ );
+ }, [decodedAddedContacts, newContact]);
+
+ const isSelf = useMemo(() => {
+ return (
+ newContact.uniqueName?.toLowerCase() ===
+ globalContactsInformation?.myProfile?.uniqueName?.toLowerCase()
+ );
+ }, [newContact.uniqueName, globalContactsInformation?.myProfile?.uniqueName]);
+
+ const isContactAdded = !!selectedContact;
+ const imageData = cache[newContact?.uuid];
+ const contactTransactions = contactsMessags[newContact?.uuid]?.messages || [];
+
+ // Memoize back handler
+ const handleBack = useMemo(() => {
+ return () => navigate(-1);
+ }, [navigate]);
+
+ return (
+
+
+
+
+
+
+ {isContactAdded ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/src/pages/contacts/components/expandedAddContactPage/style.css b/src/pages/contacts/components/expandedAddContactPage/style.css
new file mode 100644
index 0000000..b4638ff
--- /dev/null
+++ b/src/pages/contacts/components/expandedAddContactPage/style.css
@@ -0,0 +1,102 @@
+.global-theme-view {
+ width: 100%;
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+}
+
+.top-bar {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ margin-bottom: 15px;
+}
+
+.back-button-container {
+ margin-right: auto;
+ cursor: pointer;
+ background-color: unset;
+}
+
+.star-container {
+ margin-right: 5px;
+ background: none;
+ border: none;
+ color: #ffd700;
+ font-size: 24px;
+ cursor: pointer;
+ padding: 4px 8px;
+ transition: transform 0.2s;
+}
+
+.star-container:hover {
+ transform: scale(1.2);
+}
+
+.settings-button {
+ background: none;
+ border: none;
+ font-size: 24px;
+ cursor: pointer;
+ padding: 4px 8px;
+ transition: transform 0.2s;
+}
+
+.settings-button:hover {
+ transform: rotate(45deg);
+}
+
+.profile-image-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+}
+
+.profile-image {
+ width: 150px;
+ height: 150px;
+ border-radius: 75px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ margin-bottom: 10px;
+}
+
+.profile-image-placeholder {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: #333;
+}
+
+.scroll-view {
+ flex-grow: 1;
+ overflow-y: auto;
+ max-height: calc(100vh - 100px);
+ display: flex;
+ flex-direction: column;
+}
+
+.scroll-view::-webkit-scrollbar {
+ width: 8px;
+}
+
+.scroll-view::-webkit-scrollbar-track {
+ background: #2a2a2a;
+ border-radius: 4px;
+}
+
+.scroll-view::-webkit-scrollbar-thumb {
+ background: #555;
+ border-radius: 4px;
+}
+
+.scroll-view::-webkit-scrollbar-thumb:hover {
+ background: #777;
+}
diff --git a/src/pages/contacts/components/pinnedContact/pinnedContact.jsx b/src/pages/contacts/components/pinnedContact/pinnedContact.jsx
deleted file mode 100644
index 0cf2770..0000000
--- a/src/pages/contacts/components/pinnedContact/pinnedContact.jsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import { useNavigate } from "react-router-dom";
-import { useGlobalContacts } from "../../../../contexts/globalContacts";
-import { useKeysContext } from "../../../../contexts/keysContext";
-import ThemeText from "../../../../components/themeText/themeText";
-import navigateToExpandedContact from "../../../../functions/contacts/navigateToExpandedContact";
-
-export default function PinnedContactElement(props) {
- const { contactsPrivateKey, publicKey } = useKeysContext();
- const {
- decodedAddedContacts,
- globalContactsInformation,
- toggleGlobalContactsInformation,
- contactsMessags,
- } = useGlobalContacts();
- // const { backgroundOffset } = GetThemeColors();
- const contact = props.contact;
- const navigate = useNavigate();
- const hasUnlookedTransaction =
- contactsMessags[contact.uuid]?.messages.length &&
- contactsMessags[contact.uuid]?.messages.filter(
- (savedMessage) => !savedMessage.message.wasSeen
- ).length > 0;
- return (
- {
- // if (!contact.isAdded) return;
- // navigate("/ContactsPageLongPressActions", {
- // contact: contact,
- // });
- // }}
- key={contact.uuid}
- onPress={() =>
- navigateToExpandedContact(
- contact,
- decodedAddedContacts,
- globalContactsInformation,
- toggleGlobalContactsInformation,
- contactsPrivateKey,
- publicKey,
- navigate
- )
- }
- >
-
-
- {/* */}
-
-
-
- {hasUnlookedTransaction &&
}
-
-
-
-
- );
-}
diff --git a/src/pages/contacts/components/profileImage/profileImage.jsx b/src/pages/contacts/components/profileImage/profileImage.jsx
index 34e4fec..18ae156 100644
--- a/src/pages/contacts/components/profileImage/profileImage.jsx
+++ b/src/pages/contacts/components/profileImage/profileImage.jsx
@@ -1,6 +1,5 @@
import React, { useState } from "react";
-import customUUID from "../../../../functions/customUUID";
-import { userIcon, userWhite } from "../../../../constants/icons";
+import { userIcon } from "../../../../constants/icons";
export default function ContactProfileImage({
resizeMode = "cover",
@@ -12,10 +11,8 @@ export default function ContactProfileImage({
const [loadError, setLoadError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
- const fallbackIcon = darkModeType && theme ? userWhite : userIcon;
- const customURI = uri
- ? `${uri}?v=${updated ? new Date(updated).getTime() : customUUID()}`
- : null;
+ const fallbackIcon = userIcon;
+ const customURI = uri;
const source = !loadError && uri && !isLoading ? customURI : fallbackIcon;
const isProfile = !loadError && uri && !isLoading;
diff --git a/src/pages/contacts/components/sendAndRequestPage/sendAndRequestPage.css b/src/pages/contacts/components/sendAndRequestPage/sendAndRequestPage.css
new file mode 100644
index 0000000..4b44c7a
--- /dev/null
+++ b/src/pages/contacts/components/sendAndRequestPage/sendAndRequestPage.css
@@ -0,0 +1,147 @@
+/* Send and Request Page */
+.send-request-page {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.replacement-container {
+ flex-grow: 1;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+.scroll-view-container {
+ width: 100%;
+ padding-bottom: 20px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ flex-grow: 1;
+ overflow-y: auto;
+}
+
+.scroll-view-container::-webkit-scrollbar {
+ display: none;
+}
+
+.scroll-view-container {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+
+/* Input and Gift Container */
+.input-and-gift-container {
+ width: 90%;
+ max-width: 350px;
+ align-self: center;
+}
+
+/* Gift Amount Container */
+.gift-amount-container {
+ width: 90%;
+ max-width: 600px;
+ margin-top: 20px;
+}
+
+/* Gift Container Button */
+.gift-container {
+ padding: 10px 16px;
+ border-radius: 20px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ background: none;
+ border: none;
+ cursor: pointer;
+ margin: 20px 0 0 auto;
+}
+
+/* Pill Button for Gift Card */
+.pill {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ padding: 16px 20px;
+ border-radius: 24px;
+ gap: 16px;
+ border-width: 2px;
+ border-style: solid;
+ align-self: center;
+ position: relative;
+ background: none;
+ cursor: pointer;
+ transition: opacity 0.2s ease;
+}
+
+.pill:hover {
+ opacity: 0.9;
+}
+
+.logo-container {
+ width: 48px;
+ aspect-ratio: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: var(--dark-mode-text, #ffffff);
+ border-radius: 12px;
+ padding: 6px;
+}
+
+.card-logo {
+ width: 100%;
+ height: 100%;
+ max-width: 80px;
+ max-height: 80px;
+ border-radius: 8px;
+ object-fit: contain;
+}
+
+.edit-button {
+ width: 32px;
+ height: 32px;
+ border-radius: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ right: -8px;
+ top: -8px;
+ border-width: 3px;
+ border-style: solid;
+}
+
+/* Memo Section */
+.memo-section {
+ margin-top: 28px;
+}
+
+.memo-container {
+ padding: 18px;
+ border-radius: 16px;
+ border-width: 1px;
+ border-style: solid;
+ min-height: 60px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+/* Max and Accept Container */
+.max-and-accept-container {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ margin-top: 10px;
+}
+.send-request-page .sendRequetContainer {
+ max-width: 350px;
+}
diff --git a/src/pages/contacts/components/sendAndRequestPage/sendAndRequsetPage.jsx b/src/pages/contacts/components/sendAndRequestPage/sendAndRequsetPage.jsx
new file mode 100644
index 0000000..ec5a113
--- /dev/null
+++ b/src/pages/contacts/components/sendAndRequestPage/sendAndRequsetPage.jsx
@@ -0,0 +1,781 @@
+import { useNavigate, useLocation } from "react-router-dom";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { publishMessage } from "../../../../functions/messaging/publishMessage";
+
+import { useTranslation } from "react-i18next";
+import { Colors, HIDDEN_OPACITY } from "../../../../constants/theme";
+import {
+ CONTENT_KEYBOARD_OFFSET,
+ QUICK_PAY_STORAGE_KEY,
+ SATSPERBITCOIN,
+} from "../../../../constants";
+import { useGlobalContextProvider } from "../../../../contexts/masterInfoObject";
+import CustomNumberKeyboard from "../../../../components/customNumberKeyboard/customNumberKeyboard";
+import CustomButton from "../../../../components/customButton/customButton";
+import FormattedSatText from "../../../../components/formattedSatText/formattedSatText";
+import { useGlobalContacts } from "../../../../contexts/globalContacts";
+import customUUID from "../../../../functions/customUUID";
+import { useNodeContext } from "../../../../contexts/nodeContext";
+import { useAppStatus } from "../../../../contexts/appStatus";
+import { useKeysContext } from "../../../../contexts/keysContext";
+import convertTextInputValue from "../../../../functions/textInputConvertValue";
+import { useServerTimeOnly } from "../../../../contexts/serverTime";
+import useThemeColors from "../../../../hooks/useThemeColors";
+import { useThemeContext } from "../../../../contexts/themeContext";
+import { Edit, Gift } from "lucide-react";
+import fetchBackend from "../../../../../db/handleBackend";
+import { getDataFromCollection } from "../../../../../db";
+import loadNewFiatData from "../../../../functions/saveAndUpdateFiatData";
+import giftCardPurchaseAmountTracker from "../../../../functions/apps/giftCardPurchaseTracker";
+import { useSpark } from "../../../../contexts/sparkContext";
+import getReceiveAddressAndContactForContactsPayment from "../../utils/getReceiveAddressAndKindForPayment";
+import { useActiveCustodyAccount } from "../../../../contexts/activeAccount";
+import NavBarWithBalance from "../../../../components/navBarWithBalance/navbarWithBalance";
+import { sparkPaymenWrapper } from "../../../../functions/spark/payments";
+import { getBolt11InvoiceForContact } from "../../../../functions/contacts";
+import EmojiQuickBar from "../../../../components/emojiBar/emojiQuickBar";
+
+import "./sendAndRequestPage.css";
+import CustomInput from "../../../../components/customInput/customInput";
+import ThemeText from "../../../../components/themeText/themeText";
+import FormattedBalanceInput from "../../../../components/formattedBalanceInput/formattedBalanceInput";
+import { useOverlay } from "../../../../contexts/overlayContext";
+
+const MAX_SEND_OPTIONS = [
+ { label: "25%", value: "25" },
+ { label: "50%", value: "50" },
+ { label: "75%", value: "75" },
+ { label: "100%", value: "100" },
+];
+
+export default function SendAndRequestPage(props) {
+ const { openOverlay } = useOverlay();
+ const navigate = useNavigate();
+ const location = useLocation();
+ const { masterInfoObject } = useGlobalContextProvider();
+ const { sparkInformation } = useSpark();
+ const { contactsPrivateKey, publicKey } = useKeysContext();
+ const { isConnectedToTheInternet } = useAppStatus();
+ const { fiatStats } = useNodeContext();
+ const { globalContactsInformation } = useGlobalContacts();
+ const getServerTime = useServerTimeOnly();
+ const [amountValue, setAmountValue] = useState("");
+ const [isAmountFocused, setIsAmountFocused] = useState(true);
+ const [descriptionValue, setDescriptionValue] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const [inputDenomination, setInputDenomination] = useState(
+ masterInfoObject.userBalanceDenomination
+ );
+ const { currentWalletMnemoinc } = useActiveCustodyAccount();
+ const { theme, darkModeType } = useThemeContext();
+ const { backgroundOffset, textColor, backgroundColor } = useThemeColors();
+ const { t } = useTranslation();
+ const descriptionRef = useRef(null);
+ const [isGettingMax, setIsGettingMax] = useState(false);
+
+ const selectedContact =
+ location.state?.selectedContact || props.route?.params?.selectedContact;
+ const paymentType =
+ location.state?.paymentType || props.route?.params?.paymentType;
+ const fromPage = location.state?.fromPage || props.route?.params?.fromPage;
+ const imageData = location.state?.imageData || props.route?.params?.imageData;
+ const giftOption = location.state?.cardInfo || props.route?.params?.cardInfo;
+ const useAltLayout = false;
+
+ const isBTCdenominated =
+ inputDenomination == "hidden" || inputDenomination == "sats";
+
+ const convertedSendAmount = useMemo(
+ () =>
+ (isBTCdenominated
+ ? Math.round(amountValue)
+ : Math.round(
+ (SATSPERBITCOIN / fiatStats?.value) * (amountValue / 100)
+ )) || 0,
+ [amountValue, fiatStats, isBTCdenominated]
+ );
+
+ console.log(amountValue, convertedSendAmount, "testing");
+
+ const canSendPayment = useMemo(
+ () => convertedSendAmount,
+ [convertedSendAmount, paymentType]
+ );
+
+ const switchTextToConfirm = useMemo(() => {
+ return (
+ masterInfoObject[QUICK_PAY_STORAGE_KEY]?.isFastPayEnabled &&
+ convertedSendAmount <=
+ masterInfoObject[QUICK_PAY_STORAGE_KEY].fastPayThresholdSats
+ );
+ }, [convertedSendAmount]);
+
+ const handleSelctProcesss = useCallback(
+ async (item) => {
+ try {
+ const balance = sparkInformation.balance;
+ const selectedPercent = !item ? 100 : Number(item.value);
+
+ const sendingBalance = Math.floor(balance * (selectedPercent / 100));
+
+ setIsGettingMax(true);
+ await new Promise((res) => setTimeout(res, 250));
+
+ let maxAmountSats = 0;
+
+ if (selectedContact.isLNURL) {
+ const [username, domain] = selectedContact.receiveAddress.split("@");
+ const lnurlResposne = await getBolt11InvoiceForContact(
+ username,
+ sendingBalance,
+ undefined,
+ false,
+ domain
+ );
+ if (!lnurlResposne) throw new Error("Unable to get invoice");
+ const invoice = lnurlResposne;
+ const fee = await sparkPaymenWrapper({
+ getFee: true,
+ address: invoice,
+ masterInfoObject,
+ paymentType: "lightning",
+ mnemonic: currentWalletMnemoinc,
+ });
+
+ if (!fee.didWork) throw new Error(fee.error);
+
+ maxAmountSats = Math.max(
+ Number(sendingBalance) - fee.fee + fee.supportFee,
+ 0
+ );
+ } else {
+ const feeResponse = await sparkPaymenWrapper({
+ getFee: true,
+ address: sparkInformation.sparkAddress,
+ masterInfoObject,
+ paymentType: "spark",
+ amountSats: sendingBalance,
+ mnemonic: currentWalletMnemoinc,
+ });
+ if (!feeResponse.didWork) throw new Error("Unable to get invoice");
+ maxAmountSats = Math.max(
+ Number(sendingBalance) - feeResponse.fee + feeResponse.supportFee,
+ 0
+ );
+ }
+
+ const convertedMax =
+ inputDenomination != "fiat"
+ ? Math.floor(Number(maxAmountSats))
+ : (
+ Number(maxAmountSats) /
+ Math.floor(SATSPERBITCOIN / fiatStats?.value)
+ ).toFixed(2);
+
+ setAmountValue(convertedMax);
+ } catch (err) {
+ openOverlay({
+ for: "error",
+ errorMessage: t("errormessages.genericError"),
+ });
+ } finally {
+ setIsGettingMax(false);
+ }
+ },
+ [
+ sparkInformation,
+ inputDenomination,
+ currentWalletMnemoinc,
+ selectedContact,
+ ]
+ );
+
+ useEffect(() => {
+ if (!giftOption) {
+ setAmountValue("");
+ return;
+ }
+ const totalSats = Math.round(
+ giftOption.selectedDenomination * giftOption.satsPerDollar
+ );
+ const localfiatSatsPerDollar = fiatStats.value / SATSPERBITCOIN;
+ setAmountValue(
+ String(
+ isBTCdenominated
+ ? totalSats
+ : Math.round(localfiatSatsPerDollar * totalSats)
+ )
+ );
+ }, [giftOption]);
+
+ const handleSearch = useCallback((term) => {
+ setAmountValue(term);
+ }, []);
+
+ const handleSubmit = useCallback(async () => {
+ if (!isConnectedToTheInternet) {
+ openOverlay({
+ for: "error",
+ errorMessage: t("errormessages.nointernet"),
+ });
+
+ return;
+ }
+ try {
+ if (!convertedSendAmount) return;
+ if (!canSendPayment) return;
+
+ setIsLoading(true);
+
+ const sendingAmountMsat = convertedSendAmount * 1000;
+ const contactMessage = descriptionValue;
+ const myProfileMessage = descriptionValue
+ ? descriptionValue
+ : t("contacts.sendAndRequestPage.profileMessage", {
+ name: selectedContact.name || selectedContact.uniqueName,
+ });
+ const payingContactMessage = descriptionValue
+ ? descriptionValue
+ : {
+ usingTranslation: true,
+ type: "paid",
+ name:
+ globalContactsInformation.myProfile.name ||
+ globalContactsInformation.myProfile.uniqueName,
+ };
+
+ const currentTime = getServerTime();
+ const UUID = customUUID();
+ let sendObject = {};
+
+ if (globalContactsInformation.myProfile.uniqueName) {
+ sendObject["senderProfileSnapshot"] = {
+ uniqueName: globalContactsInformation.myProfile.uniqueName,
+ };
+ }
+
+ if (giftOption) {
+ const retrivedContact = await getDataFromCollection(
+ "blitzWalletUsers",
+ selectedContact.uuid
+ );
+ if (!retrivedContact) {
+ openOverlay({
+ for: "error",
+ errorMessage: t("errormessages.fullDeeplinkError"),
+ });
+ return;
+ }
+ if (!retrivedContact.enabledGiftCards) {
+ openOverlay({
+ for: "error",
+ errorMessage: t(
+ "contacts.sendAndRequestPage.giftCardappVersionError"
+ ),
+ });
+ return;
+ }
+
+ const postData = {
+ type: "buyGiftCard",
+ productId: giftOption.id,
+ cardValue: giftOption.selectedDenomination,
+ quantity: Number(1),
+ };
+
+ const response = await fetchBackend(
+ "theBitcoinCompanyV3",
+ postData,
+ contactsPrivateKey,
+ publicKey
+ );
+
+ if (response.result) {
+ const { amount, invoice, orderId, uuid } = response.result;
+ const fiatRates = await (fiatStats.coin?.toLowerCase() === "usd"
+ ? Promise.resolve({ didWork: true, fiatRateResponse: fiatStats })
+ : loadNewFiatData(
+ "usd",
+ contactsPrivateKey,
+ publicKey,
+ masterInfoObject
+ ));
+ const USDBTCValue = fiatRates.didWork
+ ? fiatRates.fiatRateResponse
+ : { coin: "USD", value: 100_000 };
+
+ const sendingAmountSat = amount;
+ const isOverDailyLimit = await giftCardPurchaseAmountTracker({
+ sendingAmountSat: sendingAmountSat,
+ USDBTCValue: USDBTCValue,
+ testOnly: true,
+ });
+
+ if (isOverDailyLimit.shouldBlock) {
+ openOverlay({
+ for: "error",
+ errorMessage: isOverDailyLimit.reason,
+ });
+ return;
+ }
+
+ sendObject["amountMsat"] = amount;
+ sendObject["description"] = giftOption.memo || "";
+ sendObject["uuid"] = UUID;
+ sendObject["isRequest"] = false;
+ sendObject["isRedeemed"] = null;
+ sendObject["wasSeen"] = null;
+ sendObject["didSend"] = null;
+ sendObject["giftCardInfo"] = {
+ amount,
+ invoice,
+ orderId,
+ uuid,
+ logo: giftOption.logo,
+ name: giftOption.name,
+ };
+
+ navigate("/confirm-payment", {
+ state: {
+ btcAddress: invoice,
+ comingFromAccept: true,
+ enteredPaymentInfo: {
+ amount: amount,
+ description:
+ descriptionValue ||
+ t("contacts.sendAndRequestPage.giftCardDescription", {
+ name: selectedContact.name || selectedContact.uniqueName,
+ giftCardName: giftOption.name,
+ }),
+ },
+ contactInfo: {
+ imageData,
+ name: selectedContact.name || selectedContact.uniqueName,
+ uniqueName: selectedContact.uniqueName,
+ uuid: selectedContact.uuid,
+ },
+ fromPage: "contacts",
+ publishMessageFunc: () => {
+ giftCardPurchaseAmountTracker({
+ sendingAmountSat: sendingAmountSat,
+ USDBTCValue: USDBTCValue,
+ });
+ publishMessage({
+ toPubKey: selectedContact.uuid,
+ fromPubKey: globalContactsInformation.myProfile.uuid,
+ data: sendObject,
+ globalContactsInformation,
+ selectedContact,
+ isLNURLPayment: false,
+ privateKey: contactsPrivateKey,
+ retrivedContact,
+ currentTime,
+ masterInfoObject,
+ });
+ },
+ },
+ });
+ } else {
+ openOverlay({
+ for: "error",
+ errorMessage: t("contacts.sendAndRequestPage.cardDetailsError"),
+ });
+ }
+ return;
+ }
+
+ const {
+ receiveAddress,
+ retrivedContact,
+ didWork,
+ error,
+ formattedPayingContactMessage,
+ } = await getReceiveAddressAndContactForContactsPayment({
+ sendingAmountSat: convertedSendAmount,
+ selectedContact,
+ myProfileMessage,
+ payingContactMessage,
+ onlyGetContact: paymentType !== "send",
+ });
+
+ if (!didWork) {
+ openOverlay({
+ for: "error",
+ errorMessage: error,
+ });
+ return;
+ }
+
+ if (paymentType === "send") {
+ sendObject["amountMsat"] = sendingAmountMsat;
+ sendObject["description"] = contactMessage;
+ sendObject["uuid"] = UUID;
+ sendObject["isRequest"] = false;
+ sendObject["isRedeemed"] = null;
+ sendObject["wasSeen"] = null;
+ sendObject["didSend"] = null;
+
+ navigate("/send", {
+ state: {
+ btcAddress: receiveAddress,
+ comingFromAccept: true,
+ enteredPaymentInfo: {
+ amount: sendingAmountMsat / 1000,
+ description: myProfileMessage,
+ },
+ contactInfo: {
+ imageData,
+ name: selectedContact.name || selectedContact.uniqueName,
+ isLNURLPayment: selectedContact?.isLNURL,
+ payingContactMessage: formattedPayingContactMessage,
+ uniqueName: retrivedContact?.contacts?.myProfile?.uniqueName,
+ uuid: selectedContact.uuid,
+ },
+ fromPage: "contacts",
+ publishMessageFuncParams: {
+ toPubKey: selectedContact.uuid,
+ fromPubKey: globalContactsInformation.myProfile.uuid,
+ data: {
+ ...sendObject,
+ name:
+ globalContactsInformation.myProfile.name ||
+ globalContactsInformation.myProfile.uniqueName,
+ },
+ globalContactsInformation,
+ selectedContact,
+ isLNURLPayment: selectedContact?.isLNURL,
+ privateKey: contactsPrivateKey,
+ retrivedContact,
+ currentTime,
+ masterInfoObject,
+ },
+ },
+ });
+ } else {
+ sendObject["amountMsat"] = sendingAmountMsat;
+ sendObject["description"] = descriptionValue;
+ sendObject["uuid"] = UUID;
+ sendObject["isRequest"] = true;
+ sendObject["isRedeemed"] = null;
+ sendObject["wasSeen"] = null;
+ sendObject["didSend"] = null;
+
+ await publishMessage({
+ toPubKey: selectedContact.uuid,
+ fromPubKey: globalContactsInformation.myProfile.uuid,
+ data: sendObject,
+ globalContactsInformation,
+ selectedContact,
+ isLNURLPayment: selectedContact?.isLNURL,
+ privateKey: contactsPrivateKey,
+ retrivedContact,
+ currentTime,
+ masterInfoObject,
+ });
+
+ navigate(-1);
+ }
+ } catch (err) {
+ console.log(err, "publishing message error");
+ openOverlay({
+ for: "error",
+ errorMessage: selectedContact.isLNURL
+ ? t("errormessages.contactInvoiceGenerationError")
+ : t("errormessages.invoiceRetrivalError"),
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ }, [
+ isConnectedToTheInternet,
+ convertedSendAmount,
+ canSendPayment,
+ selectedContact,
+ navigate,
+ contactsPrivateKey,
+ descriptionValue,
+ paymentType,
+ globalContactsInformation,
+ getServerTime,
+ giftOption,
+ masterInfoObject,
+ fiatStats,
+ imageData,
+ ]);
+
+ const memorizedContainerStyles = useMemo(() => {
+ return {
+ flex: 0,
+ borderRadius: 8,
+ height: "unset",
+ minWidth: "unset",
+ justifyContent: "center",
+ };
+ }, []);
+
+ const handleEmoji = (newDescription) => {
+ setDescriptionValue(newDescription);
+ };
+
+ return (
+
+
+
+
+
{
+ if (!isAmountFocused) return;
+ setInputDenomination((prev) => {
+ const newPrev = prev === "sats" ? "fiat" : "sats";
+ return newPrev;
+ });
+ setAmountValue(
+ convertTextInputValue(amountValue, fiatStats, inputDenomination)
+ );
+ }}
+ />
+
+
+
+ {paymentType === "send" && !giftOption && !useAltLayout && (
+
+ {/* */}
+
+ )}
+
+ {giftOption && (
+
+
+ {giftOption.memo && (
+
+ )}
+
+ )}
+
+
+ {!giftOption && (
+ <>
+
+ {/* {paymentType === "send" &&
+ !giftOption &&
+ !selectedContact?.isLNURL && (
+
+ )} */}
+
{
+ setIsAmountFocused(false);
+ }}
+ onBlurFunction={() => {
+ setIsAmountFocused(true);
+ }}
+ textInputRef={descriptionRef}
+ placeholder={t(
+ "contacts.sendAndRequestPage.descriptionPlaceholder"
+ )}
+ customInputStyles={{
+ borderRadius: useAltLayout ? 15 : 8,
+ height: useAltLayout ? 50 : "unset",
+ }}
+ editable={paymentType === "send" ? true : !!convertedSendAmount}
+ containerStyles={{ maxWidth: 350, marginTop: 8 }}
+ onchange={setDescriptionValue}
+ inputText={descriptionValue}
+ textInputMultiline={true}
+ textAlignVertical={"center"}
+ maxLength={149}
+ />
+
+ {useAltLayout && (
+
+ )}
+
+ {isAmountFocused && (
+
+ )}
+ >
+ )}
+ {((isAmountFocused && !useAltLayout) || giftOption) && (
+
+ )}
+
+ {!isAmountFocused && (
+
+ )}
+
+ );
+}
diff --git a/src/pages/contacts/contacts.css b/src/pages/contacts/contacts.css
index 41f0d50..c9d6968 100644
--- a/src/pages/contacts/contacts.css
+++ b/src/pages/contacts/contacts.css
@@ -6,8 +6,24 @@
}
/* navbar */
#contactsPage .contactsPageTopBar {
+ min-height: 40px;
display: flex;
justify-content: end;
+ align-items: center;
+ position: relative;
+}
+#contactsPage .contactsPageTopBar .pageHeaderText {
+ width: 100%;
+ position: absolute;
+ font-size: 1.25rem;
+ text-align: center;
+ white-space: nowrap; /* Prevent line break */
+ overflow: hidden; /* Hide overflow */
+ text-overflow: ellipsis; /* Add "..." at end */
+ max-width: 100%; /* Optional: constrain width */
+ flex: 1; /* Fill available space if needed */
+ padding: 0 35px;
+ margin: 0;
}
#contactsPage .contactsPageTopBar .myContactContainer {
width: 35px;
@@ -28,3 +44,160 @@
justify-content: center;
padding: 20px 0;
}
+
+#contactsPage .not-found-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-top: 25px;
+}
+#contactsPage .not-found-username {
+ font-weight: 500;
+ margin: 0;
+ font-size: 1.25em;
+ line-height: 1;
+ text-align: center;
+}
+#contactsPage .not-found-label {
+ opacity: 0.6;
+ margin: 0;
+ font-size: 0.8em;
+ line-height: 1;
+ margin-top: 10px;
+ margin-bottom: 20px;
+ text-align: center;
+}
+
+/* row items */
+#contactsPage .pinned-contact {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 10px 0;
+ cursor: pointer;
+ max-width: 80px;
+}
+#contactsPage .pinnedContactScrollview {
+ display: flex;
+ flex-direction: row;
+ column-gap: 15px;
+ overflow-x: scroll;
+}
+#contactsPage .pinned-contact-image-wrapper {
+ width: 100%;
+ margin-bottom: 5px;
+}
+
+#contactsPage .pinned-contact-image-container {
+ width: 100%;
+
+ aspect-ratio: 1;
+ border-radius: 9999px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+
+#contactsPage .pinned-contact-footer {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ overflow: hidden;
+}
+
+#contactsPage .pinned-contact-name {
+ width: 100%;
+ font-size: 12px;
+ margin: 5px 0 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-align: center;
+}
+
+#contactsPage .contact-row {
+ width: 95%;
+ display: flex;
+ align-items: center;
+ padding: 15px 0;
+ cursor: pointer;
+ margin: 0 auto;
+}
+
+#contactsPage .contact-image-container {
+ width: 35px;
+ height: 35px;
+ border-radius: 30px;
+ margin-right: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+
+#contactsPage .contact-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+#contactsPage .contact-row-inline {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ margin-bottom: 5px;
+}
+
+#contactsPage .contact-name-block {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ margin-right: 5px;
+}
+
+#contactsPage .unknown-sender {
+ font-size: 12px;
+ opacity: 0.8;
+ margin: 0;
+}
+
+#contactsPage .contact-date {
+ font-size: 12px;
+ margin: 0;
+ margin-right: 5px;
+}
+
+#contactsPage .contact-arrow {
+ width: 20px;
+ height: 20px;
+ transform: rotate(180deg);
+}
+
+#contactsPage .contact-preview {
+ font-size: 12px;
+ opacity: 0.85;
+ margin: 0;
+}
+
+#contactsPage .notification-dot {
+ width: 10px;
+ height: 10px;
+ margin: 0;
+ border-radius: 50%;
+ margin-right: 5px;
+ position: relative;
+}
+
+#contactsPage .add-contact-icon {
+ width: 25px;
+ height: 25px;
+ transform: rotate(45deg);
+}
+
+#contactsPage .add-contact-text {
+ font-weight: 500;
+ margin: 0;
+}
diff --git a/src/pages/contacts/contacts.jsx b/src/pages/contacts/contacts.jsx
index 6bb1dcf..30576ab 100644
--- a/src/pages/contacts/contacts.jsx
+++ b/src/pages/contacts/contacts.jsx
@@ -1,11 +1,9 @@
-import { useMemo, useState } from "react";
-import PinnedContactElement from "./components/pinnedContact/pinnedContact";
+import { memo, useCallback, useMemo, useState } from "react";
import { useGlobalContacts } from "../../contexts/globalContacts";
import { useImageCache } from "../../contexts/imageCacheContext";
import { useGlobalContextProvider } from "../../contexts/masterInfoObject";
import { useAppStatus } from "../../contexts/appStatus";
import { useLocation, useNavigate } from "react-router-dom";
-import { ContactElement } from "./components/contactElement/contactElement";
import CustomInput from "../../components/customInput/customInput";
import ThemeText from "../../components/themeText/themeText";
import CustomButton from "../../components/customButton/customButton";
@@ -17,195 +15,578 @@ import { useThemeContext } from "../../contexts/themeContext";
import useThemeColors from "../../hooks/useThemeColors";
import { questionMarkSVG } from "../../constants/icons";
import ThemeImage from "../../components/ThemeImage/themeImage";
+import NavBarProfileImage from "../../components/navBar/profileImage";
+import { useKeysContext } from "../../contexts/keysContext";
+import { useServerTime, useServerTimeOnly } from "../../contexts/serverTime";
+import { useTranslation } from "react-i18next";
+import { useFilteredContacts, useProcessedContacts } from "./utils/hooks";
+import { encryptMessage } from "../../functions/encodingAndDecoding";
+import { ChevronRight, PlusIcon } from "lucide-react";
+import { createFormattedDate, formatMessage } from "./utils/utilityFunctions";
+import { formatDisplayName } from "./utils/formatListDisplayName";
+import { useOverlay } from "../../contexts/overlayContext";
-export default function Contacts({ openOverlay }) {
+export default function Contacts() {
+ const { openOverlay } = useOverlay();
+ const { contactsPrivateKey, publicKey } = useKeysContext();
const { masterInfoObject } = useGlobalContextProvider();
- const { decodedAddedContacts, globalContactsInformation, contactsMessags } =
- useGlobalContacts();
+ const { cache } = useImageCache();
const { theme, darkModeType } = useThemeContext();
- const { backgroundOffset } = useThemeColors();
+ const {
+ decodedAddedContacts,
+ globalContactsInformation,
+ contactsMessags,
+ toggleGlobalContactsInformation,
+ giftCardsList,
+ } = useGlobalContacts();
+ const { serverTimeOffset } = useServerTime();
+ const getServerTime = useServerTimeOnly();
+ const { backgroundOffset, backgroundColor, textColor } = useThemeColors();
const { isConnectedToTheInternet } = useAppStatus();
- const { cache } = useImageCache();
+ const { t } = useTranslation();
+ const [inputText, setInputText] = useState("");
const hideUnknownContacts = masterInfoObject.hideUnknownContacts;
- const myProfile = globalContactsInformation?.myProfile;
- const didEditProfile = globalContactsInformation?.myProfile?.didEditProfile;
const navigate = useNavigate();
- const location = useLocation();
- const [inputText, setInputText] = useState("");
+ const myProfile = globalContactsInformation.myProfile;
+ const didEditProfile = myProfile?.didEditProfile;
+
+ const contactInfoList = useProcessedContacts(
+ decodedAddedContacts,
+ contactsMessags
+ );
+
+ const filteredContacts =
+ useFilteredContacts(
+ contactInfoList,
+ inputText.trim(),
+ hideUnknownContacts
+ ) ?? [];
+ const profileContainerStyle = useMemo(
+ () => ({
+ backgroundColor: backgroundOffset,
+ }),
+ [backgroundOffset]
+ );
+
+ const searchInputStyle = useMemo(
+ () => ({
+ width: "100%",
+ paddingBottom: "10px",
+ backgroundColor,
+ }),
+ [backgroundColor]
+ );
+
+ const scrollContentStyle = useMemo(
+ () => ({
+ paddingTop: contactInfoList.some((c) => c.contact.isFavorite) ? 0 : 10,
+ }),
+ [contactInfoList]
+ );
- console.log("test", import.meta.env.MODE);
+ const showAddContactRowItem =
+ !contactInfoList?.length ||
+ filteredContacts?.length ||
+ (contactInfoList?.length &&
+ !filteredContacts?.length &&
+ !inputText?.trim()?.length);
+
+ const showHighlightedGifts = useMemo(() => {
+ return giftCardsList && !!giftCardsList?.length;
+ }, [giftCardsList]);
+
+ const navigateToExpandedContact = useCallback(
+ async (contact) => {
+ try {
+ if (!contact.isAdded) {
+ let newAddedContacts = [...decodedAddedContacts];
+ const indexOfContact = decodedAddedContacts.findIndex(
+ (obj) => obj.uuid === contact.uuid
+ );
+
+ let newContact = newAddedContacts[indexOfContact];
+ newContact["isAdded"] = true;
+
+ toggleGlobalContactsInformation(
+ {
+ myProfile: { ...globalContactsInformation.myProfile },
+ addedContacts: await encryptMessage(
+ contactsPrivateKey,
+ publicKey,
+ JSON.stringify(newAddedContacts)
+ ),
+ },
+ true
+ );
+ }
+ navigate("/expandedContactsPage", {
+ state: { uuid: contact.uuid },
+ });
+ } catch (err) {
+ console.log("error navigating to expanded contact", err);
+ navigate("/expandedContactsPage", {
+ state: { uuid: contact.uuid },
+ });
+ }
+ },
+ [
+ decodedAddedContacts,
+ globalContactsInformation,
+ toggleGlobalContactsInformation,
+ contactsPrivateKey,
+ publicKey,
+ navigate,
+ ]
+ );
const pinnedContacts = useMemo(() => {
- return decodedAddedContacts
- .filter((contact) => contact.isFavorite)
- .map((contact, id) => {
- return (
-
- );
- });
- }, [decodedAddedContacts, contactsMessags, cache]);
+ return contactInfoList
+ .filter((contact) => contact.contact.isFavorite)
+ .map((contact) => (
+
+ ));
+ }, [
+ contactInfoList,
+ cache,
+ darkModeType,
+ theme,
+ backgroundOffset,
+ navigateToExpandedContact,
+ // dimensions,
+ navigate,
+ openOverlay,
+ ]);
const contactElements = useMemo(() => {
- return decodedAddedContacts
- .filter((contact) => {
- return (
- (contact.name?.toLowerCase()?.startsWith(inputText.toLowerCase()) ||
- contact?.uniqueName
- ?.toLowerCase()
- ?.startsWith(inputText.toLowerCase())) &&
- !contact.isFavorite &&
- (!hideUnknownContacts || contact.isAdded)
- );
- })
- .sort((a, b) => {
- const earliset_A = contactsMessags[a.uuid]?.lastUpdated;
- const earliset_B = contactsMessags[b.uuid]?.lastUpdated;
- return (earliset_B || 0) - (earliset_A || 0);
- })
- .map((contact, id) => {
- return (
-
- );
- });
+ const currentTime = getServerTime();
+
+ let contacts = filteredContacts.map((item, index) => (
+
+ ));
+ if (showAddContactRowItem) {
+ contacts.unshift(
+
+ );
+ }
+ return contacts;
}, [
- decodedAddedContacts,
- inputText,
- hideUnknownContacts,
- contactsMessags,
+ filteredContacts,
cache,
+ darkModeType,
+ theme,
+ backgroundOffset,
+ navigateToExpandedContact,
+ isConnectedToTheInternet,
+ navigate,
+ getServerTime,
+ serverTimeOffset,
+ showAddContactRowItem,
+ t,
+ openOverlay,
]);
+ console.log(cache, "test");
+
+ const handleButtonPress = useCallback(() => {
+ if (!isConnectedToTheInternet) {
+ openOverlay({
+ for: "error",
+ errorMessage: "Not connected to the internet",
+ });
+ return;
+ }
+ if (didEditProfile) {
+ openOverlay({
+ for: "halfModal",
+ contentType: "addContactsHalfModal",
+ });
+ } else {
+ navigate("/edit-profile", {
+ state: { pageType: "myProfile", fromSettings: false },
+ });
+ }
+ }, [isConnectedToTheInternet, didEditProfile, navigate]);
+
+ const hasContacts =
+ decodedAddedContacts.filter(
+ (contact) => !hideUnknownContacts || contact.isAdded
+ ).length !== 0;
+
+ const stickyHeaderIndicesValue = useMemo(() => {
+ return [pinnedContacts.length ? 1 : 0];
+ }, [pinnedContacts]);
+
return (
- {myProfile?.didEditProfile && (
+ {(didEditProfile || hasContacts) && (
- {/*
{
- keyboardNavigate(() => {
- if (!isConnectedToTheInternet) {
- navigate.navigate("ErrorScreen", {
- errorMessage:
- "Please connect to the internet to use this feature",
- });
- return;
- }
- navigate.navigate("CustomHalfModal", {
- wantedContent: "addContacts",
- sliderHight: 0.4,
- });
- });
- }}
- >
-
-
*/}
-
{
- navigate("/my-profile");
- }}
- >
-
+
+
+
)}
- {/* {decodedAddedContacts.filter(
- (contact) => !hideUnknownContacts || contact.isAdded
- ).length !== 0 && myProfile.didEditProfile ? (
+ {hasContacts && didEditProfile ? (
{pinnedContacts.length != 0 && (
-
+
)}
- {contactElements}
+ {contactElements.length ? (
+ contactElements
+ ) : (
+
+
+
+
+ openOverlay({
+ for: "halfModal",
+ contentType: "addContactsHalfModal",
+ params: {
+ startingSearchValue: inputText.trim(),
+ },
+ })
+ }
+ textContent={t("constants.search")}
+ />
+
+ )}
- ) : ( */}
-
-
-
-
-
{
- if (!isConnectedToTheInternet) {
- openOverlay({
- for: "error",
- errorMessage:
- "Please connect to the internet to use this feature.",
- });
- return;
- }
- if (didEditProfile) {
- //navigate to add contacts popup
- openOverlay({
- for: "error",
- errorMessage: "Feature coming soon...",
- });
- } else {
- navigate("/edit-profile", {
- state: { pageType: "myProfile", fromSettings: false },
- });
+ ) : (
+
+
+
-
- {/* )} */}
+ />
+
+
+
+ )}
);
}
+
+export const PinnedContactElement = memo(
+ ({
+ contact,
+ hasUnlookedTransaction,
+ cache,
+ darkModeType,
+ theme,
+ backgroundOffset,
+ navigateToExpandedContact,
+ navigate,
+ }) => {
+ const [textWidth, setTextWidth] = useState(0);
+
+ const containerSize = useMemo(() => "calc(95% / 4 - 15px)", []);
+
+ const handleLongPress = useCallback(
+ (e) => {
+ e.preventDefault();
+ if (!contact.isAdded) return;
+ navigate("ContactsPageLongPressActions", { contact });
+ },
+ [contact, navigate]
+ );
+
+ const handlePress = useCallback(() => {
+ navigateToExpandedContact(contact);
+ }, [contact, navigateToExpandedContact]);
+
+ return (
+
+
+
+
+
+
+ {hasUnlookedTransaction && (
+
+ )}
+
+ el && setTextWidth(el.offsetWidth)}
+ textStyles={{
+ width: `calc(100% - ${hasUnlookedTransaction ? "25px" : "0px"})`,
+ }}
+ className="pinned-contact-name"
+ textContent={formatDisplayName(contact)}
+ />
+
+
+ );
+ }
+);
+
+export const ContactElement = memo(
+ ({
+ contact,
+ hasUnlookedTransaction,
+ lastUpdated,
+ firstMessage,
+ cache,
+ darkModeType,
+ theme,
+ backgroundOffset,
+ navigateToExpandedContact,
+ isConnectedToTheInternet,
+ navigate,
+ currentTime,
+ serverTimeOffset,
+ t,
+ isLastElement,
+ openOverlay,
+ }) => {
+ const handlePress = useCallback(() => {
+ navigateToExpandedContact(contact);
+ }, [contact, navigateToExpandedContact]);
+
+ const formattedDate = lastUpdated
+ ? createFormattedDate(
+ lastUpdated - serverTimeOffset,
+ currentTime - serverTimeOffset,
+ t
+ )
+ : "";
+
+ return (
+
+
+
+
+
+
+
+
+
+ {!contact.isAdded && (
+
+ )}
+
+
+ {hasUnlookedTransaction && (
+
+ )}
+
+
+
+
+
+ {!!formatMessage(firstMessage) && contact.isAdded && (
+
+
+
+ )}
+
+
+ );
+ }
+);
+export const AddContactRowItem = memo(
+ ({
+ darkModeType,
+ theme,
+ backgroundOffset,
+ t,
+ isConnectedToTheInternet,
+ navigate,
+ numberOfContacts,
+ openOverlay,
+ }) => {
+ const goToAddContact = useCallback(() => {
+ if (!isConnectedToTheInternet) {
+ navigate("ErrorScreen", {
+ errorMessage: t("errormessages.nointernet"),
+ });
+ } else {
+ openOverlay({
+ for: "halfModal",
+ contentType: "addContactsHalfModal",
+ });
+ }
+ }, [isConnectedToTheInternet, navigate, t]);
+
+ return (
+
+ );
+ }
+);
diff --git a/src/pages/contacts/internalComponents/contactTransactions/contactsTransactionItem.css b/src/pages/contacts/internalComponents/contactTransactions/contactsTransactionItem.css
new file mode 100644
index 0000000..2ea3905
--- /dev/null
+++ b/src/pages/contacts/internalComponents/contactTransactions/contactsTransactionItem.css
@@ -0,0 +1,89 @@
+/* Transaction Item Button */
+.transaction-item-button {
+ width: 100%;
+ background: none;
+ border: none;
+ padding: 0;
+ cursor: pointer;
+ text-align: left;
+}
+
+.transaction-item-button:active {
+ opacity: 1;
+}
+
+/* Transaction Container */
+.transaction-container {
+ width: 95%;
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ margin: 8px auto;
+}
+
+.transaction-container.centered {
+ align-items: center;
+}
+
+/* Icon Styles */
+.icon {
+ width: 30px;
+ height: 30px;
+ margin-right: 5px;
+}
+
+/* Transaction Content */
+.transaction-content {
+ width: 100%;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ margin-left: 10px;
+}
+.transaction-content > * {
+ margin: 0;
+}
+.transaction-content .time-label {
+ font-size: 0.8em;
+ font-weight: 300;
+ opacity: 0.6;
+}
+
+/* Accept or Pay Button Base Styles */
+.accept-or-pay-btn {
+ width: 100%;
+ overflow: hidden;
+ border-radius: 15px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 10px;
+ font-size: 16px;
+ cursor: pointer;
+ transition: opacity 0.2s ease;
+ border: none;
+}
+
+.accept-or-pay-btn:hover:not(:disabled) {
+ opacity: 0.9;
+}
+
+.accept-or-pay-btn:disabled {
+ cursor: not-allowed;
+ opacity: 0.6;
+}
+
+/* Text Ellipsis Support */
+.ellipsis-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.ellipsis-text-multiline {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+}
diff --git a/src/pages/contacts/internalComponents/contactTransactions/contactsTransactions.jsx b/src/pages/contacts/internalComponents/contactTransactions/contactsTransactions.jsx
new file mode 100644
index 0000000..402cfb5
--- /dev/null
+++ b/src/pages/contacts/internalComponents/contactTransactions/contactsTransactions.jsx
@@ -0,0 +1,482 @@
+import React, { useCallback, useState, useMemo } from "react";
+
+import { useNavigate } from "react-router-dom";
+
+// import { sendPushNotification } from "../../../../../functions/messaging/publishMessage";
+
+import { useTranslation } from "react-i18next";
+
+import { useGlobalContextProvider } from "../../../../contexts/masterInfoObject";
+import FormattedSatText from "../../../../components/formattedSatText/formattedSatText";
+import useThemeColors from "../../../../hooks/useThemeColors";
+import ThemeText from "../../../../components/themeText/themeText";
+import { getDataFromCollection, updateMessage } from "../../../../../db";
+import { useThemeContext } from "../../../../contexts/themeContext";
+import { useKeysContext } from "../../../../contexts/keysContext";
+import CustomButton from "../../../../components/customButton/customButton";
+import { useServerTimeOnly } from "../../../../contexts/serverTime";
+import GiftCardTxItem from "../giftCardTxItem/giftCardTxItem";
+import { getTimeDisplay } from "../../../../functions/contacts";
+import getReceiveAddressAndContactForContactsPayment from "../../utils/getReceiveAddressAndKindForPayment";
+import { useGlobalContacts } from "../../../../contexts/globalContacts";
+import { getTransactionContent } from "../../utils/transactionText";
+import displayCorrectDenomination from "../../../../functions/displayCorrectDenomination";
+import { useNodeContext } from "../../../../contexts/nodeContext";
+
+import "./contactsTransactionItem.css";
+import { ArrowDown, ArrowUp, CircleX } from "lucide-react";
+import { Colors } from "../../../../constants/theme";
+import {
+ handlePaymentUpdate,
+ sendPushNotification,
+} from "../../../../functions/messaging/publishMessage";
+import { useOverlay } from "../../../../contexts/overlayContext";
+
+function ConfirmedOrSentTransaction({
+ txParsed,
+ paymentDescription,
+ timeDifferenceMinutes,
+ timeDifferenceHours,
+ timeDifferenceDays,
+ timeDifferenceYears,
+ props,
+ navigate,
+}) {
+ const { t } = useTranslation();
+ const { theme, darkModeType } = useThemeContext();
+ const { masterInfoObject } = useGlobalContextProvider();
+ const { textColor, backgroundOffset } = useThemeColors();
+
+ const didDeclinePayment = txParsed.isRedeemed != null && !txParsed.isRedeemed;
+
+ const isOutgoingPayment =
+ (txParsed.didSend && !txParsed.isRequest) ||
+ (txParsed.isRequest && txParsed.isRedeemed && !txParsed.didSend);
+
+ if (txParsed.giftCardInfo) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {didDeclinePayment ? (
+
+ ) : isOutgoingPayment ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+ );
+}
+
+export default function ContactsTransactionItem(props) {
+ const { selectedContact, transaction, myProfile, currentTime, imageData } =
+ props;
+ const { openOverlay } = useOverlay();
+ const { t } = useTranslation();
+ const { fiatStats } = useNodeContext();
+ const { masterInfoObject } = useGlobalContextProvider();
+ const { contactsPrivateKey, publicKey } = useKeysContext();
+ const { theme, darkModeType } = useThemeContext();
+ const { textColor, backgroundColor } = useThemeColors();
+ const navigate = useNavigate();
+ const getServerTime = useServerTimeOnly();
+ const { globalContactsInformation } = useGlobalContacts();
+ const [isLoading, setIsLoading] = useState({
+ sendBTN: false,
+ declineBTN: false,
+ });
+
+ // Memoized calculations
+ const timeCalculations = useMemo(() => {
+ const endDate = currentTime;
+ const startDate = transaction.timestamp;
+
+ const timeDifferenceMs = Math.abs(endDate - startDate);
+
+ return {
+ timeDifferenceMinutes: timeDifferenceMs / (1000 * 60),
+ timeDifferenceHours: timeDifferenceMs / (1000 * 60 * 60),
+ timeDifferenceDays: timeDifferenceMs / (1000 * 60 * 60 * 24),
+ timeDifferenceYears: timeDifferenceMs / (1000 * 60 * 60 * 24 * 365),
+ };
+ }, [currentTime, transaction.serverTimestamp, transaction.timestamp]);
+
+ const {
+ timeDifferenceMinutes,
+ timeDifferenceHours,
+ timeDifferenceDays,
+ timeDifferenceYears,
+ } = timeCalculations;
+
+ const txParsed = transaction.message;
+ const paymentDescription = txParsed?.description || "";
+
+ const updatePaymentStatus = useCallback(
+ async (transaction, usingOnPage, didPay, txid) => {
+ try {
+ usingOnPage &&
+ setIsLoading((prev) => ({
+ ...prev,
+ [didPay ? "sendBTN" : "declineBTN"]: true,
+ }));
+ const currentTime = getServerTime();
+ const response = await handlePaymentUpdate({
+ transaction,
+ didPay,
+ txid,
+ globalContactsInformation,
+ selectedContact,
+ currentTime,
+ contactsPrivateKey,
+ publicKey,
+ masterInfoObject,
+ });
+
+ if (!response && usingOnPage) {
+ openOverlay({
+ for: "error",
+ errorMessage: t("errormessages.updateContactMessageError"),
+ });
+ }
+ } catch (err) {
+ console.log(err);
+ if (usingOnPage) {
+ openOverlay({
+ for: "error",
+ errorMessage: t("errormessages.declinePaymentError"),
+ });
+ }
+ } finally {
+ if (usingOnPage) {
+ setIsLoading((prev) => ({
+ ...prev,
+ [didPay ? "sendBTN" : "declineBTN"]: false,
+ }));
+ }
+ }
+ },
+ [
+ selectedContact,
+ myProfile,
+ contactsPrivateKey,
+ publicKey,
+ getServerTime,
+ navigate,
+ masterInfoObject,
+ globalContactsInformation,
+ ]
+ );
+
+ const acceptPayRequest = useCallback(
+ async (transaction, selectedContact) => {
+ setIsLoading((prev) => ({
+ ...prev,
+ sendBTN: true,
+ }));
+ const sendingAmount = transaction.message.amountMsat / 1000;
+
+ const myProfileMessage = t(
+ "contacts.internalComponents.contactsTransactions.acceptProfileMessage",
+ {
+ name: selectedContact.name || selectedContact.uniqueName,
+ }
+ );
+ const payingContactMessage = t(
+ "contacts.internalComponents.contactsTransactions.acceptPayingContactMessage",
+ {
+ name:
+ globalContactsInformation.myProfile.name ||
+ globalContactsInformation.myProfile.uniqueName,
+ }
+ );
+
+ const {
+ receiveAddress,
+ didWork,
+ error,
+ formattedPayingContactMessage,
+ retrivedContact,
+ } = await getReceiveAddressAndContactForContactsPayment({
+ sendingAmountSat: sendingAmount,
+ selectedContact,
+ myProfileMessage,
+ payingContactMessage,
+ });
+
+ if (!didWork) {
+ openOverlay({
+ for: "error",
+ errorMessage: t(error),
+ });
+ return;
+ }
+
+ setIsLoading((prev) => ({
+ ...prev,
+ sendBTN: false,
+ }));
+
+ const currentTime = getServerTime();
+ navigate("/send", {
+ state: {
+ btcAddress: receiveAddress,
+ comingFromAccept: true,
+ enteredPaymentInfo: {
+ amount: sendingAmount,
+ description: myProfileMessage,
+ },
+ contactInfo: {
+ imageData,
+ name: selectedContact.name || selectedContact.uniqueName,
+ payingContactMessage: formattedPayingContactMessage,
+ uniqueName: retrivedContact?.contacts?.myProfile?.uniqueName,
+ uuid: retrivedContact?.uuid,
+ },
+ fromPage: "contacts-request",
+ publishMessageFuncParams: {
+ transaction,
+ didPay: true,
+ globalContactsInformation,
+ selectedContact,
+ currentTime,
+ },
+ },
+ });
+ return;
+ },
+ [
+ myProfile,
+ navigate,
+ updatePaymentStatus,
+ globalContactsInformation,
+ imageData,
+ selectedContact,
+ getServerTime,
+ ]
+ );
+
+ const handleDescriptionClick = () => {
+ if (!paymentDescription) return;
+ navigate("/modal", {
+ state: {
+ wantedContent: "expandedContactMessage",
+ sliderHeight: 0.3,
+ message: paymentDescription,
+ },
+ });
+ };
+
+ if (txParsed === undefined) return null;
+
+ const isCompletedTransaction =
+ txParsed.didSend ||
+ !txParsed.isRequest ||
+ (txParsed.isRequest && txParsed.isRedeemed != null);
+
+ return (
+
+ {isCompletedTransaction ? (
+
+ ) : (
+
+
+
+
+
+
+
+ {paymentDescription && (
+
+ )}
+
+ {
+ acceptPayRequest(transaction, props.selectedContact);
+ }}
+ buttonStyles={{
+ width: "100%",
+ maxWidth: "unset",
+ overflow: "hidden",
+ borderRadius: 15,
+ alignItems: "center",
+ marginBottom: 10,
+ backgroundColor: theme ? textColor : Colors.constants.blue,
+ }}
+ textStyles={{
+ color: backgroundColor,
+ opacity: 1,
+ }}
+ textContent={t(
+ "contacts.internalComponents.contactsTransactions.send"
+ )}
+ />
+
+ {
+ updatePaymentStatus(transaction, true, false);
+ }}
+ buttonStyles={{
+ width: "100%",
+ maxWidth: "unset",
+ overflow: "hidden",
+ borderRadius: 15,
+ alignItems: "center",
+ borderWidth: 1,
+ borderColor: theme ? textColor : Colors.constants.blue,
+ backgroundColor: "transparent",
+ }}
+ textStyles={{
+ color: theme ? textColor : Colors.constants.blue,
+ }}
+ textContent={t("constants.decline")}
+ />
+
+
+ )}
+
+ );
+}
diff --git a/src/pages/contacts/internalComponents/giftCardTxItem/giftCardTxItem.css b/src/pages/contacts/internalComponents/giftCardTxItem/giftCardTxItem.css
new file mode 100644
index 0000000..4e5b25f
--- /dev/null
+++ b/src/pages/contacts/internalComponents/giftCardTxItem/giftCardTxItem.css
@@ -0,0 +1,52 @@
+/* Gift Card Transaction Container */
+.gift-card-transaction-container {
+ width: 95%;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ padding: 12.5px 0;
+ margin: 0 auto;
+ background: none;
+ border: none;
+ cursor: pointer;
+ text-align: left;
+ transition: opacity 0.2s ease;
+}
+
+.gift-card-transaction-container:hover:not(:disabled) {
+ opacity: 0.9;
+}
+
+.gift-card-transaction-container:disabled {
+ cursor: default;
+}
+
+/* Gift Card Logo Container */
+.gift-card-logo-container {
+ width: 50px;
+ height: 50px;
+ aspect-ratio: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 12px;
+ padding: 6px;
+ overflow: visible;
+ margin-right: 15px;
+ position: relative;
+}
+
+.gift-card-logo {
+ width: 100%;
+ height: 100%;
+ border-radius: 8px;
+ object-fit: contain;
+}
+
+/* Gift Card Content */
+.gift-card-content {
+ width: 100%;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
diff --git a/src/pages/contacts/internalComponents/giftCardTxItem/giftCardTxItem.jsx b/src/pages/contacts/internalComponents/giftCardTxItem/giftCardTxItem.jsx
new file mode 100644
index 0000000..595000f
--- /dev/null
+++ b/src/pages/contacts/internalComponents/giftCardTxItem/giftCardTxItem.jsx
@@ -0,0 +1,122 @@
+import FormattedSatText from "../../../../components/formattedSatText/formattedSatText";
+import ThemeText from "../../../../components/themeText/themeText";
+import { Colors } from "../../../../constants/theme";
+import "./giftCardTxItem.css";
+
+export default function GiftCardTxItem({
+ txParsed,
+ isOutgoingPayment,
+ theme,
+ darkModeType,
+ backgroundOffset,
+ textColor,
+ t,
+ timeDifference,
+ isFromProfile,
+ navigate,
+ masterInfoObject,
+}) {
+ const giftCardName = txParsed.giftCardInfo?.name;
+
+ const handleClick = () => {
+ if (isFromProfile || !navigate) return;
+
+ navigate("/modal", {
+ state: {
+ wantedContent: "viewContactsGiftInfo",
+ giftCardInfo: txParsed.giftCardInfo,
+ message: txParsed.description,
+ from: "txItem",
+ sliderHeight: 1,
+ isOutgoingPayment,
+ },
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/src/pages/contacts/screens/editMyProfilePage/editMyProfilePage.jsx b/src/pages/contacts/screens/editMyProfilePage/editMyProfilePage.jsx
index 4ae4514..9f6bbec 100644
--- a/src/pages/contacts/screens/editMyProfilePage/editMyProfilePage.jsx
+++ b/src/pages/contacts/screens/editMyProfilePage/editMyProfilePage.jsx
@@ -10,33 +10,39 @@ import ThemeText from "../../../../components/themeText/themeText";
import CustomButton from "../../../../components/customButton/customButton";
import { isValidUniqueName } from "../../../../../db";
import { encryptMessage } from "../../../../functions/encodingAndDecoding";
-import {
- deleteDatabaseImage,
- setDatabaseIMG,
-} from "../../../../../db/photoStorage";
import "./style.css";
import ContactProfileImage from "../../components/profileImage/profileImage";
import { Colors } from "../../../../constants/theme";
-import CustomInput from "../../../../components/customInput/customInput";
import { VALID_USERNAME_REGEX } from "../../../../constants";
import { useThemeContext } from "../../../../contexts/themeContext";
import useThemeColors from "../../../../hooks/useThemeColors";
-import { ImagesIconDark, xSmallIconBlack } from "../../../../constants/icons";
+import { useOverlay } from "../../../../contexts/overlayContext";
+import CustomSettingsNavbar from "../../../../components/customSettingsNavbar";
+import { Image, Trash, X } from "lucide-react";
+import SafeAreaComponent from "../../../../components/safeAreaContainer";
+import { useProfileImage } from "../../utils/useProfileImage";
+import { EditProfileTextInput } from "../../components/EditProfileTextInput/EditProfileTextInput";
+import { areImagesSame } from "../../utils/imageComparison";
-export default function EditMyProfilePage({ navProps, openOverlay }) {
+export default function EditMyProfilePage({ navProps }) {
+ const { openOverlay } = useOverlay();
const navigate = useNavigate();
const {
decodedAddedContacts,
toggleGlobalContactsInformation,
globalContactsInformation,
+ deleteContact,
} = useGlobalContacts();
-
+ const { t } = useTranslation();
const location = useLocation();
+ const queryParams = new URLSearchParams(location.search);
+ const wantsToDeleteAccount = queryParams.get("confirmed");
+
const props = { ...location.state, ...navProps };
const pageType = props?.pageType || props.route?.params?.pageType;
const fromSettings = props.fromSettings || props.route?.params?.fromSettings;
-
+ const hideProfileImage = props?.hideProfileImage;
const isEditingMyProfile = pageType.toLowerCase() === "myprofile";
const providedContact =
!isEditingMyProfile &&
@@ -44,61 +50,211 @@ export default function EditMyProfilePage({ navProps, openOverlay }) {
const myContact = globalContactsInformation.myProfile;
const isFirstTimeEditing = myContact?.didEditProfile;
+ console.log(
+ fromSettings,
+ "tesing",
+ t("contacts.editMyProfilePage.navTitle"),
+ props
+ );
+
const selectedAddedContact = props.fromInitialAdd
? providedContact
: decodedAddedContacts.find(
(contact) => contact.uuid === providedContact?.uuid
);
+ console.log(props.fromInitialAdd, selectedAddedContact, providedContact);
- return (
-
- {!fromSettings && (
-
-
{
- if (!isFirstTimeEditing) {
- toggleGlobalContactsInformation(
- {
- myProfile: {
- ...globalContactsInformation.myProfile,
- didEditProfile: true,
- },
- addedContacts: globalContactsInformation.addedContacts,
- },
- true
- );
- }
- navigate(-1);
- }}
- />
- {fromSettings ? "Edit Profile" : ""}
-
- )}
+ useEffect(() => {
+ if (!props.confirmed) return;
+ deleteContact(selectedAddedContact);
+ navigate("/contacts");
+ }, [props]);
+
+ if (hideProfileImage) {
+ return (
-
+ );
+ }
+
+ return (
+
+
+ {
+ if (!isFirstTimeEditing) {
+ toggleGlobalContactsInformation(
+ {
+ myProfile: {
+ ...globalContactsInformation.myProfile,
+ didEditProfile: true,
+ },
+ addedContacts: globalContactsInformation.addedContacts,
+ },
+ true
+ );
+ }
+ navigate(-1);
+ }}
+ LeftImageIcon={Trash}
+ leftImageFunction={() =>
+ openOverlay({
+ for: "confirm-action",
+ confirmHeader: t("contacts.editMyProfilePage.deleateWarning"),
+ fromRoute: "edit-profile",
+ useProps: true,
+ useCustomProps: true,
+ customProps: { ...props, confirmed: true },
+ })
+ }
+ leftImageStyles={{ height: 25, width: "unset", aspectRatio: 1 }}
+ showLeftImage={!isEditingMyProfile}
+ />
+
+
+
);
}
-function InnerContent({
+function ProfileInputFields({
+ inputs,
+ changeInputText,
+ setIsKeyboardActive,
+ nameRef,
+ uniquenameRef,
+ bioRef,
+ receiveAddressRef,
isEditingMyProfile,
selectedAddedContact,
+ myContact,
+ theme,
+ darkModeType,
+ textInputColor,
+ textInputBackground,
+ textColor,
+ navigate,
+ t,
+}) {
+ const { openOverlay } = useOverlay();
+ return (
+ <>
+
changeInputText(text, "name")}
+ onFocus={() => setIsKeyboardActive(true)}
+ onBlur={() => setIsKeyboardActive(false)}
+ inputRef={nameRef}
+ maxLength={30}
+ theme={theme}
+ darkModeType={darkModeType}
+ textInputColor={textInputColor}
+ textInputBackground={textInputBackground}
+ textColor={textColor}
+ />
+
+ {selectedAddedContact?.isLNURL && (
+ changeInputText(text, "receiveAddress")}
+ onFocus={() => setIsKeyboardActive(true)}
+ onBlur={() => setIsKeyboardActive(false)}
+ inputRef={receiveAddressRef}
+ maxLength={200}
+ multiline={false}
+ minHeight={60}
+ theme={theme}
+ darkModeType={darkModeType}
+ textInputColor={textInputColor}
+ textInputBackground={textInputBackground}
+ textColor={textColor}
+ />
+ )}
+
+ {isEditingMyProfile && (
+ changeInputText(text, "uniquename")}
+ onFocus={() => setIsKeyboardActive(true)}
+ onBlur={() => setIsKeyboardActive(false)}
+ inputRef={uniquenameRef}
+ maxLength={30}
+ theme={theme}
+ darkModeType={darkModeType}
+ textInputColor={textInputColor}
+ textInputBackground={textInputBackground}
+ textColor={textColor}
+ showInfoIcon={true}
+ onInfoPress={() =>
+ openOverlay({
+ for: "informationPopup",
+ textContent: t(
+ "wallet.receivePages.editLNURLContact.informationMessage"
+ ),
+ buttonText: t("constants.understandText"),
+ })
+ }
+ />
+ )}
+
+ changeInputText(text, "bio")}
+ onFocus={() => setIsKeyboardActive(true)}
+ onBlur={() => setIsKeyboardActive(false)}
+ inputRef={bioRef}
+ maxLength={150}
+ multiline={true}
+ minHeight={60}
+ maxHeight={100}
+ theme={theme}
+ darkModeType={darkModeType}
+ textInputColor={textInputColor}
+ textInputBackground={textInputBackground}
+ textColor={textColor}
+ containerStyle={{ marginBottom: 10 }}
+ />
+ >
+ );
+}
+
+function InnerContent({
+ isEditingMyProfile,
+ selectedAddedContact = {},
fromInitialAdd,
fromSettings,
openOverlay,
+ hideProfileImage,
}) {
- const location = useLocation();
+ const navigate = useNavigate();
const { contactsPrivateKey, publicKey } = useKeysContext();
- // const { theme, darkModeType } = useGlobalThemeContext();
- const { cache, refreshCache, removeProfileImageFromCache } = useImageCache();
- // const { backgroundOffset, textInputColor, textInputBackground, textColor } =
- // GetThemeColors();
const { theme, darkModeType } = useThemeContext();
+ const { cache, refreshCacheObject } = useImageCache();
const { backgroundOffset, textInputColor, textInputBackground, textColor } =
useThemeColors();
const {
@@ -107,33 +263,57 @@ function InnerContent({
toggleGlobalContactsInformation,
} = useGlobalContacts();
const { t } = useTranslation();
- const [isAddingImage, setIsAddingImage] = useState(false);
+ const {
+ isAddingImage,
+ deleteProfilePicture,
+ getProfileImage,
+ saveProfileImage,
+ } = useProfileImage();
const nameRef = useRef(null);
const uniquenameRef = useRef(null);
const bioRef = useRef(null);
const receiveAddressRef = useRef(null);
+ const didCallImagePicker = useRef(null);
const myContact = globalContactsInformation.myProfile;
- const myContactName = myContact?.name;
- const myContactBio = myContact?.bio;
- const myContactUniqueName = myContact?.uniqueName;
+ const myContactName = myContact?.name || "";
+ const myContactBio = myContact?.bio || "";
+ const myContactUniqueName = myContact?.uniqueName || "";
const isFirstTimeEditing = myContact?.didEditProfile;
- const selectedAddedContactName = selectedAddedContact?.name;
- const selectedAddedContactBio = selectedAddedContact?.bio;
- const selectedAddedContactUniqueName = selectedAddedContact?.uniqueName;
+ const selectedAddedContactName = selectedAddedContact?.name || "";
+ const selectedAddedContactBio = selectedAddedContact?.bio || "";
+ const selectedAddedContactUniqueName = selectedAddedContact?.uniqueName || "";
const selectedAddedContactReceiveAddress =
- selectedAddedContact?.receiveAddress;
+ selectedAddedContact?.receiveAddress || "";
- const [inputs, setInputs] = useState({
- name: "",
- bio: "",
- uniquename: "",
- receiveAddress: "",
- });
+ const [isSaving, setIsSaving] = useState(false);
+ const [inputs, setInputs] = useState(() => ({
+ name: isFirstTimeEditing
+ ? isEditingMyProfile
+ ? myContactName || ""
+ : selectedAddedContactName || ""
+ : "",
+ bio: isFirstTimeEditing
+ ? isEditingMyProfile
+ ? myContactBio || ""
+ : selectedAddedContactBio || ""
+ : "",
+ uniquename: isFirstTimeEditing
+ ? isEditingMyProfile
+ ? myContactUniqueName || ""
+ : selectedAddedContactUniqueName || ""
+ : "",
+ receiveAddress: selectedAddedContactReceiveAddress || "",
+ }));
- const navigate = useNavigate();
+ const [tempImage, setTempImage] = useState({
+ uri: null,
+ comparison: null,
+ updated: 0,
+ shouldDelete: false,
+ });
function changeInputText(text, type) {
setInputs((prev) => {
@@ -141,73 +321,127 @@ function InnerContent({
});
}
- useEffect(() => {
- changeInputText(
- isFirstTimeEditing
- ? isEditingMyProfile
- ? myContactName || ""
- : selectedAddedContactName || ""
- : "",
- "name"
- );
- changeInputText(
- isFirstTimeEditing
- ? isEditingMyProfile
- ? myContactBio || ""
- : selectedAddedContactBio || ""
- : "",
- "bio"
- );
- changeInputText(
- isFirstTimeEditing
- ? isEditingMyProfile
- ? myContactUniqueName || ""
- : selectedAddedContactUniqueName || ""
- : "",
- "uniquename"
- );
- changeInputText(selectedAddedContactReceiveAddress || "", "receiveAddress");
- }, [
- isEditingMyProfile,
- myContactName,
- myContactBio,
- myContactUniqueName,
- selectedAddedContactName,
- selectedAddedContactBio,
- selectedAddedContactUniqueName,
- ]);
-
const myProfileImage = cache[myContact?.uuid];
const selectedAddedContactImage = cache[selectedAddedContact?.uuid];
- const hasImage = isEditingMyProfile
+ const hasImage = tempImage.shouldDelete
+ ? false
+ : tempImage.uri
+ ? true
+ : isEditingMyProfile
? !!myProfileImage?.localUri
: !!selectedAddedContactImage?.localUri;
+ const hasChangedInfo = isEditingMyProfile
+ ? myContactName !== inputs.name ||
+ myContactBio !== inputs.bio ||
+ myContactUniqueName !== inputs.uniquename ||
+ tempImage.uri ||
+ tempImage.shouldDelete
+ : selectedAddedContactName !== inputs.name ||
+ selectedAddedContactBio !== inputs.bio ||
+ selectedAddedContactUniqueName !== inputs.uniquename ||
+ selectedAddedContactReceiveAddress !== inputs.receiveAddress ||
+ fromInitialAdd ||
+ tempImage.uri ||
+ tempImage.shouldDelete;
+
+ const handleDeleteProfilePicture = () => {
+ setTempImage({
+ uri: null,
+ comparison: null,
+ updated: 0,
+ shouldDelete: true,
+ });
+ };
+
+ const addProfilePicture = async () => {
+ if (didCallImagePicker.current) return;
+ didCallImagePicker.current = true;
+ const response = await getProfileImage();
+ if (response?.imgURL && response?.comparison) {
+ setTempImage({
+ comparison: response?.comparison,
+ uri: response?.imgURL,
+ updated: Date.now(),
+ });
+ }
+ didCallImagePicker.current = false;
+ };
+
+ useEffect(() => {
+ if (!fromInitialAdd) return;
+ if (hasImage) return;
+ // Making sure to update UI for new contacts image
+ refreshCacheObject();
+ }, []);
+
+ const inputFieldsProps = {
+ inputs,
+ changeInputText,
+ nameRef,
+ uniquenameRef,
+ bioRef,
+ receiveAddressRef,
+ isEditingMyProfile,
+ selectedAddedContact,
+ myContact,
+ theme,
+ darkModeType,
+ textInputColor,
+ textInputBackground,
+ textColor,
+ navigate,
+ t,
+ };
+
+ if (hideProfileImage) {
+ return (
+ <>
+
+
+ >
+ );
+ }
+
return (
-
+
{
- openOverlay({
- for: "error",
- errorMessage: "Feature coming soon...",
- });
- return;
if (!isEditingMyProfile && !selectedAddedContact.isLNURL) return;
if (isAddingImage) return;
if (!hasImage) {
- addProfilePicture();
+ addProfilePicture(isEditingMyProfile, selectedAddedContact);
return;
+ } else {
+ handleDeleteProfilePicture();
}
- navigate("AddOrDeleteContactImage", {
- addPhoto: addProfilePicture,
- deletePhoto: deleteProfilePicture,
- hasImage: hasImage,
- });
}}
>
-

+ {hasImage ?
:
}
)}
-
{
- nameRef.current.focus();
- }}
- >
-
- {
- changeInputText(data, "name");
- }}
- value={inputs.name || ""}
- placeholder={"Name..."}
- customInputStyles={{
- color:
- inputs.name.length < 30
- ? Colors.light.text
- : Colors.constants.cancelRed,
- }}
- />
-
-
- {selectedAddedContact?.isLNURL && (
-
{
- receiveAddressRef.current.focus();
- }}
- >
-
- {
- changeInputText(data, "receiveAddress");
- }}
- value={inputs.receiveAddress || ""}
- placeholder={"LNURL..."}
- customInputStyles={{
- color:
- inputs.receiveAddress.length < 60
- ? Colors.light.text
- : Colors.constants.cancelRed,
- }}
- />
-
-
- )}
- {isEditingMyProfile && (
-
{
- uniquenameRef.current.focus();
- }}
- >
-
- {
- changeInputText(data, "uniquename");
- }}
- value={inputs.uniquename || ""}
- placeholder={myContact.uniqueName}
- customInputStyles={{
- color:
- inputs.uniquename.length < 30
- ? Colors.light.text
- : Colors.constants.cancelRed,
- }}
- />
-
-
- )}
-
{
- bioRef.current.focus();
- }}
- >
-
- {
- changeInputText(data, "bio");
- }}
- value={inputs.bio || ""}
- placeholder={"Bio..."}
- customInputStyles={{
- color:
- inputs.bio.length < 150
- ? Colors.light.text
- : Colors.constants.cancelRed,
- maxHeight: "100px",
- }}
- multiline={true}
- />
-
-
-
+
);
async function saveChanges() {
- if (
- inputs.name.length > 30 ||
- inputs.bio.length > 150 ||
- inputs.uniquename.length > 30 ||
- (selectedAddedContact?.isLNURL && inputs.receiveAddress.length > 100)
- )
- return;
-
- const uniqueName =
- isEditingMyProfile && !isFirstTimeEditing
- ? inputs.uniquename || myContact.uniqueName
- : inputs.uniquename;
-
- if (isEditingMyProfile) {
+ try {
if (
- myContact?.bio === inputs.bio &&
- myContact?.name === inputs.name &&
- myContact?.uniqueName === inputs.uniquename &&
- isFirstTimeEditing
- ) {
- navigate(-1);
- } else {
- console.log(uniqueName, "testing");
- if (!VALID_USERNAME_REGEX.test(uniqueName)) {
- openOverlay({
- for: "error",
- errorMessage:
- "You can only have letters, numbers, or underscores in your username, and must contain at least 1 letter.",
- });
- return;
- }
+ inputs.name.length >= 30 ||
+ inputs.bio.length >= 150 ||
+ inputs.uniquename.length >= 30 ||
+ (selectedAddedContact?.isLNURL &&
+ inputs.receiveAddress.length >= 200) ||
+ isAddingImage
+ )
+ return;
+ setIsSaving(true);
+
+ console.log(tempImage, selectedAddedContact, isEditingMyProfile);
- if (myContact?.uniqueName != uniqueName) {
- const isFreeUniqueName = await isValidUniqueName(
- "blitzWalletUsers",
- inputs.uniquename.trim()
+ if (tempImage.shouldDelete) {
+ await deleteProfilePicture(isEditingMyProfile, selectedAddedContact);
+ } else if (tempImage.uri && tempImage.comparison) {
+ const areImagesTheSame = await areImagesSame(
+ tempImage.imgURL?.uri,
+ isEditingMyProfile
+ ? myProfileImage?.localUri
+ : selectedAddedContactImage?.localUri
+ );
+
+ if (!areImagesTheSame) {
+ await saveProfileImage(
+ tempImage,
+ isEditingMyProfile,
+ selectedAddedContact
);
- if (!isFreeUniqueName) {
+ }
+ }
+
+ const uniqueName =
+ isEditingMyProfile && !isFirstTimeEditing
+ ? inputs.uniquename || myContact.uniqueName
+ : inputs.uniquename;
+
+ if (isEditingMyProfile) {
+ if (
+ myContact?.bio === inputs.bio &&
+ myContact?.name === inputs.name &&
+ myContact?.uniqueName === inputs.uniquename &&
+ isFirstTimeEditing
+ ) {
+ navigate(-1);
+ } else {
+ console.log(uniqueName, "testing");
+ if (!VALID_USERNAME_REGEX.test(uniqueName)) {
openOverlay({
for: "error",
- errorMessage: "Username already taken, try again!",
+ errorMessage: t(
+ "contacts.editMyProfilePage.unqiueNameRegexError"
+ ),
});
return;
}
- }
- toggleGlobalContactsInformation(
- {
- myProfile: {
- ...globalContactsInformation.myProfile,
- name: inputs.name.trim(),
- nameLower: inputs.name.trim().toLowerCase(),
- bio: inputs.bio,
- uniqueName: uniqueName.trim(),
- uniqueNameLower: uniqueName.trim().toLowerCase(),
- didEditProfile: true,
- },
- addedContacts: globalContactsInformation.addedContacts,
- // unaddedContacts:
- // globalContactsInformation.unaddedContacts,
- },
- true
- );
- navigate(-1);
- }
- } else {
- if (fromInitialAdd) {
- let tempContact = JSON.parse(JSON.stringify(selectedAddedContact));
- tempContact.name = inputs.name.trim();
- tempContact.nameLower = inputs.name.trim().toLowerCase();
- tempContact.bio = inputs.bio;
- if (selectedAddedContact.isLNURL) {
- tempContact.receiveAddress = inputs.receiveAddress;
- }
- let newAddedContacts = JSON.parse(JSON.stringify(decodedAddedContacts));
- const isContactInAddedContacts = newAddedContacts.filter(
- (addedContact) => addedContact.uuid === tempContact.uuid
- ).length;
-
- if (isContactInAddedContacts) {
- newAddedContacts = newAddedContacts.map((addedContact) => {
- if (addedContact.uuid === tempContact.uuid) {
- return {
- ...addedContact,
- name: inputs.name,
- nameLower: inputs.name.toLowerCase(),
+ if (myContact?.uniqueName != uniqueName) {
+ const isFreeUniqueName = await isValidUniqueName(
+ "blitzWalletUsers",
+ inputs.uniquename.trim()
+ );
+ if (!isFreeUniqueName) {
+ openOverlay({
+ for: "error",
+ errorMessage: t(
+ "contacts.editMyProfilePage.usernameAlreadyExistsError"
+ ),
+ });
+ return;
+ }
+ }
+ toggleGlobalContactsInformation(
+ {
+ myProfile: {
+ ...globalContactsInformation.myProfile,
+ name: inputs.name.trim(),
+ nameLower: inputs.name.trim().toLowerCase(),
bio: inputs.bio,
- unlookedTransactions: 0,
- isAdded: true,
- };
- } else return addedContact;
- });
- } else newAddedContacts.push(tempContact);
-
- toggleGlobalContactsInformation(
- {
- myProfile: {
- ...globalContactsInformation.myProfile,
+ uniqueName: uniqueName.trim(),
+ uniqueNameLower: uniqueName.trim().toLowerCase(),
+ didEditProfile: true,
+ },
+ addedContacts: globalContactsInformation.addedContacts,
},
- addedContacts: encryptMessage(
- contactsPrivateKey,
- publicKey,
- JSON.stringify(newAddedContacts)
- ),
- // unaddedContacts:
- // globalContactsInformation.unaddedContacts,
- },
- true
- );
+ true
+ );
+ navigate(-1);
+ }
+ } else {
+ if (fromInitialAdd) {
+ let tempContact = JSON.parse(JSON.stringify(selectedAddedContact));
+ tempContact.name = inputs.name.trim();
+ tempContact.nameLower = inputs.name.trim().toLowerCase();
+ tempContact.bio = inputs.bio;
+ tempContact.isAdded = true;
+ tempContact.unlookedTransactions = 0;
+ if (selectedAddedContact.isLNURL) {
+ tempContact.receiveAddress = inputs.receiveAddress;
+ }
- return;
- }
- if (
- selectedAddedContact?.bio === inputs.bio &&
- selectedAddedContact?.name === inputs.name &&
- selectedAddedContact?.receiveAddress === inputs.receiveAddress
- )
- navigate(-1);
- else {
- let newAddedContacts = [...decodedAddedContacts];
- const indexOfContact = decodedAddedContacts.findIndex(
- (obj) => obj.uuid === selectedAddedContact.uuid
- );
+ let newAddedContacts = JSON.parse(
+ JSON.stringify(decodedAddedContacts)
+ );
+ const isContactInAddedContacts = newAddedContacts.filter(
+ (addedContact) => addedContact.uuid === tempContact.uuid
+ ).length;
- let contact = newAddedContacts[indexOfContact];
+ if (isContactInAddedContacts) {
+ newAddedContacts = newAddedContacts.map((addedContact) => {
+ if (addedContact.uuid === tempContact.uuid) {
+ return {
+ ...addedContact,
+ name: tempContact.name,
+ nameLower: tempContact.nameLower,
+ bio: tempContact.bio,
+ unlookedTransactions: 0,
+ isAdded: true,
+ };
+ } else return addedContact;
+ });
+ } else newAddedContacts.push(tempContact);
- contact["name"] = inputs.name.trim();
- contact["nameLower"] = inputs.name.trim().toLowerCase();
- contact["bio"] = inputs.bio.trim();
- if (
- selectedAddedContact.isLNURL &&
- selectedAddedContact?.receiveAddress !== inputs.receiveAddress
- ) {
- contact["receiveAddress"] = inputs.receiveAddress.trim();
+ toggleGlobalContactsInformation(
+ {
+ myProfile: {
+ ...globalContactsInformation.myProfile,
+ },
+ addedContacts: await encryptMessage(
+ contactsPrivateKey,
+ publicKey,
+ JSON.stringify(newAddedContacts)
+ ),
+ // unaddedContacts:
+ // globalContactsInformation.unaddedContacts,
+ },
+ true
+ );
+
+ return;
}
+ if (
+ selectedAddedContact?.bio === inputs.bio &&
+ selectedAddedContact?.name === inputs.name &&
+ selectedAddedContact?.receiveAddress === inputs.receiveAddress
+ )
+ navigate(-1);
+ else {
+ let newAddedContacts = [...decodedAddedContacts];
+ console.log(selectedAddedContact);
+ const indexOfContact = decodedAddedContacts.findIndex(
+ (obj) => obj.uuid === selectedAddedContact.uuid
+ );
+ console.log(indexOfContact);
+ console.log("testing");
- toggleGlobalContactsInformation(
- {
- myProfile: {
- ...globalContactsInformation.myProfile,
+ let contact = newAddedContacts[indexOfContact];
+ console.log(contact);
+
+ contact["name"] = inputs.name.trim();
+ contact["nameLower"] = inputs.name.trim().toLowerCase();
+ contact["bio"] = inputs.bio.trim();
+
+ console.log(contact);
+
+ if (
+ selectedAddedContact.isLNURL &&
+ selectedAddedContact?.receiveAddress !== inputs.receiveAddress
+ ) {
+ contact["receiveAddress"] = inputs.receiveAddress.trim();
+ }
+
+ toggleGlobalContactsInformation(
+ {
+ myProfile: {
+ ...globalContactsInformation.myProfile,
+ },
+ addedContacts: await encryptMessage(
+ contactsPrivateKey,
+ publicKey,
+ JSON.stringify(newAddedContacts)
+ ),
},
- addedContacts: encryptMessage(
- contactsPrivateKey,
- publicKey,
- JSON.stringify(newAddedContacts)
- ),
- // unaddedContacts:
- // globalContactsInformation.unaddedContacts,
- },
- true
- );
- navigate(-1);
+ true
+ );
+ navigate(-1);
+ }
}
+ } catch (err) {
+ console.log("Error saving changes", err);
+ } finally {
+ setIsSaving(false);
}
}
-
- async function addProfilePicture() {
- // const imagePickerResponse = await getImageFromLibrary({ quality: 1 });
- // const { didRun, error, imgURL } = imagePickerResponse;
- // if (!didRun) return;
- // if (error) {
- // navigate("/error", {
- // state: {
- // errorMessage: error,
- // background: location,
- // },
- // });
- // return;
- // }
- // if (isEditingMyProfile) {
- // const response = await uploadProfileImage({ imgURL: imgURL });
- // if (!response) return;
- // toggleGlobalContactsInformation(
- // {
- // myProfile: {
- // ...globalContactsInformation.myProfile,
- // hasProfileImage: true,
- // },
- // addedContacts: globalContactsInformation.addedContacts,
- // },
- // true
- // );
- // return;
- // }
- // await refreshCache(selectedAddedContact.uuid, imgURL.uri);
- }
- async function uploadProfileImage({ imgURL, removeImage }) {
- // try {
- // setIsAddingImage(true);
- // if (!removeImage) {
- // const resized = ImageManipulator.ImageManipulator.manipulate(
- // imgURL.uri
- // ).resize({ width: 350 });
- // const image = await resized.renderAsync();
- // const savedImage = await image.saveAsync({
- // compress: 0.4,
- // format: ImageManipulator.SaveFormat.WEBP,
- // });
- // const response = await setDatabaseIMG(
- // globalContactsInformation.myProfile.uuid,
- // { uri: savedImage.uri }
- // );
- // if (response) {
- // await refreshCache(
- // globalContactsInformation.myProfile.uuid,
- // response
- // );
- // return true;
- // } else throw new Error("Unable to save image");
- // } else {
- // await deleteDatabaseImage(globalContactsInformation.myProfile.uuid);
- // await removeProfileImageFromCache(
- // globalContactsInformation.myProfile.uuid
- // );
- // return true;
- // }
- // } catch (err) {
- // console.log(err);
- // navigate("/error", {
- // state: {
- // errorMessage: err.message,
- // background: location,
- // },
- // });
- // return false;
- // } finally {
- // setIsAddingImage(false);
- // }
- }
- async function deleteProfilePicture() {
- // try {
- // if (isEditingMyProfile) {
- // const response = await uploadProfileImage({ removeImage: true });
- // console.log(response);
- // if (!response) return;
- // toggleGlobalContactsInformation(
- // {
- // myProfile: {
- // ...globalContactsInformation.myProfile,
- // hasProfileImage: false,
- // },
- // addedContacts: globalContactsInformation.addedContacts,
- // },
- // true
- // );
- // return;
- // }
- // await removeProfileImageFromCache(selectedAddedContact.uuid);
- // } catch (err) {
- // navigate("/error", {
- // state: {
- // errorMessage: "Unable to deleate image.",
- // background: location,
- // },
- // });
- // console.log(err);
- // }
- }
}
diff --git a/src/pages/contacts/screens/editMyProfilePage/style.css b/src/pages/contacts/screens/editMyProfilePage/style.css
index 7d3328d..af313f2 100644
--- a/src/pages/contacts/screens/editMyProfilePage/style.css
+++ b/src/pages/contacts/screens/editMyProfilePage/style.css
@@ -9,6 +9,7 @@
flex: 1;
display: flex;
flex-direction: column;
+ margin: 0 auto;
}
#editMyProfile .editProfileScrollContainer {
flex: 1;
@@ -20,6 +21,7 @@
position: relative;
width: max-content;
align-self: center;
+ margin-bottom: 25px;
}
#editMyProfile .profileImageBackground {
width: 150px;
@@ -58,3 +60,11 @@
margin: 0;
max-width: unset;
}
+
+#editMyProfile .input-label {
+ /* margin: 0; */
+}
+#editMyProfile .charCount-label {
+ text-align: right;
+ margin: 10px 0 15px;
+}
diff --git a/src/pages/contacts/screens/myProfilePage/myProfilePage.css b/src/pages/contacts/screens/myProfilePage/myProfilePage.css
deleted file mode 100644
index e2f1935..0000000
--- a/src/pages/contacts/screens/myProfilePage/myProfilePage.css
+++ /dev/null
@@ -1,72 +0,0 @@
-#myProfilePageContainer {
- display: flex;
- flex-direction: column;
- align-items: center;
- flex: 1;
-}
-
-#myProfilePageContainer .pageNavbar {
- width: 100%;
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px;
-}
-#myProfilePageContainer .pageNavbar .settingsIcon {
- width: 30px;
- height: 30px;
- cursor: pointer;
-}
-#myProfilePageContainer .profileImageBackground {
- width: 150px;
- height: 150px;
- display: flex;
- align-items: center;
- justify-content: center;
- position: relative;
- border-radius: 50%;
- cursor: pointer;
- margin-top: 20px;
-}
-#myProfilePageContainer .scanProfileImageContianer {
- position: absolute;
- height: 30px;
- width: 30px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 50%;
- right: 20px;
- bottom: -5px;
-}
-#myProfilePageContainer .scanProfileImageContianer img {
- width: 50%;
- height: 50%;
-}
-#myProfilePageContainer .uniqueNameText {
- font-size: 2rem;
- margin-top: 20px;
- margin-bottom: 0;
-}
-#myProfilePageContainer .nameText {
- margin: 0;
-}
-#myProfilePageContainer .bioContainer {
- width: 100%;
- max-width: 400px;
- padding: 10px;
- border-radius: 8px;
- vertical-align: top;
- margin: 0;
- min-height: 60px;
- max-height: 100px;
- margin-top: 30px;
- overflow-y: scroll;
-}
-#myProfilePageContainer .bioContainer p {
- margin: 0;
-}
-
-#myProfilePageContainer .noTxText {
- text-align: center;
-}
diff --git a/src/pages/contacts/screens/myProfilePage/myProfilePage.jsx b/src/pages/contacts/screens/myProfilePage/myProfilePage.jsx
deleted file mode 100644
index ce17714..0000000
--- a/src/pages/contacts/screens/myProfilePage/myProfilePage.jsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import BackArrow from "../../../../components/backArrow/backArrow";
-import "./myProfilePage.css";
-import ContactProfileImage from "../../components/profileImage/profileImage";
-import { useGlobalContacts } from "../../../../contexts/globalContacts";
-import { useAppStatus } from "../../../../contexts/appStatus";
-import { useImageCache } from "../../../../contexts/imageCacheContext";
-import { useLocation, useNavigate } from "react-router-dom";
-import { useMemo } from "react";
-import MaxHeap from "../../../../functions/maxHeap";
-import ThemeText from "../../../../components/themeText/themeText";
-import { Colors } from "../../../../constants/theme";
-import { useThemeContext } from "../../../../contexts/themeContext";
-import useThemeColors from "../../../../hooks/useThemeColors";
-import ThemeImage from "../../../../components/ThemeImage/themeImage";
-import { ImagesIconDark, settingsIcon } from "../../../../constants/icons";
-
-export default function MyProfilePage({ openOverlay }) {
- const { cache } = useImageCache();
- const { theme, darkModeType } = useThemeContext();
- const { backgroundOffset } = useThemeColors();
-
- const { globalContactsInformation, decodedAddedContacts, contactsMessags } =
- useGlobalContacts();
- const navigate = useNavigate();
- const location = useLocation();
- const currentTime = new Date();
-
- const myContact = globalContactsInformation.myProfile;
-
- const createdPayments = useMemo(() => {
- const messageHeap = new MaxHeap();
- const MAX_MESSAGES = 50;
-
- for (let contact of Object.keys(contactsMessags)) {
- if (contact === "lastMessageTimestamp") continue;
- const data = contactsMessags[contact];
- const selectedAddedContact = decodedAddedContacts.find(
- (contactElement) => contactElement.uuid === contact
- );
-
- for (let message of data.messages) {
- const timestamp = message.timestamp;
-
- const messageObj = {
- transaction: message,
- selectedProfileImage: selectedAddedContact?.profileImage || null,
- name:
- selectedAddedContact?.name ||
- selectedAddedContact?.uniqueName ||
- "Unknown",
- contactUUID: selectedAddedContact?.uuid || contact,
- time: timestamp,
- };
-
- messageHeap.add(messageObj);
- }
- }
-
- const result = [];
- while (!messageHeap.isEmpty() && result.length < MAX_MESSAGES) {
- result.push(messageHeap.poll());
- }
-
- console.log(result.length, "LENGTH OF RESULT ARRAY");
-
- return result;
- }, [decodedAddedContacts, contactsMessags]);
-
- return (
-
-
-
-
- navigate("/edit-profile", {
- state: { pageType: "myProfile", fromSettings: false },
- })
- }
- icon={settingsIcon}
- />
-
-
{
- openOverlay({
- for: "error",
- errorMessage: "Feature coming soon...",
- });
- }}
- className="profileImageBackground"
- style={{ backgroundColor: backgroundOffset }}
- >
-
-
-

-
-
-
- {myContact?.name && (
-
- )}
-
-
-
- {createdPayments?.length != 0 ? (
-
Transactions go here
- ) : (
-
- )}
-
- );
-}
diff --git a/src/pages/contacts/utils/formatListDisplayName.js b/src/pages/contacts/utils/formatListDisplayName.js
new file mode 100644
index 0000000..9cb52f4
--- /dev/null
+++ b/src/pages/contacts/utils/formatListDisplayName.js
@@ -0,0 +1,17 @@
+export function formatDisplayName(contact) {
+ try {
+ if (contact.name?.length) {
+ return contact.name?.trim();
+ } else if (contact.uniqueName?.length) {
+ return contact.uniqueName?.trim();
+ } else if (contact.isLNURL) {
+ const [prefix, suffix] = contact.receiveAddress.split('@');
+ console.log(prefix, suffix);
+ return prefix;
+ }
+ return '';
+ } catch (err) {
+ console.log('error formatting display name', err);
+ return '';
+ }
+}
diff --git a/src/pages/contacts/utils/getReceiveAddressAndKindForPayment.js b/src/pages/contacts/utils/getReceiveAddressAndKindForPayment.js
new file mode 100644
index 0000000..850172c
--- /dev/null
+++ b/src/pages/contacts/utils/getReceiveAddressAndKindForPayment.js
@@ -0,0 +1,55 @@
+import { getDataFromCollection } from "../../../../db";
+
+export default async function getReceiveAddressAndContactForContactsPayment({
+ sendingAmountSat,
+ selectedContact,
+ myProfileMessage = "",
+ payingContactMessage = "",
+ onlyGetContact = false,
+}) {
+ try {
+ let receiveAddress;
+ let retrivedContact;
+ let message = "";
+
+ if (selectedContact.isLNURL) {
+ receiveAddress = selectedContact.receiveAddress;
+ retrivedContact = selectedContact;
+ } else {
+ retrivedContact = await getDataFromCollection(
+ "blitzWalletUsers",
+ selectedContact.uuid
+ );
+
+ if (!retrivedContact) throw new Error("errormessages.fullDeeplinkError");
+
+ if (onlyGetContact)
+ return { didWork: true, receiveAddress: "", retrivedContact };
+
+ if (retrivedContact?.contacts?.myProfile?.sparkAddress) {
+ if (payingContactMessage?.usingTranslation) {
+ message = retrivedContact.isUsingNewNotifications
+ ? JSON.stringify({
+ name: payingContactMessage.name,
+ translation: "contacts.sendAndRequestPage.contactMessage",
+ })
+ : `${payingContactMessage.name} paid you`;
+ } else {
+ message = payingContactMessage;
+ }
+
+ receiveAddress = retrivedContact?.contacts?.myProfile?.sparkAddress;
+ } else throw new Error("errormessages.legacyContactError");
+ }
+
+ return {
+ didWork: true,
+ receiveAddress,
+ retrivedContact,
+ formattedPayingContactMessage: message,
+ };
+ } catch (err) {
+ console.log("error getting receive address for contact payment");
+ return { didWork: false, error: err.message };
+ }
+}
diff --git a/src/pages/contacts/utils/hooks.js b/src/pages/contacts/utils/hooks.js
new file mode 100644
index 0000000..3d8c549
--- /dev/null
+++ b/src/pages/contacts/utils/hooks.js
@@ -0,0 +1,43 @@
+import { useMemo } from 'react';
+
+export const useProcessedContacts = (decodedAddedContacts, contactsMessags) => {
+ return useMemo(() => {
+ return decodedAddedContacts.map(contact => {
+ const info = contactsMessags[contact.uuid] || {};
+ return {
+ contact,
+ hasUnlookedTransaction: !!info.messages?.some(m => !m.message.wasSeen),
+ lastUpdated: info.lastUpdated,
+ firstMessage: info.messages?.[0],
+ };
+ });
+ }, [decodedAddedContacts, contactsMessags]);
+};
+
+export const useFilteredContacts = (
+ contactInfoList,
+ inputText,
+ hideUnknownContacts,
+) => {
+ return useMemo(() => {
+ const searchTerm = inputText.toLowerCase();
+ return contactInfoList
+ .filter(item => {
+ const matchesSearch =
+ item.contact.name?.toLowerCase().startsWith(searchTerm) ||
+ item.contact.uniqueName?.toLowerCase().startsWith(searchTerm);
+ const isNotFavorite = !item.contact.isFavorite;
+ const shouldShow = !hideUnknownContacts || item.contact.isAdded;
+
+ return matchesSearch && isNotFavorite && shouldShow;
+ })
+ .sort((a, b) => {
+ const timeDiff = (b.lastUpdated || 0) - (a.lastUpdated || 0);
+ if (timeDiff !== 0) return timeDiff;
+
+ const nameA = a.contact.name || a.contact.uniqueName || '';
+ const nameB = b.contact.name || b.contact.uniqueName || '';
+ return nameA.localeCompare(nameB);
+ });
+ }, [contactInfoList, inputText, hideUnknownContacts]);
+};
diff --git a/src/pages/contacts/utils/imageComparison.js b/src/pages/contacts/utils/imageComparison.js
new file mode 100644
index 0000000..cd3694b
--- /dev/null
+++ b/src/pages/contacts/utils/imageComparison.js
@@ -0,0 +1,92 @@
+import sha256Hash from "../../../functions/hash";
+
+/**
+ * Convert blob to ArrayBuffer
+ */
+async function blobToArrayBuffer(blob) {
+ return await blob.arrayBuffer();
+}
+
+/**
+ * Convert data URL to ArrayBuffer
+ */
+async function dataURLToArrayBuffer(dataURL) {
+ const base64 = dataURL.split(",")[1];
+ const binaryString = atob(base64);
+ const bytes = new Uint8Array(binaryString.length);
+ for (let i = 0; i < binaryString.length; i++) {
+ bytes[i] = binaryString.charCodeAt(i);
+ }
+ return bytes.buffer;
+}
+
+/**
+ * Get MD5 hash of image from various sources
+ * @param {string|Blob} imageSource - Can be a data URL, object URL, or Blob
+ * @returns {Promise
} MD5 hash as hex string
+ */
+export async function getImageHash(imageSource) {
+ if (!imageSource) return "";
+
+ try {
+ let arrayBuffer;
+
+ if (imageSource instanceof Blob) {
+ // Handle Blob directly
+ arrayBuffer = await blobToArrayBuffer(imageSource);
+ } else if (typeof imageSource === "string") {
+ if (imageSource.startsWith("data:")) {
+ // Handle data URL
+ arrayBuffer = await dataURLToArrayBuffer(imageSource);
+ } else if (imageSource.startsWith("blob:")) {
+ // Handle object URL
+ const response = await fetch(imageSource);
+ const blob = await response.blob();
+ arrayBuffer = await blobToArrayBuffer(blob);
+ } else {
+ // Handle regular URL
+ const response = await fetch(imageSource);
+ const blob = await response.blob();
+ arrayBuffer = await blobToArrayBuffer(blob);
+ }
+ } else {
+ throw new Error("Invalid image source type");
+ }
+
+ // Use SubtleCrypto to compute MD5-equivalent hash
+ // Note: MD5 is not available in Web Crypto API, so we use SHA-256
+ // and take first 32 chars to simulate MD5 length
+
+ const hashBuffer = sha256Hash(arrayBuffer);
+ // await crypto.subtle.digest('SHA-256', arrayBuffer);
+ // const fullHash = arrayBufferToHex(hashBuffer);
+
+ // Return first 32 characters to match MD5 length (128 bits)
+ return hashBuffer;
+ } catch (e) {
+ console.error("Error getting image hash:", e);
+ return "";
+ }
+}
+
+/**
+ * Compare two images to see if they're identical
+ * @param {string|Blob} source1 - First image (data URL, object URL, or Blob)
+ * @param {string|Blob} source2 - Second image (data URL, object URL, or Blob)
+ * @returns {Promise} True if images are identical
+ */
+export async function areImagesSame(source1, source2) {
+ try {
+ const hash1 = await getImageHash(source1);
+ const hash2 = await getImageHash(source2);
+ console.log(source1, source2);
+ if (!hash1 || !hash2) {
+ return false;
+ }
+
+ return hash1 === hash2;
+ } catch (e) {
+ console.error("Error comparing images:", e);
+ return false;
+ }
+}
diff --git a/src/pages/contacts/utils/transactionText.js b/src/pages/contacts/utils/transactionText.js
new file mode 100644
index 0000000..41b152b
--- /dev/null
+++ b/src/pages/contacts/utils/transactionText.js
@@ -0,0 +1,29 @@
+export const getTransactionContent = ({
+ paymentDescription,
+ didDeclinePayment,
+ txParsed,
+ t,
+}) => {
+ if (paymentDescription) {
+ return paymentDescription;
+ }
+
+ if (didDeclinePayment) {
+ return txParsed.didSend
+ ? t('transactionLabelText.requestDeclined')
+ : t('transactionLabelText.declinedRequest');
+ }
+
+ if (txParsed.isRequest) {
+ if (txParsed.didSend) {
+ return txParsed.isRedeemed === null
+ ? t('transactionLabelText.requestSent')
+ : t('transactionLabelText.requestPaid');
+ }
+ return t('transactionLabelText.paidRequest');
+ }
+
+ return txParsed.didSend
+ ? t('transactionLabelText.sent')
+ : t('transactionLabelText.received');
+};
diff --git a/src/pages/contacts/utils/useExpandedNavbar.js b/src/pages/contacts/utils/useExpandedNavbar.js
new file mode 100644
index 0000000..8497a5e
--- /dev/null
+++ b/src/pages/contacts/utils/useExpandedNavbar.js
@@ -0,0 +1,93 @@
+import { useTranslation } from "react-i18next";
+import { useNavigate } from "react-router-dom";
+import { useGlobalContacts } from "../../../contexts/globalContacts";
+import { useAppStatus } from "../../../contexts/appStatus";
+import {
+ decryptMessage,
+ encryptMessage,
+} from "../../../functions/encodingAndDecoding";
+import { useKeysContext } from "../../../contexts/keysContext";
+import { useOverlay } from "../../../contexts/overlayContext";
+
+/**
+ * Custom hook for managing profile image operations
+ * Handles adding, uploading, and deleting profile images for contacts
+ */
+export function useExpandedNavbar() {
+ const navigate = useNavigate();
+ const { t } = useTranslation();
+ const { openOverlay } = useOverlay();
+ const { contactsPrivateKey, publicKey } = useKeysContext();
+ const { isConnectedToTheInternet } = useAppStatus();
+ const { globalContactsInformation, toggleGlobalContactsInformation } =
+ useGlobalContacts();
+
+ /**
+ * toggle whether a contact is a favorite
+ * @param {object} selectedContact - The contact object (only needed if not editing own profile)
+ */
+ const handleFavortie = async ({ selectedContact }) => {
+ if (!isConnectedToTheInternet) {
+ navigate.navigate("ErrorScreen", {
+ errorMessage: t("errormessages.nointernet"),
+ });
+ return;
+ }
+ if (!selectedContact) return;
+ toggleGlobalContactsInformation(
+ {
+ myProfile: { ...globalContactsInformation.myProfile },
+ addedContacts: await encryptMessage(
+ contactsPrivateKey,
+ publicKey,
+ JSON.stringify(
+ [
+ ...JSON.parse(
+ await decryptMessage(
+ contactsPrivateKey,
+ publicKey,
+ globalContactsInformation.addedContacts
+ )
+ ),
+ ].map((savedContact) => {
+ if (savedContact.uuid === selectedContact.uuid) {
+ return {
+ ...savedContact,
+ isFavorite: !savedContact.isFavorite,
+ };
+ } else return savedContact;
+ })
+ )
+ ),
+ },
+ true
+ );
+ };
+
+ /**
+ * navigate to settings page
+ * @param {object} params - Upload parameters
+ * @param {object} selectedContact - The contact object (only needed if not editing own profile)
+ */
+ const handleSettings = ({ selectedContact }) => {
+ if (!isConnectedToTheInternet) {
+ openOverlay({
+ for: "error",
+ errorMessage: t("errormessages.nointernet"),
+ });
+ return;
+ }
+ if (!selectedContact) return;
+ navigate("/edit-profile", {
+ state: {
+ pageType: "addedContact",
+ selectedAddedContact: selectedContact,
+ },
+ });
+ };
+
+ return {
+ handleFavortie,
+ handleSettings,
+ };
+}
diff --git a/src/pages/contacts/utils/useProfileImage.js b/src/pages/contacts/utils/useProfileImage.js
new file mode 100644
index 0000000..2eb0b34
--- /dev/null
+++ b/src/pages/contacts/utils/useProfileImage.js
@@ -0,0 +1,287 @@
+import React, { useState, useRef, createContext, useContext } from "react";
+import { useOverlay } from "../../../contexts/overlayContext";
+import { useGlobalContacts } from "../../../contexts/globalContacts";
+import { useImageCache } from "../../../contexts/imageCacheContext";
+import {
+ deleteDatabaseImage,
+ setDatabaseIMG,
+} from "../../../../db/photoStorage";
+
+// Profile Image Hook
+export function useProfileImage() {
+ const { openOverlay } = useOverlay();
+ const { globalContactsInformation, toggleGlobalContactsInformation } =
+ useGlobalContacts();
+ const { refreshCache, removeProfileImageFromCache } = useImageCache();
+
+ const [isAddingImage, setIsAddingImage] = useState(false);
+
+ /**
+ * Opens file picker and gets image from library
+ */
+ const getImageFromLibrary = async () => {
+ return new Promise((resolve) => {
+ const input = document.createElement("input");
+ input.type = "file";
+ input.accept = "image/*";
+
+ input.onchange = async (e) => {
+ const file = e.target.files?.[0];
+ if (!file) {
+ resolve({ didRun: false });
+ return;
+ }
+
+ if (!file.type.startsWith("image/")) {
+ resolve({ didRun: true, error: "Please select a valid image file" });
+ return;
+ }
+
+ try {
+ const imgURL = await fileToImageData(file);
+ resolve({ didRun: true, imgURL, file });
+ } catch (error) {
+ resolve({ didRun: true, error: "Failed to load image" });
+ }
+ };
+
+ input.click();
+ });
+ };
+
+ /**
+ * Converts File to image data with dimensions
+ */
+ const fileToImageData = (file) => {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ const img = new Image();
+ img.onload = () => {
+ resolve({
+ uri: e.target.result,
+ width: img.width,
+ height: img.height,
+ file,
+ });
+ };
+ img.onerror = reject;
+ img.src = e.target.result;
+ };
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+ });
+ };
+
+ /**
+ * Gets a profile picture for a contact
+ */
+ const getProfileImage = async () => {
+ try {
+ const imagePickerResponse = await getImageFromLibrary();
+ const { didRun, error, imgURL } = imagePickerResponse;
+
+ if (!didRun) return;
+
+ if (error) {
+ openOverlay({
+ for: "error",
+ errorMessage: error,
+ });
+ return;
+ }
+
+ const startTime = Date.now();
+ setIsAddingImage(true);
+
+ const savedImage = await resizeImage({ imgURL });
+
+ if (!savedImage.uri) return;
+
+ const offsetTime = Date.now() - startTime;
+ const remainingTime = Math.max(0, 700 - offsetTime);
+
+ if (remainingTime > 0) {
+ await new Promise((resolve) => setTimeout(resolve, remainingTime));
+ }
+
+ console.log(savedImage, imgURL, "test");
+ return { comparison: savedImage, imgURL };
+ } catch (err) {
+ console.log("error getting profile image", err);
+ } finally {
+ setIsAddingImage(false);
+ }
+ };
+
+ /**
+ * Saves a profile picture for a contact
+ */
+ const saveProfileImage = async (
+ imgData,
+ isEditingMyProfile,
+ selectedContact
+ ) => {
+ try {
+ if (isEditingMyProfile) {
+ const response = await uploadProfileImage({
+ imgBlob: imgData.comparison.blob, // Pass the blob, not the URI
+ imgURL: imgData.comparison.uri, // Keep URI for cache
+ uuid: globalContactsInformation.myProfile.uuid,
+ });
+
+ if (!response) return;
+
+ toggleGlobalContactsInformation(
+ {
+ myProfile: {
+ ...globalContactsInformation.myProfile,
+ hasProfileImage: true,
+ },
+ addedContacts: globalContactsInformation.addedContacts,
+ },
+ true
+ );
+ return;
+ }
+
+ if (selectedContact) {
+ await refreshCache(selectedContact.uuid, imgData.comparison.uri, false);
+ }
+ } catch (err) {
+ console.log("error saving profile image", err);
+ }
+ };
+
+ /**
+ * Resizes and crops an image to a circle for profile pictures
+ */
+ const resizeImage = async ({ imgURL }) => {
+ try {
+ const { width: originalWidth, height: originalHeight, uri } = imgURL;
+ const photoWidth = originalWidth * 0.95;
+ const photoHeight = originalHeight * 0.95;
+ const targetSize = 250;
+
+ const smallerDimension = Math.min(photoWidth, photoHeight);
+ const cropSize = smallerDimension;
+ const cropX = (photoWidth - cropSize) / 2;
+ const cropY = (photoHeight - cropSize) / 2;
+
+ return new Promise((resolve) => {
+ const img = new Image();
+ img.onload = () => {
+ const canvas = document.createElement("canvas");
+ canvas.width = targetSize;
+ canvas.height = targetSize;
+ const ctx = canvas.getContext("2d");
+
+ ctx.drawImage(
+ img,
+ cropX,
+ cropY,
+ cropSize,
+ cropSize,
+ 0,
+ 0,
+ targetSize,
+ targetSize
+ );
+
+ canvas.toBlob(
+ (blob) => {
+ const resizedUri = URL.createObjectURL(blob);
+ resolve({
+ uri: resizedUri,
+ width: targetSize,
+ height: targetSize,
+ blob,
+ });
+ },
+ "image/webp",
+ 0.4
+ );
+ };
+ img.src = uri;
+ });
+ } catch (err) {
+ console.log("Error resizing image", err);
+ return {};
+ }
+ };
+
+ /**
+ * Uploads and processes a profile image
+ */
+ const uploadProfileImage = async ({ imgBlob, imgURL, uuid, removeImage }) => {
+ try {
+ if (!removeImage) {
+ console.log(imgURL);
+ const response = await setDatabaseIMG(uuid, imgBlob);
+ if (response) {
+ await refreshCache(uuid, imgURL, false);
+ return true;
+ } else {
+ throw new Error("Unable to save profile image");
+ }
+ } else {
+ await deleteDatabaseImage(uuid);
+ await removeProfileImageFromCache(uuid);
+ return true;
+ }
+ } catch (err) {
+ console.log(err);
+ openOverlay({ for: "error", errorMessage: err.message });
+ return false;
+ }
+ };
+
+ /**
+ * Deletes a profile picture
+ */
+ const deleteProfilePicture = async (
+ isEditingMyProfile,
+ selectedContact = null
+ ) => {
+ try {
+ if (isEditingMyProfile) {
+ console.log(globalContactsInformation.myProfile.uuid);
+ const response = await uploadProfileImage({
+ removeImage: true,
+ uuid: globalContactsInformation.myProfile.uuid,
+ });
+
+ if (!response) return;
+
+ toggleGlobalContactsInformation(
+ {
+ myProfile: {
+ ...globalContactsInformation.myProfile,
+ hasProfileImage: false,
+ },
+ addedContacts: globalContactsInformation.addedContacts,
+ },
+ true
+ );
+ return;
+ }
+
+ if (selectedContact) {
+ await removeProfileImageFromCache(selectedContact.uuid);
+ }
+ } catch (err) {
+ openOverlay({
+ for: "error",
+ errorMessage: "Failed to delete profile image",
+ });
+ console.log(err);
+ }
+ };
+
+ return {
+ isAddingImage,
+ deleteProfilePicture,
+ getProfileImage,
+ saveProfileImage,
+ };
+}
diff --git a/src/pages/contacts/utils/utilityFunctions.js b/src/pages/contacts/utils/utilityFunctions.js
new file mode 100644
index 0000000..cc4f50d
--- /dev/null
+++ b/src/pages/contacts/utils/utilityFunctions.js
@@ -0,0 +1,66 @@
+import { formatLocalTimeNumeric } from "../../../functions/timeFormatter";
+
+function isSameDay(date1, date2) {
+ return (
+ date1.getFullYear() === date2.getFullYear() &&
+ date1.getMonth() === date2.getMonth() &&
+ date1.getDate() === date2.getDate()
+ );
+}
+
+function getDaysDifference(laterDate, earlierDate) {
+ const later = new Date(
+ laterDate.getFullYear(),
+ laterDate.getMonth(),
+ laterDate.getDate()
+ );
+ const earlier = new Date(
+ earlierDate.getFullYear(),
+ earlierDate.getMonth(),
+ earlierDate.getDate()
+ );
+
+ const differenceMs = later - earlier;
+ return Math.floor(differenceMs / (1000 * 60 * 60 * 24));
+}
+
+export function createFormattedDate(time, currentTime, t) {
+ const date = new Date(time);
+ const currentDate = new Date(currentTime);
+
+ const daysOfWeek = [
+ t("weekdays.Sun"),
+ t("weekdays.Mon"),
+ t("weekdays.Tue"),
+ t("weekdays.Wed"),
+ t("weekdays.Thu"),
+ t("weekdays.Fri"),
+ t("weekdays.Sat"),
+ ];
+
+ let formattedTime;
+
+ if (isSameDay(date, currentDate)) {
+ const hours = date.getHours();
+ const minutes = date.getMinutes();
+ const ampm = hours >= 12 ? "PM" : "AM";
+ const formattedHours = hours % 12 === 0 ? 12 : hours % 12;
+ const formattedMinutes = minutes < 10 ? "0" + minutes : minutes;
+ formattedTime = `${formattedHours}:${formattedMinutes} ${ampm}`;
+ } else {
+ const daysDiff = getDaysDifference(currentDate, date);
+ if (daysDiff === 1) {
+ formattedTime = t("constants.yesterday");
+ } else if (daysDiff <= 7) {
+ formattedTime = daysOfWeek[date.getDay()];
+ } else {
+ formattedTime = formatLocalTimeNumeric(date);
+ }
+ }
+
+ return formattedTime;
+}
+
+export function formatMessage(message) {
+ return message?.message?.description;
+}
diff --git a/src/pages/createSeed/createSeed.jsx b/src/pages/createSeed/createSeed.jsx
index 349f05d..ef98604 100644
--- a/src/pages/createSeed/createSeed.jsx
+++ b/src/pages/createSeed/createSeed.jsx
@@ -13,8 +13,10 @@ import { useTranslation } from "react-i18next";
import useThemeColors from "../../hooks/useThemeColors";
import ThemeText from "../../components/themeText/themeText";
import PageNavBar from "../../components/navBar/navBar";
+import { useOverlay } from "../../contexts/overlayContext";
-function CreateSeed({ openOverlay }) {
+function CreateSeed() {
+ const { openOverlay } = useOverlay();
const { t } = useTranslation();
const { mnemoinc, setMnemoinc } = useAuth();
const seed = mnemoinc?.split(" ");
diff --git a/src/pages/customHalfModal/Modal.css b/src/pages/customHalfModal/Modal.css
index 2623c24..c143369 100644
--- a/src/pages/customHalfModal/Modal.css
+++ b/src/pages/customHalfModal/Modal.css
@@ -8,7 +8,7 @@
display: flex;
justify-content: center;
align-items: flex-end; /* modal slides up from bottom */
- z-index: 999;
+ z-index: 1002;
}
.modal {
diff --git a/src/pages/customHalfModal/components/editLNURLOnReceive/index.jsx b/src/pages/customHalfModal/components/editLNURLOnReceive/index.jsx
index 6a1fa68..243ba2d 100644
--- a/src/pages/customHalfModal/components/editLNURLOnReceive/index.jsx
+++ b/src/pages/customHalfModal/components/editLNURLOnReceive/index.jsx
@@ -4,8 +4,6 @@ import ContactProfileImage from "../../../contacts/components/profileImage/profi
import useThemeColors from "../../../../hooks/useThemeColors";
import ActivityIndicator from "../../../../components/activityIndicator/activityIndicator";
import { Colors } from "../../../../constants/theme";
-import ThemeImage from "../../../../components/ThemeImage/themeImage";
-import { aboutIcon, ImagesIconDark } from "../../../../constants/icons";
import ThemeText from "../../../../components/themeText/themeText";
import { useTranslation } from "react-i18next";
import CustomInput from "../../../../components/customInput/customInput";
@@ -13,13 +11,15 @@ import { useGlobalContacts } from "../../../../contexts/globalContacts";
import CustomButton from "../../../../components/customButton/customButton";
import { VALID_USERNAME_REGEX } from "../../../../constants";
import { isValidUniqueName } from "../../../../../db";
+import { useOverlay } from "../../../../contexts/overlayContext";
+import { Image, Info } from "lucide-react";
export default function EditLNURLContactOnReceivePage({
theme,
darkModeType,
- openOverlay,
onClose,
}) {
+ const { openOverlay } = useOverlay();
const { globalContactsInformation, toggleGlobalContactsInformation } =
useGlobalContacts();
const { t } = useTranslation();
@@ -107,11 +107,7 @@ export default function EditLNURLContactOnReceivePage({
)}
-
+
@@ -123,8 +119,8 @@ export default function EditLNURLContactOnReceivePage({
"wallet.receivePages.editLNURLContact.usernameInputDesc"
)}
/>
-
+
openOverlay({
for: "informationPopup",
textContent: t(
@@ -133,8 +129,10 @@ export default function EditLNURLContactOnReceivePage({
buttonText: t("constants.understandText"),
})
}
- styles={{ width: 20, height: 20, cursor: "pointer" }}
- icon={aboutIcon}
+ style={{ width: 20, height: 20, cursor: "pointer" }}
+ color={
+ theme && darkModeType ? Colors.dark.text : Colors.constants.blue
+ }
/>
diff --git a/src/pages/customHalfModal/index.jsx b/src/pages/customHalfModal/index.jsx
index 60c7860..a8512c2 100644
--- a/src/pages/customHalfModal/index.jsx
+++ b/src/pages/customHalfModal/index.jsx
@@ -8,6 +8,7 @@ import ManualEnterSendAddress from "../wallet/components/sendOptions/manualEnter
import SwitchReceiveOption from "../switchReceiveOption/switchReceiveOption";
import EditLNURLContactOnReceivePage from "./components/editLNURLOnReceive";
import LRC20TokenInformation from "../../functions/lrc20/lrc20TokenDataHalfModal";
+import AddContactsModal from "../contacts/components/addContactsHalfModal/addContactsHalfModal";
export default function CustomHalfModal({
onClose,
@@ -81,6 +82,20 @@ export default function CustomHalfModal({
params={params}
/>
);
+ case "addContactsHalfModal":
+ return (
+
+ );
case "confirmSMS":
return
Confirm SMS: {params?.message}
;
default:
diff --git a/src/pages/disclaimer/disclaimer.jsx b/src/pages/disclaimer/disclaimer.jsx
index 495d682..cd4f3ca 100644
--- a/src/pages/disclaimer/disclaimer.jsx
+++ b/src/pages/disclaimer/disclaimer.jsx
@@ -8,7 +8,11 @@ import ThemeText from "../../components/themeText/themeText";
import Icon from "../../components/customIcon/customIcon";
import PageNavBar from "../../components/navBar/navBar";
import { disclaimerKeys } from "../../constants/icons";
-function DisclaimerPage({ openOverlay }) {
+import { useOverlay } from "../../contexts/overlayContext";
+import { KeyRound } from "lucide-react";
+
+function DisclaimerPage() {
+ const { openOverlay } = useOverlay();
const location = useLocation();
const params = location.state;
const nextPageName = params?.nextPageName;
@@ -52,7 +56,7 @@ function DisclaimerPage({ openOverlay }) {
{t("createAccount.disclaimerPage.subHeader")}
-

+
{t("createAccount.disclaimerPage.imgCaption")}
@@ -83,7 +87,7 @@ function DisclaimerPage({ openOverlay }) {
{" "}
+ />
e.stopPropagation()}
>
-
+
+
+
{
+ return decodedAddedContacts?.find((c) => c.uuid === sendingContactUUID);
+ }, [decodedAddedContacts, sendingContactUUID]);
- const description = transaction.details.description;
+ const isPending = transaction.paymentStatus === "pending";
+ const isFailed = transaction.paymentStatus === "failed";
const isLRC20Payment = transaction.details.isLRC20Payment;
const selectedToken = isLRC20Payment
? sparkInformation.tokens?.[transaction.details.LRC20Token]
- : "";
+ : undefined;
+
const formattedTokensBalance = formatTokensNumber(
- transaction?.details?.amount,
+ transaction.details.amount,
selectedToken?.tokenMetadata?.decimals
);
+ console.log(transaction, sendingContactUUID);
+ const paymentType = sendingContactUUID
+ ? t("screens.inAccount.expandedTxPage.contactPaymentType")
+ : transaction.details.isGift
+ ? t("constants.gift")
+ : transaction.paymentType;
- console.log(isLRC20Payment, formattedTokensBalance);
-
- useEffect(() => {
- setWindowWidth(window.innerWidth);
- window.addEventListener("resize", (e) => {
- setWindowWidth(window.innerWidth);
- });
- }, []);
- const renderLRC20TokenRow = () => {
- if (!isLRC20Payment) return null;
+ const paymentDate = new Date(transaction.details.time);
- return (
- <>
-
- 320 ? "right" : "center" }}
- textContent={selectedToken?.tokenMetadata?.tokenTicker
- ?.toUpperCase()
- ?.slice(0, TOKEN_TICKER_MAX_LENGTH)}
- />
- >
+ const handleSave = async (memoText) => {
+ if (memoText === transaction.details.description) return;
+ const newTx = structuredClone(transaction);
+ newTx.details.description = memoText;
+ newTx.useTempId = true;
+ newTx.id = transaction.sparkID;
+ newTx.tempID = transaction.sparkID;
+ await bulkUpdateSparkTransactions(
+ [newTx],
+ undefined,
+ undefined,
+ undefined,
+ true
);
+ setTransaction(newTx);
};
+ const statusColors = getStatusColors({
+ theme,
+ darkModeType,
+ isPending,
+ isFailed,
+ });
+
return (
<>
navigate(-1)} />
+
-
-
-
-

-
-
-
+
+
+
1
+ isLRC20Payment && formattedTokensBalance >= 1
? formattedTokensBalance
: transaction.details.amount
}
useCustomLabel={isLRC20Payment}
customLabel={selectedToken?.tokenMetadata?.tokenTicker}
useMillionDenomination={true}
+ styles={{ margin: 0, fontSize: "1.5em" }}
/>
-
-
-
+
+
+
+
+
+ {sendingContactUUID && (
+
+
+

+
-
-
+ )}
+
-
- 320 ? "right" : "center" }}
- textContent={`${
- paymentDate.getHours() <= 9
- ? "0" + paymentDate.getHours()
- : paymentDate.getHours()
- }:${
- paymentDate.getMinutes() <= 9
- ? "0" + paymentDate.getMinutes()
- : paymentDate.getMinutes()
- }`}
+
-
- 320 ? "end" : "center",
- }}
- styles={{ marginTop: 0, marginBottom: 0 }}
- neverHideBalance={true}
- balance={isFailed ? 0 : transaction.details.fee}
+
-
- 320 ? "right" : "center",
- }}
- textContent={paymentType}
+
- {isPending && transaction.paymentType === "bitcoin" && (
- <>
-
- 320 ? "right" : "center",
- }}
- textContent={
- transaction.details.direction === "INCOMING" ? "3" : "2"
- }
- />
- >
+
+ {isLRC20Payment && (
+
)}
- {renderLRC20TokenRow()}
- {description && (
-
- )}
+
+
+
- navigate("/technical-details", { state: { transaction } })
- }
+ textContent={t("screens.inAccount.expandedTxPage.detailsBTN")}
buttonStyles={{
- width: "100%",
- maxWidth: "max-content",
- minWidth: "unset",
backgroundColor: theme ? Colors.dark.text : Colors.light.blue,
margin: "30px auto",
}}
textStyles={{ color: theme ? Colors.light.text : Colors.dark.text }}
- textContent={"Technical details"}
+ actionFunction={() =>
+ navigate("/technical-details", { state: { transaction } })
+ }
/>
-
+
+
>
);
}
-function Border({ windowWidth }) {
- console.log(windowWidth);
- const { theme } = useThemeContext();
- const dotsWidth = windowWidth * 0.95 - 30;
- const numDots = Math.floor(dotsWidth / 25);
-
- let dotElements = [];
-
- for (let index = 0; index < numDots; index++) {
- dotElements.push(
+function StatusCircle({ isPending, isFailed, colors, backgroundColor, theme }) {
+ return (
+
- );
- }
+ className="paymentStatusOuterContainer"
+ style={{ backgroundColor: colors.bg }}
+ >
+
+
+ {isPending ? (
+
+ ) : isFailed ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+}
+function PaymentStatus({ isPending, isFailed, colors }) {
return (
-
-
{dotElements}
+
);
}
-function ReceiptDots({ windowWidth }) {
- const { backgroundColor } = useThemeColors();
- let dotElements = [];
- const dotsWidth = windowWidth;
- const numDots = Math.floor(dotsWidth / 20);
+function Info({ label, value }) {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+function MemoSection({ initialValue, onSave, backgroundColor }) {
+ const [editing, setEditing] = useState(false);
+ const [value, setValue] = useState(initialValue || "");
+ const ref = useRef();
- for (let index = 0; index < numDots; index++) {
- dotElements.push(
-
- );
- }
+ return (
+
+
+
+ {/*
*/}
+
+
+ {(editing || initialValue) && (
+
+ {editing ? (
+
+ )}
+
+ );
+}
+
+function Border({ backgroundColor }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+function ReceiptDots({ backgroundColor }) {
return (
-
{dotElements}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
+
+function formatTime(date) {
+ return `${String(date.getHours()).padStart(2, "0")}:${String(
+ date.getMinutes()
+ ).padStart(2, "0")}`;
+}
+
+function getStatusColors({ theme, darkModeType, isPending, isFailed }) {
+ if (isPending) {
+ return {
+ outer: theme
+ ? Colors.dark.expandedTxPendingOuter
+ : Colors.light.expandedTxPendingOuter,
+ inner: theme
+ ? Colors.dark.expandedTxPendingInner
+ : Colors.light.expandedTxPendingInner,
+ text: theme ? Colors.dark.text : Colors.light.expandedTxPendingInner,
+ bg: theme
+ ? Colors.dark.expandedTxPendingInner
+ : Colors.light.expandedTxPendingOuter,
+ };
+ }
+
+ if (isFailed) {
+ return {
+ outer:
+ theme && darkModeType
+ ? Colors.lightsout.backgroundOffset
+ : Colors.light.expandedTxFailed,
+ inner:
+ theme && darkModeType ? Colors.dark.text : Colors.constants.cancelRed,
+ text:
+ theme && darkModeType ? Colors.dark.text : Colors.constants.cancelRed,
+ bg:
+ theme && darkModeType
+ ? Colors.lightsout.background
+ : Colors.light.expandedTxFailed,
+ };
+ }
+
+ return {
+ outer: theme
+ ? Colors.dark.expandedTxConfimred
+ : Colors.light.expandedTxConfimred,
+ inner: theme ? Colors.dark.text : Colors.constants.blue,
+ text: theme ? Colors.dark.text : Colors.constants.blue,
+ bg: theme
+ ? Colors.dark.expandedTxConfimred
+ : Colors.light.expandedTxConfimred,
+ };
+}
diff --git a/src/pages/expandedTxPage/style.css b/src/pages/expandedTxPage/style.css
index 1a07d2c..3f12feb 100644
--- a/src/pages/expandedTxPage/style.css
+++ b/src/pages/expandedTxPage/style.css
@@ -1,156 +1,254 @@
+/* Root container */
.expandedTxContainer {
- height: 100%;
- max-width: 600px;
width: 95%;
- align-self: center;
- overflow-y: scroll;
+ max-width: 600px;
+ margin: 0 auto;
+ height: 100%;
+ overflow-y: auto;
}
-.expandedTxContainer .receiptContainer {
+
+/* Receipt */
+.receiptContainer {
+ position: relative;
width: 100%;
- height: auto;
- border-top-right-radius: 20px;
border-top-left-radius: 20px;
- padding: 15px;
+ border-top-right-radius: 20px;
+ padding: 20px;
padding-top: 40px;
+ margin-top: 70px;
display: flex;
- align-items: center;
flex-direction: column;
- margin-top: 80px;
- margin-bottom: 20px;
- position: relative;
+ align-items: center;
}
-.expandedTxContainer .receiptContainer .paymentStatusOuterContainer {
- width: 100px;
- height: 100px;
+/* =========================
+ STATUS CIRCLE
+========================= */
+
+.paymentStatusContainer {
position: absolute;
top: -70px;
- border-radius: 50px;
+ width: 120px;
+ height: 120px;
+ border-radius: 50%;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.paymentStatusOuterContainer {
+ width: 100px;
+ height: 100px;
display: flex;
align-items: center;
justify-content: center;
+ overflow: hidden;
+ border-radius: 50%;
}
-.expandedTxContainer
- .receiptContainer
- .paymentStatusOuterContainer
- .paymentStatusFirstCircle {
+
+.paymentStatusFirstCircle {
width: 80px;
height: 80px;
- border-radius: 40px;
+ border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
-.expandedTxContainer
- .receiptContainer
- .paymentStatusOuterContainer
- .paymentStatusSecondCircle {
+
+.paymentStatusSecondCircle {
width: 60px;
height: 60px;
- border-radius: 40px;
+ border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
-.expandedTxContainer .receiptContainer .paymentStatusIcon {
- width: 60%;
- height: 60%;
+
+.paymentStatusSecondCircle svg {
+ width: 26px;
+ height: 26px;
}
-.expandedTxContainer .receiptContainer .receiveAmountLabel {
- font-size: 20px;
- text-align: center;
+
+/* =========================
+ AMOUNT + LABEL
+========================= */
+
+.receiveAmountLabel {
margin-top: 20px;
+ text-align: center;
+ margin-bottom: 0;
+}
+
+.amountContainer {
+ margin-top: 8px;
}
-.expandedTxContainer .paymentStatusTextContanier {
+/* =========================
+ PAYMENT STATUS ROW
+========================= */
+
+.paymentStatusTextContanier {
+ width: 100%;
+ max-width: 320px;
display: flex;
- align-items: center;
justify-content: space-between;
- width: 100%;
- max-width: 300px;
- margin-top: 30px;
- margin-bottom: 10px;
+ align-items: center;
+ margin-top: 24px;
}
-.expandedTxContainer .paymentStatusPillContiner {
- padding: 2px 15px;
- border-radius: 20px;
+
+.paymentStatusPillContiner {
+ padding: 6px 16px;
+ border-radius: 16px;
}
-.expandedTxContainer .paymentStatusPillContiner p {
+
+.paymentStatusPillContiner p {
margin: 0;
+ white-space: nowrap;
}
-.expandedTxContainer .borderElementsContainer {
+/* =========================
+ BORDER DOTS
+========================= */
+
+.borderElementsContainer {
width: 100%;
display: flex;
justify-content: space-between;
- overflow: hidden;
-}
-.expandedTxContainer .borderElementScroll {
- width: max-content;
+ margin: 20px 0 10px;
display: flex;
- justify-content: space-between;
+ gap: 5px;
}
-.expandedTxContainer .dotElementsContainer {
- position: absolute;
- bottom: -10px;
+.borderElementsContainer .border-element {
+ width: calc(100% / 25);
+ height: 2px;
+ background-color: red;
+}
+
+/* =========================
+ CONTACT ROW
+========================= */
+
+.contactRow {
width: 100%;
display: flex;
- justify-content: space-between;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 10px;
+ padding: 10px;
+ border-radius: 8px;
+}
+
+.profileImage {
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
overflow: hidden;
+ margin-right: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.profileImage img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
}
-.expandedTxContainer .infoGridContainer {
+
+/* =========================
+ DETAILS GRID
+========================= */
+
+.infoGridContainer {
width: 100%;
- max-width: 250px;
+ max-width: 320px;
display: grid;
grid-template-columns: 1fr 1fr;
- align-items: center;
+ gap: 12px 8px;
+ margin-top: 10px;
}
-.expandedTxContainer .infoGridContainer p:nth-child(2n) {
+.infoGridContainer p:nth-child(2n) {
+ text-align: right;
margin: 0;
}
-.expandedTxContainer .descriptionContainer {
+/* =========================
+ MEMO SECTION
+========================= */
+
+.descriptionContainer {
width: 100%;
- max-width: 300px;
+ max-width: 320px;
+ margin-top: 20px;
+}
+
+.memoHeader {
display: flex;
- flex-direction: column;
+ justify-content: space-between;
align-items: center;
- margin-top: 20px;
+ margin-bottom: 12px;
+}
+
+.iconButton {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 4px;
}
-.expandedTxContainer .descriptionScrollviewContainer {
+
+.descriptionScrollviewContainer {
width: 100%;
- max-width: 300px;
height: 100px;
- padding: 10px;
+ padding: 12px;
border-radius: 8px;
- overflow-y: scroll;
+ overflow-y: auto;
}
-.expandedTxContainer .descriptionScrollviewContainer p {
- margin: 0;
+
+.descriptionScrollviewContainer textarea {
+ width: 100%;
+ height: 100%;
+ resize: none;
+ border: none;
+ outline: none;
+ background: transparent;
+ font-size: 14px;
+ font-family: inherit;
}
-@media screen and (max-width: 320px) {
- .expandedTxContainer .paymentStatusTextContanier {
+/* =========================
+ RECEIPT DOTS
+========================= */
+
+.dotElementsContainer {
+ position: absolute;
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ bottom: -7.5px;
+}
+.dotElementsContainer .dot-element {
+ width: 15px;
+ height: 15px;
+ border-radius: 50%;
+}
+
+/* =========================
+ RESPONSIVE
+========================= */
+
+@media (max-width: 320px) {
+ .paymentStatusTextContanier {
flex-direction: column;
- margin-bottom: 20px;
- }
- .expandedTxContainer .paymentStatusTextContanier p {
- margin: 0;
- text-align: center;
+ gap: 8px;
}
- .expandedTxContainer .infoGridContainer {
+ .infoGridContainer {
grid-template-columns: 1fr;
text-align: center;
}
- .expandedTxContainer .infoGridContainer p:nth-child(1n) {
- margin-bottom: 0px;
- }
-}
-@media screen and (max-width: 200px) {
- .expandedTxContainer .receiptContainer .receiveAmountLabel {
- font-size: 16px;
+ .infoGridContainer p:nth-child(2n) {
+ text-align: center;
}
}
diff --git a/src/pages/informationPopup/index.jsx b/src/pages/informationPopup/index.jsx
index b3afd28..af52a9d 100644
--- a/src/pages/informationPopup/index.jsx
+++ b/src/pages/informationPopup/index.jsx
@@ -7,8 +7,7 @@ import CustomButton from "../../components/customButton/customButton";
import { useThemeContext } from "../../contexts/themeContext";
import useThemeColors from "../../hooks/useThemeColors";
import ThemeText from "../../components/themeText/themeText";
-import ThemeImage from "../../components/ThemeImage/themeImage";
-import { xSmallIcon } from "../../constants/icons";
+import { X } from "lucide-react";
export default function InformationPopup({ overlay, onClose }) {
const navigate = useNavigate();
@@ -63,10 +62,11 @@ export default function InformationPopup({ overlay, onClose }) {
className="contentContainer"
onClick={(e) => e.stopPropagation()}
>
-
diff --git a/src/pages/loadingScreen/index.jsx b/src/pages/loadingScreen/index.jsx
index bec6bee..d8d671c 100644
--- a/src/pages/loadingScreen/index.jsx
+++ b/src/pages/loadingScreen/index.jsx
@@ -22,6 +22,7 @@ import { useThemeContext } from "../../contexts/themeContext.jsx";
import useThemeColors from "../../hooks/useThemeColors.js";
import loadNewFiatData from "../../functions/saveAndUpdateFiatData.js";
import Storage from "../../functions/localStorage.js";
+import { useTranslation } from "react-i18next";
export default function LoadingScreen() {
const didInitializeMessageIntervalRef = useRef(false);
@@ -30,29 +31,27 @@ export default function LoadingScreen() {
const navigate = useNavigate();
const { theme, darkModeType } = useThemeContext();
const { textColor } = useThemeColors();
+ const { connectToSparkWallet } = useSpark();
const {
- setStartConnectingToSpark,
- setNumberOfCachedTxs,
- connectToSparkWallet,
- } = useSpark();
- const { toggleMasterInfoObject, masterInfoObject, setMasterInfoObject } =
- useGlobalContextProvider();
+ toggleMasterInfoObject,
+ masterInfoObject,
+ setMasterInfoObject,
+ preloadedUserData,
+ setPreLoadedUserData,
+ } = useGlobalContextProvider();
const { mnemoinc } = useAuth();
- const { toggleContactsPrivateKey, contactsPrivateKey, publicKey } =
- useKeysContext();
+ const { toggleContactsPrivateKey } = useKeysContext();
const { toggleGlobalContactsInformation, globalContactsInformation } =
useGlobalContacts();
- const { onLiquidBreezEvent } = useLiquidEvent();
+ const didRunConnectionRef = useRef(false);
+ const { t } = useTranslation();
+
const { toggleGlobalAppDataInformation } = useGlobalAppData();
- const { setBitcoinPriceArray, toggleSelectedBitcoinPrice } =
- useBitcoinPriceContext();
- const { toggleFiatStats, toggleLiquidNodeInformation } = useNodeContext();
+
const [loadingMessage, setLoadingMessage] = useState(
"Please don't leave the tab"
);
const [hasError, setHasError] = useState("");
- const [didOpenDatabases, setDidOpenDatabases] = useState(false);
- const numberOfCachedTransactionsRef = useRef(null);
useEffect(() => {
if (didInitializeMessageIntervalRef.current) return;
@@ -71,54 +70,79 @@ export default function LoadingScreen() {
}, []);
useEffect(() => {
- async function retriveExternalData() {
+ async function startConnectProcess() {
+ const startTime = Date.now();
+
try {
+ console.log("Process 1", new Date().getTime());
connectToSparkWallet();
- const [didOpen, posTransactions, sparkTxs] = await Promise.all([
- initializeDatabase(),
- initializePOSTransactionsDatabase(),
- initializeSparkDatabase(),
- ]);
- if (!didOpen || !posTransactions || !sparkTxs)
- throw new Error("Database initialization failed");
-
- const [txs, initWallet] = await Promise.all([
- getCachedSparkTransactions(),
- initializeUserSettings(
+
+ const hasSavedInfo = Object.keys(masterInfoObject || {}).length > 5; //arbitrary number but filters out onboarding items
+
+ if (!hasSavedInfo) {
+ // connectToLiquidNode(accountMnemoinc);
+ const [
+ didOpen,
+ giftCardTable,
+ posTransactions,
+ // sparkTxs,
+ // rootstockSwaps,
+ ] = await Promise.all([
+ initializeDatabase(),
+ initializePOSTransactionsDatabase(),
+ initializeSparkDatabase(),
+ ]);
+
+ if (!didOpen || !posTransactions || !giftCardTable)
+ throw new Error("Database initialization failed");
+
+ const didLoadUserSettings = await initializeUserSettings({
mnemoinc,
toggleContactsPrivateKey,
setMasterInfoObject,
toggleGlobalContactsInformation,
- toggleGlobalAppDataInformation
- ),
- ]);
+ // toggleGLobalEcashInformation,
+ toggleGlobalAppDataInformation,
+ toggleMasterInfoObject,
+ preloadedData: preloadedUserData.data,
+ setPreLoadedUserData,
+ });
+
+ console.log("Process 2", new Date().getTime());
+
+ if (!didLoadUserSettings)
+ throw new Error(
+ t("screens.inAccount.loadingScreen.userSettingsError")
+ );
+ }
- if (!initWallet) throw new Error("Error loading user profile");
+ console.log("Process 3", new Date().getTime());
- numberOfCachedTransactionsRef.current = txs;
- setDidOpenDatabases(true);
+ const elapsedTime = Date.now() - startTime;
+ const remainingTime = Math.max(
+ 0,
+ (hasSavedInfo ? 500 : 800) - elapsedTime
+ );
+
+ if (remainingTime > 0) {
+ console.log(
+ `Waiting ${remainingTime}ms to reach minimum 1s duration`
+ );
+ await new Promise((resolve) => setTimeout(resolve, remainingTime));
+ }
+
+ navigate("/wallet", { replace: true });
} catch (err) {
+ console.log("intializatiion error", err);
setHasError(err.message);
}
}
- if (!mnemoinc) return;
- if (didInitializeWalletRef.current) return;
- didInitializeWalletRef.current = true;
- retriveExternalData();
- }, [mnemoinc]);
+ if (preloadedUserData.isLoading && !preloadedUserData.data) return;
+ if (didRunConnectionRef.current) return;
+ didRunConnectionRef.current = true;
- useEffect(() => {
- if (
- Object.keys(masterInfoObject).length === 0 ||
- didLoadInformation.current ||
- Object.keys(globalContactsInformation).length === 0 ||
- !didOpenDatabases
- )
- return;
- didLoadInformation.current = true;
-
- initWallet(numberOfCachedTransactionsRef.current);
- }, [masterInfoObject, globalContactsInformation]);
+ startConnectProcess();
+ }, [preloadedUserData, masterInfoObject]);
return (
@@ -131,88 +155,4 @@ export default function LoadingScreen() {
/>
);
- async function initWallet(txs) {
- console.log("HOME RENDER BREEZ EVENT FIRST LOAD");
-
- try {
- setStartConnectingToSpark(true);
- console.log(txs, "CACHED TRANSACTIONS");
- setNumberOfCachedTxs(txs?.length || 0);
-
- const didSetLiquid = await setLiquidNodeInformationForSession();
-
- // Same thing for here, if liquid does not set continue on in the process
- if (didSetLiquid) {
- navigate("/wallet", { replace: true });
- } else
- throw new Error(
- "Wallet information was not set properly, please try again."
- );
- } catch (err) {
- setHasError(String(err.message));
- console.log(err, "homepage connection to node err");
- }
- }
-
- async function setupFiatCurrencies() {
- const currency = masterInfoObject.fiatCurrency;
-
- let fiatRate;
- try {
- fiatRate = await loadNewFiatData(
- currency,
- contactsPrivateKey,
- publicKey,
- masterInfoObject
- );
-
- if (!fiatRate.didWork) {
- // fallback API
- const response = await fetch(import.meta.env.FALLBACK_FIAT_PRICE_DATA);
- const data = await response.json();
- if (data[currency]?.["15m"]) {
- // ✅ 4. Store in new format
- Storage.setItem("didFetchFiatRateToday", {
- lastFetched: new Date().getTime(),
- fiatRate: {
- coin: currency,
- value: data[currency]?.["15m"],
- },
- });
- Storage.setItem("cachedBitcoinPrice", {
- coin: currency,
- value: data[currency]?.["15m"],
- });
-
- fiatRate = {
- coin: currency,
- value: data[currency]?.["15m"],
- };
- } else {
- fiatRate = {
- coin: currency,
- value: 100_000, // random number to make sure nothing else down the line errors out
- };
- }
- } else fiatRate = fiatRate.fiatRateResponse;
- } catch (error) {
- console.error("Failed to fetch fiat data:", error);
- return { coin: "USD", value: 100_000 };
- }
-
- return fiatRate;
- }
-
- async function setLiquidNodeInformationForSession() {
- try {
- const [fiat_rate] = await Promise.all([setupFiatCurrencies()]);
-
- toggleFiatStats(fiat_rate);
-
- return true;
- } catch (err) {
- console.log(err, "LIQUID INFORMATION ERROR");
- return false;
- }
- }
}
diff --git a/src/pages/login/login.jsx b/src/pages/login/login.jsx
index 3618892..ea4f870 100644
--- a/src/pages/login/login.jsx
+++ b/src/pages/login/login.jsx
@@ -9,8 +9,10 @@ import { Colors } from "../../constants/theme";
import { useThemeContext } from "../../contexts/themeContext";
import useThemeColors from "../../hooks/useThemeColors";
import ThemeText from "../../components/themeText/themeText";
+import { useOverlay } from "../../contexts/overlayContext";
-function Login({ openOverlay }) {
+function Login() {
+ const { openOverlay } = useOverlay();
const { theme, darkModeType } = useThemeContext();
const { backgroundOffset, textInputBackground, textInputColor, textColor } =
useThemeColors();
diff --git a/src/pages/receiveAmount/style.css b/src/pages/receiveAmount/style.css
index 54e29bb..a4a633c 100644
--- a/src/pages/receiveAmount/style.css
+++ b/src/pages/receiveAmount/style.css
@@ -42,7 +42,7 @@
.description-input {
width: 100%;
- padding: 15px;
+ padding: 10px;
border: unset;
border-radius: 8px;
}
diff --git a/src/pages/receiveQRPage/components/buttonContainer.jsx b/src/pages/receiveQRPage/components/buttonContainer.jsx
index 2a5345d..c2c209b 100644
--- a/src/pages/receiveQRPage/components/buttonContainer.jsx
+++ b/src/pages/receiveQRPage/components/buttonContainer.jsx
@@ -7,6 +7,7 @@ import CustomButton from "../../../components/customButton/customButton";
import { useThemeContext } from "../../../contexts/themeContext";
import { Colors } from "../../../constants/theme";
import useThemeColors from "../../../hooks/useThemeColors";
+import { useOverlay } from "../../../contexts/overlayContext";
export default function ReceiveButtonsContainer({
generatingInvoiceQRCode,
@@ -14,8 +15,8 @@ export default function ReceiveButtonsContainer({
receiveOption,
initialSendAmount,
description,
- openOverlay,
}) {
+ const { openOverlay } = useOverlay();
const navigate = useNavigate();
const location = useLocation();
const { theme, darkModeType } = useThemeContext();
diff --git a/src/pages/receiveQRPage/receiveQRPage.jsx b/src/pages/receiveQRPage/receiveQRPage.jsx
index 990b543..cc0dc56 100644
--- a/src/pages/receiveQRPage/receiveQRPage.jsx
+++ b/src/pages/receiveQRPage/receiveQRPage.jsx
@@ -11,17 +11,18 @@ import FormattedSatText from "../../components/formattedSatText/formattedSatText
import { useThemeContext } from "../../contexts/themeContext";
import useThemeColors from "../../hooks/useThemeColors";
import ThemeText from "../../components/themeText/themeText";
-import ThemeImage from "../../components/ThemeImage/themeImage";
import { useActiveCustodyAccount } from "../../contexts/activeAccount";
import { encodeLNURL } from "../../functions/lnurl/bench32Formmater";
import FullLoadingScreen from "../../components/fullLoadingScreen/fullLoadingScreen";
-import { aboutIcon, editIconLight } from "../../constants/icons";
import { useGlobalContextProvider } from "../../contexts/masterInfoObject";
import { useNodeContext } from "../../contexts/nodeContext";
import displayCorrectDenomination from "../../functions/displayCorrectDenomination";
import { Colors } from "../../constants/theme";
+import { useOverlay } from "../../contexts/overlayContext";
+import { Edit, Info } from "lucide-react";
-export default function ReceiveQRPage({ openOverlay }) {
+export default function ReceiveQRPage() {
+ const { openOverlay } = useOverlay();
const { masterInfoObject } = useGlobalContextProvider();
const { fiatStats } = useNodeContext();
const { globalContactsInformation } = useGlobalContacts();
@@ -81,18 +82,6 @@ export default function ReceiveQRPage({ openOverlay }) {
globalContactsInformation?.myProfile?.uniqueName,
]);
- useEffect(() => {
- if (selectedRecieveOption !== "bitcoin") return;
- // requestAnimationFrame(() => {
- // navigate("/error", {
- // state: {
- // errorMessage:
- // "Currently, on-chain payment addresses are single-use only...",
- // },
- // });
- // });
- }, [selectedRecieveOption, navigate]);
-
return (
)}
-
-
+
{selectedRecieveOption.toLowerCase() === "bitcoin" ? (
@@ -185,6 +180,7 @@ function LNURLContainer({
masterInfoObject,
fiatStats,
openOverlay,
+ darkModeType,
}) {
return (
@@ -229,14 +222,11 @@ function LNURLContainer({
/>
{selectedRecieveOption.toLowerCase() === "lightning" &&
!initialSendAmount && (
-
)}
diff --git a/src/pages/restoreWallet/restoreWallet.jsx b/src/pages/restoreWallet/restoreWallet.jsx
index 473c3aa..c55b908 100644
--- a/src/pages/restoreWallet/restoreWallet.jsx
+++ b/src/pages/restoreWallet/restoreWallet.jsx
@@ -16,6 +16,7 @@ import { Colors } from "../../constants/theme";
import { validateMnemonic } from "@scure/bip39";
import { wordlist } from "@scure/bip39/wordlists/english";
import { handleRestoreFromText } from "../../functions/seed";
+import { useOverlay } from "../../contexts/overlayContext";
const NUMARRAY = Array.from({ length: 12 }, (_, i) => i + 1);
const INITIAL_KEY_STATE = NUMARRAY.reduce((acc, num) => {
@@ -23,7 +24,8 @@ const INITIAL_KEY_STATE = NUMARRAY.reduce((acc, num) => {
return acc;
}, {});
-export default function RestoreWallet({ openOverlay }) {
+export default function RestoreWallet() {
+ const { openOverlay } = useOverlay();
const location = useLocation();
const params = location.state;
const navigate = useNavigate();
diff --git a/src/pages/sendPage/components/acceptButton.jsx b/src/pages/sendPage/components/acceptButton.jsx
index 952ded6..64925f1 100644
--- a/src/pages/sendPage/components/acceptButton.jsx
+++ b/src/pages/sendPage/components/acceptButton.jsx
@@ -28,7 +28,7 @@ export default function AcceptButtonSendPage({
sparkInformation,
seletctedToken,
isLRC20Payment,
- sendWebViewRequest,
+
openOverlay,
}) {
const { t } = useTranslation();
@@ -215,7 +215,6 @@ export default function AcceptButtonSendPage({
seletctedToken,
currentWalletMnemoinc,
t,
- sendWebViewRequest,
});
} catch (error) {
console.log("Accept button error:", error);
diff --git a/src/pages/sendPage/components/feeInfo.css b/src/pages/sendPage/components/feeInfo.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/pages/sendPage/components/feeInfo.jsx b/src/pages/sendPage/components/feeInfo.jsx
new file mode 100644
index 0000000..09f079b
--- /dev/null
+++ b/src/pages/sendPage/components/feeInfo.jsx
@@ -0,0 +1,38 @@
+import { useTranslation } from "react-i18next";
+
+import ThemeText from "../../../components/themeText/themeText";
+import FormattedSatText from "../../../components/formattedSatText/formattedSatText";
+
+export default function SendTransactionFeeInfo({
+ paymentFee,
+ isLightningPayment,
+ isLiquidPayment,
+ isBitcoinPayment,
+ isSparkPayment,
+}) {
+ const { t } = useTranslation();
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/src/pages/sendPage/components/invoiceInfo.css b/src/pages/sendPage/components/invoiceInfo.css
new file mode 100644
index 0000000..7390943
--- /dev/null
+++ b/src/pages/sendPage/components/invoiceInfo.css
@@ -0,0 +1,35 @@
+.invoiceContainer {
+ width: 80%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 8px;
+ border-radius: 8px;
+ margin-top: 30px;
+ cursor: pointer;
+ transition: opacity 0.2s;
+}
+
+.invoiceContainer .contactRow {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ margin: 0;
+ padding: 0;
+}
+
+.invoiceContainer .profileImage {
+ width: 40px;
+ height: 40px;
+ border-radius: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ margin-right: 10px;
+}
+
+.invoiceContainer .addressText {
+ flex-shrink: 1;
+ margin: 0;
+}
diff --git a/src/pages/sendPage/components/invoiceInfo.jsx b/src/pages/sendPage/components/invoiceInfo.jsx
new file mode 100644
index 0000000..efa518e
--- /dev/null
+++ b/src/pages/sendPage/components/invoiceInfo.jsx
@@ -0,0 +1,66 @@
+import { InputTypes } from "bitcoin-address-parser";
+import formatSparkPaymentAddress from "../../../functions/sendBitcoin/formatSparkPaymentAddress";
+import useThemeColors from "../../../hooks/useThemeColors";
+import { useOverlay } from "../../../contexts/overlayContext";
+import ThemeText from "../../../components/themeText/themeText";
+import ContactProfileImage from "../../contacts/components/profileImage/profileImage";
+import "./invoiceInfo.css";
+
+export default function InvoiceInfo({
+ paymentInfo,
+ fromPage,
+ contactInfo,
+ theme,
+ darkModeType,
+}) {
+ const formmateedSparkPaymentInfo = formatSparkPaymentAddress(paymentInfo);
+ const { backgroundOffset, backgroundColor } = useThemeColors();
+ const { openOverlay } = useOverlay();
+
+ return (
+ {
+ openOverlay({
+ for: "error",
+ errorMessage: formmateedSparkPaymentInfo.address,
+ });
+ }}
+ style={{
+ backgroundColor: backgroundOffset,
+ }}
+ >
+ {fromPage === "contacts" ? (
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/src/pages/sendPage/sendPage.jsx b/src/pages/sendPage/sendPage.jsx
index a1319b8..03c3bbc 100644
--- a/src/pages/sendPage/sendPage.jsx
+++ b/src/pages/sendPage/sendPage.jsx
@@ -18,26 +18,32 @@ import {
SMALLEST_ONCHAIN_SPARK_SEND_AMOUNT,
} from "../../constants";
import CustomInput from "../../components/customInput/customInput";
-import CustomNumberKeyboard from "../../components/customNumberKeyboard/customNumberKeyboard";
import NumberInputSendPage from "./components/numberInput";
import CustomButton from "../../components/customButton/customButton";
-import { getBoltzApiUrl } from "../../functions/boltz/boltzEndpoitns";
-
import displayCorrectDenomination from "../../functions/displayCorrectDenomination";
import FormattedSatText from "../../components/formattedSatText/formattedSatText";
import formatSparkPaymentAddress from "../../functions/sendBitcoin/formatSparkPaymentAddress";
import { useActiveCustodyAccount } from "../../contexts/activeAccount";
import { useTranslation } from "react-i18next";
import { InputTypes } from "bitcoin-address-parser";
-import ThemeImage from "../../components/ThemeImage/themeImage";
-import { adminHomeWallet } from "../../constants/icons";
import ThemeText from "../../components/themeText/themeText";
import SelectLRC20Token from "./components/selectLRC20Token";
import { formatTokensNumber } from "../../functions/lrc20/formatTokensBalance";
import CustomSettingsNavbar from "../../components/customSettingsNavbar";
import AcceptButtonSendPage from "./components/acceptButton";
+import { useOverlay } from "../../contexts/overlayContext";
+import NavBarWithBalance from "../../components/navBarWithBalance/navbarWithBalance";
+import {
+ handlePaymentUpdate,
+ publishMessage,
+} from "../../functions/messaging/publishMessage";
+import { useKeysContext } from "../../contexts/keysContext";
+import SendTransactionFeeInfo from "./components/feeInfo";
+import { useThemeContext } from "../../contexts/themeContext";
+import InvoiceInfo from "./components/invoiceInfo";
-export default function SendPage({ openOverlay }) {
+export default function SendPage() {
+ const { openOverlay } = useOverlay();
const location = useLocation();
const { sparkInformation } = useSpark();
const params = location.state || {};
@@ -49,10 +55,14 @@ export default function SendPage({ openOverlay }) {
comingFromAccept,
enteredPaymentInfo,
errorMessage: globalError,
+ contactInfo,
} = params;
+ console.log(params, "oi");
const [paymentInfo, setPaymentInfo] = useState({});
const { masterInfoObject, toggleMasterInfoObject } =
useGlobalContextProvider();
+ const { theme, darkModeType } = useThemeContext();
+ const { contactsPrivateKey, publicKey } = useKeysContext();
const { currentWalletMnemoinc } = useActiveCustodyAccount();
const { liquidNodeInformation, fiatStats } = useNodeContext();
const { minMaxLiquidSwapAmounts } = useAppStatus();
@@ -223,11 +233,34 @@ export default function SendPage({ openOverlay }) {
usingZeroAmountInvoice: paymentInfo.usingZeroAmountInvoice,
seletctedToken: selectedLRC20Asset,
mnemonic: currentWalletMnemoinc,
+ contactInfo,
+ fromMainSendScreen: true,
};
// Shouuld be same for all paymetns
const paymentResponse = await sparkPaymenWrapper(paymentObject);
if (paymentResponse.didWork) {
+ if (fromPage?.includes("contacts") && paymentResponse.response?.id) {
+ if (fromPage === "contacts-request") {
+ handlePaymentUpdate({
+ transaction: params.publishMessageFuncParams.transaction,
+ didPay: params.publishMessageFuncParams.didPay,
+ txid: paymentResponse.response?.id,
+ globalContactsInformation:
+ params.publishMessageFuncParams.globalContactsInformation,
+ selectedContact: params.publishMessageFuncParams.selectedContact,
+ currentTime: params.publishMessageFuncParams.currentTime,
+ contactsPrivateKey,
+ publicKey,
+ masterInfoObject,
+ });
+ } else {
+ const sendObject = params.publishMessageFuncParams;
+ sendObject.data.txid = paymentResponse.response?.id;
+ console.log(sendObject);
+ publishMessage(sendObject);
+ }
+ }
navigate("/confirm-page", {
state: {
for: "paymentsucceed",
@@ -416,40 +449,7 @@ export default function SendPage({ openOverlay }) {
return (
-
+
@@ -488,18 +488,24 @@ export default function SendPage({ openOverlay }) {
{!canEditPaymentAmount && (
<>
-
-
>
)}
+ {!canEditPaymentAmount && (
+
+ )}
{canEditPaymentAmount && (
<>
diff --git a/src/pages/settings/components/settingsItemWithSlider/settingsItemsWithSlider.jsx b/src/pages/settings/components/settingsItemWithSlider/settingsItemsWithSlider.jsx
index b3f1492..407b79a 100644
--- a/src/pages/settings/components/settingsItemWithSlider/settingsItemsWithSlider.jsx
+++ b/src/pages/settings/components/settingsItemWithSlider/settingsItemsWithSlider.jsx
@@ -5,9 +5,8 @@ import useThemeColors from "../../../../hooks/useThemeColors";
import { Colors } from "../../../../constants/theme";
import ThemeText from "../../../../components/themeText/themeText";
import CustomToggleSwitch from "../../../../components/switch/switch";
-import ThemeImage from "../../../../components/ThemeImage/themeImage";
-import { aboutIcon } from "../../../../constants/icons";
import FullLoadingScreen from "../../../../components/fullLoadingScreen/fullLoadingScreen";
+import { Info } from "lucide-react";
export default function SettingsItemWithSlider({
settingsTitle = "",
@@ -23,7 +22,7 @@ export default function SettingsItemWithSlider({
containerStyles = {},
openOverlay,
}) {
- const { theme } = useThemeContext();
+ const { theme, darkModeType } = useThemeContext();
const { backgroundOffset, backgroundColor, textColor } = useThemeColors();
const goToInformationPopup = () => {
@@ -72,7 +71,14 @@ export default function SettingsItemWithSlider({
onClick={goToInformationPopup}
className="info-button"
>
-
+
)}
diff --git a/src/pages/settings/pages/about/about.jsx b/src/pages/settings/pages/about/about.jsx
index 9e532de..8bc1d8a 100644
--- a/src/pages/settings/pages/about/about.jsx
+++ b/src/pages/settings/pages/about/about.jsx
@@ -127,7 +127,7 @@ export default function AboutPage() {
textContent={"Oliver Koblizek"}
/>
-
Version 0.1.3
+
Version 0.1.4
);
}
diff --git a/src/pages/settings/pages/currency/displayCurrency.jsx b/src/pages/settings/pages/currency/displayCurrency.jsx
index d2a6348..41536ca 100644
--- a/src/pages/settings/pages/currency/displayCurrency.jsx
+++ b/src/pages/settings/pages/currency/displayCurrency.jsx
@@ -14,8 +14,10 @@ import ThemeText from "../../../../components/themeText/themeText";
import { fiatCurrencies } from "../../../../functions/currencyOptions";
import { useKeysContext } from "../../../../contexts/keysContext";
import loadNewFiatData from "../../../../functions/saveAndUpdateFiatData";
+import { useOverlay } from "../../../../contexts/overlayContext";
-export default function DisplayCurrency({ openOverlay }) {
+export default function DisplayCurrency() {
+ const { openOverlay } = useOverlay();
const { masterInfoObject, toggleMasterInfoObject } =
useGlobalContextProvider();
const { contactsPrivateKey, publicKey } = useKeysContext();
diff --git a/src/pages/settings/pages/exploreUsers/exploreUsers.jsx b/src/pages/settings/pages/exploreUsers/exploreUsers.jsx
index c836676..a281963 100644
--- a/src/pages/settings/pages/exploreUsers/exploreUsers.jsx
+++ b/src/pages/settings/pages/exploreUsers/exploreUsers.jsx
@@ -145,7 +145,10 @@ export default function ExploreUsers() {
if (masterInfoObject.exploreData) return;
const pastExploreData = Storage.getItem("savedExploreData");
- const shouldLoadExporeDataResp = shouldLoadExploreData(pastExploreData);
+ const shouldLoadExporeDataResp = shouldLoadExploreData(
+ pastExploreData,
+ currentTime
+ );
if (!shouldLoadExporeDataResp) {
toggleMasterInfoObject({ exploreData: pastExploreData.data });
diff --git a/src/pages/settings/pages/fastPay/fastPay.jsx b/src/pages/settings/pages/fastPay/fastPay.jsx
index 85d263c..5ddbd9c 100644
--- a/src/pages/settings/pages/fastPay/fastPay.jsx
+++ b/src/pages/settings/pages/fastPay/fastPay.jsx
@@ -6,8 +6,10 @@ import TextInputWithSliderSettingsItem from "../../components/textInputWithSlide
import "./fastPay.css";
import { useThemeContext } from "../../../../contexts/themeContext";
import useThemeColors from "../../../../hooks/useThemeColors";
+import { useOverlay } from "../../../../contexts/overlayContext";
-export default function FastPay({ openOverlay }) {
+export default function FastPay() {
+ const { openOverlay } = useOverlay();
const { masterInfoObject, toggleMasterInfoObject } =
useGlobalContextProvider();
const location = useLocation();
diff --git a/src/pages/settings/pages/sparkInfo/sparkInfo.jsx b/src/pages/settings/pages/sparkInfo/sparkInfo.jsx
index 3a8aa4e..1a0c337 100644
--- a/src/pages/settings/pages/sparkInfo/sparkInfo.jsx
+++ b/src/pages/settings/pages/sparkInfo/sparkInfo.jsx
@@ -6,15 +6,16 @@ import useThemeColors from "../../../../hooks/useThemeColors";
import { useThemeContext } from "../../../../contexts/themeContext";
import { Colors } from "../../../../constants/theme";
import ThemeText from "../../../../components/themeText/themeText";
-import ThemeImage from "../../../../components/ThemeImage/themeImage";
-import { clipboardBlue } from "../../../../constants/icons";
+import { useOverlay } from "../../../../contexts/overlayContext";
+import { ClipboardIcon } from "lucide-react";
-export default function SparkInformation({ openOverlay }) {
+export default function SparkInformation() {
+ const { openOverlay } = useOverlay();
const { sparkInformation } = useSpark();
const navigate = useNavigate();
const location = useLocation();
const { backgroundOffset } = useThemeColors();
- const { theme } = useThemeContext();
+ const { theme, darkModeType } = useThemeContext();
return (
@@ -40,15 +41,19 @@ export default function SparkInformation({ openOverlay }) {
className="techicalData"
>
-
@@ -65,15 +70,19 @@ export default function SparkInformation({ openOverlay }) {
className="techicalData"
>
-
diff --git a/src/pages/settings/pages/sparkSettingsPage/index.jsx b/src/pages/settings/pages/sparkSettingsPage/index.jsx
index bda8d5b..fb371a4 100644
--- a/src/pages/settings/pages/sparkSettingsPage/index.jsx
+++ b/src/pages/settings/pages/sparkSettingsPage/index.jsx
@@ -6,8 +6,10 @@ import SettingsItemWithSlider from "../../components/settingsItemWithSlider/sett
import displayCorrectDenomination from "../../../../functions/displayCorrectDenomination";
import { useNodeContext } from "../../../../contexts/nodeContext";
import { useSpark } from "../../../../contexts/sparkContext";
+import { useOverlay } from "../../../../contexts/overlayContext";
-export default function SparkSettingsPage({ openOverlay }) {
+export default function SparkSettingsPage() {
+ const { openOverlay } = useOverlay();
const { sparkInformation } = useSpark();
const { masterInfoObject, toggleMasterInfoObject } =
useGlobalContextProvider();
diff --git a/src/pages/settings/settings.jsx b/src/pages/settings/settings.jsx
index f6cc820..03e1dec 100644
--- a/src/pages/settings/settings.jsx
+++ b/src/pages/settings/settings.jsx
@@ -5,71 +5,52 @@ import PageNavBar from "../../components/navBar/navBar";
import ThemeText from "../../components/themeText/themeText";
import { useEffect } from "react";
import SocialOptionsBottomBar from "./socialOptions/socialOptions";
-import Icon from "../../components/customIcon/customIcon";
import { Colors } from "../../constants/theme";
-import ThemeImage from "../../components/ThemeImage/themeImage";
import { useThemeContext } from "../../contexts/themeContext";
import useThemeColors from "../../hooks/useThemeColors";
-import {
- aboutIcon,
- colorIcon,
- contactsIconBlue,
- currencyIcon,
- keyIcon,
- leftCheveronIcon,
- navigationIcon,
- nodeIcon,
- receiptIcon,
- trashIcon,
-} from "../../constants/icons";
+import { useOverlay } from "../../contexts/overlayContext";
+import * as LucidIcons from "lucide-react";
+import CustomSettingsNavBar from "../../components/customSettingsNavbar";
const GENERALOPTIONS = [
{
for: "general",
name: "About",
- icon: aboutIcon,
- arrowIcon: leftCheveronIcon,
+ newIconName: "Info",
},
{
for: "general",
name: "Display Currency",
- icon: currencyIcon,
- arrowIcon: leftCheveronIcon,
+ newIconName: "Coins",
},
{
for: "general",
name: "Display Options",
- icon: colorIcon,
- arrowIcon: leftCheveronIcon,
+ newIconName: "Palette",
},
{
for: "general",
name: "Edit Contact Profile",
- icon: contactsIconBlue,
- arrowIcon: leftCheveronIcon,
+ newIconName: "UserPen",
},
{
for: "general",
name: "Fast Pay",
- svgIcon: true,
- svgName: "quickPayIcon",
- arrowIcon: leftCheveronIcon,
+ newIconName: "ClockFading",
},
{
for: "general",
name: "Blitz Stats",
- svgName: "crashDebugIcon",
- icon: navigationIcon,
- arrowIcon: leftCheveronIcon,
+ newIconName: "ChartArea",
},
];
+
const SECURITYOPTIONS = [
{
for: "Security & Customization",
name: "Backup wallet",
- icon: keyIcon,
- arrowIcon: leftCheveronIcon,
+ newIconName: "KeyRound",
},
];
@@ -77,20 +58,17 @@ const ADVANCEDOPTIONS = [
{
for: "Closing Account",
name: "Blitz Fee Details",
- icon: receiptIcon,
- arrowIcon: leftCheveronIcon,
+ newIconName: "ReceiptText",
},
{
for: "Closing Account",
name: "Delete Wallet",
- icon: trashIcon,
- arrowIcon: leftCheveronIcon,
+ newIconName: "Trash",
},
{
for: "Closing Account",
name: "Spark Info",
- icon: nodeIcon,
- arrowIcon: leftCheveronIcon,
+ newIconName: "Asterisk",
},
];
const SETTINGSOPTIONS = [
@@ -103,21 +81,20 @@ const DOOMSDAYSETTINGS = [
{
for: "Security & Customization",
name: "Backup wallet",
- icon: keyIcon,
- arrowIcon: leftCheveronIcon,
+ newIconName: "KeyRound",
},
],
[
{
for: "Closing Account",
name: "Delete Wallet",
- icon: trashIcon,
- arrowIcon: leftCheveronIcon,
+ newIconName: "Trash",
},
],
];
-export default function SettingsHome({ openOverlay }) {
+export default function SettingsHome() {
+ const { openOverlay } = useOverlay();
const navigate = useNavigate();
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
@@ -138,6 +115,8 @@ export default function SettingsHome({ openOverlay }) {
const settingsItems = settignsList.map((item, id) => {
const internalElements = item.map((settingsElement, id) => {
+ const IconElemnt = LucidIcons[settingsElement.newIconName];
+ console.log(IconElemnt, settingsElement.newIconName);
return (
- {settingsElement.svgIcon ? (
-
- ) : (
-
- )}
+
-
);
@@ -210,7 +181,7 @@ export default function SettingsHome({ openOverlay }) {
return (
-
+
{settingsItems}
diff --git a/src/pages/settings/settingsItem/settingsItem.jsx b/src/pages/settings/settingsItem/settingsItem.jsx
index a13d4dc..a834800 100644
--- a/src/pages/settings/settingsItem/settingsItem.jsx
+++ b/src/pages/settings/settingsItem/settingsItem.jsx
@@ -12,8 +12,10 @@ import FastPay from "../pages/fastPay/fastPay";
import BlitzFeeInformation from "../pages/feeDetails/feeInformation";
import ExploreUsers from "../pages/exploreUsers/exploreUsers";
import CustomSettingsNavbar from "../../../components/customSettingsNavbar";
+import { useOverlay } from "../../../contexts/overlayContext";
-export default function SettingsContentIndex({ openOverlay }) {
+export default function SettingsContentIndex() {
+ const { openOverlay } = useOverlay();
const location = useLocation();
const props = location.state;
const selectedPage = props.for?.toLowerCase();
@@ -22,6 +24,15 @@ export default function SettingsContentIndex({ openOverlay }) {
if (selectedPage === "point-of-sale") {
return <>{selectedPage === "point-of-sale" &&
}>;
}
+
+ if (selectedPage === "edit contact profile") {
+ return (
+
+ );
+ }
+
return (
}
{selectedPage === "display options" && }
- {selectedPage === "edit contact profile" && (
-
- )}
-
{selectedPage === "fast pay" && }
{selectedPage === "blitz stats" && }
diff --git a/src/pages/settings/socialOptions/socialOptions.jsx b/src/pages/settings/socialOptions/socialOptions.jsx
index 3960937..1138a8d 100644
--- a/src/pages/settings/socialOptions/socialOptions.jsx
+++ b/src/pages/settings/socialOptions/socialOptions.jsx
@@ -1,9 +1,6 @@
import telegramIcon from "../../../assets/telegram.png";
-import telegramIconWhite from "../../../assets/telegramWhite.png";
import twitter from "../../../assets/twitter.png";
-import twitterIconWhite from "../../../assets/twitterWhite.png";
import github from "../../../assets/github.png";
-import githubIconWhite from "../../../assets/githubWhite.png";
import "./socialOptions.css";
import ThemeImage from "../../../components/ThemeImage/themeImage";
export default function SocialOptionsBottomBar() {
diff --git a/src/pages/switchReceiveOption/switchReceiveOption.jsx b/src/pages/switchReceiveOption/switchReceiveOption.jsx
index dcac628..24c280c 100644
--- a/src/pages/switchReceiveOption/switchReceiveOption.jsx
+++ b/src/pages/switchReceiveOption/switchReceiveOption.jsx
@@ -4,7 +4,6 @@ import {
blockstreamLiquid,
lightningReceiveIcon,
rootstockLogo,
- smallArrowLeft,
sparkAsteriskWhite,
} from "../../constants/icons";
import ThemeText from "../../components/themeText/themeText";
@@ -19,7 +18,9 @@ import { useKeysContext } from "../../contexts/keysContext";
import { useNavigate } from "react-router-dom";
import displayCorrectDenomination from "../../functions/displayCorrectDenomination";
import { useNodeContext } from "../../contexts/nodeContext";
-import ThemeImage from "../../components/ThemeImage/themeImage";
+import { useOverlay } from "../../contexts/overlayContext";
+import { ArrowDown } from "lucide-react";
+import { Colors } from "../../constants/theme";
const MAIN_PAYMENTS = [
["Lightning", "Instant"],
@@ -29,7 +30,9 @@ const MAIN_PAYMENTS = [
// ["Rootstock", "~ 1 minute"],
];
-export default function SwitchReceiveOption({ onClose, params, openOverlay }) {
+export default function SwitchReceiveOption({ params }) {
+ const { openOverlay, closeOverlay } = useOverlay();
+ const onClose = closeOverlay;
const navigate = useNavigate();
const { fiatStats } = useNodeContext();
const { currentWalletMnemonic } = useActiveCustodyAccount();
@@ -248,16 +251,16 @@ export default function SwitchReceiveOption({ onClose, params, openOverlay }) {
}
)}
/>
-
diff --git a/src/pages/technicalDetails/technicalDetails.jsx b/src/pages/technicalDetails/technicalDetails.jsx
index 1442163..8884cb4 100644
--- a/src/pages/technicalDetails/technicalDetails.jsx
+++ b/src/pages/technicalDetails/technicalDetails.jsx
@@ -3,7 +3,9 @@ import BackArrow from "../../components/backArrow/backArrow";
import ThemeText from "../../components/themeText/themeText";
import copyToClipboard from "../../functions/copyToClipboard";
import "./style.css";
-export default function TechnicalDetailsPage({ openOverlay }) {
+import { useOverlay } from "../../contexts/overlayContext";
+export default function TechnicalDetailsPage() {
+ const { openOverlay } = useOverlay();
const location = useLocation();
const navigate = useNavigate();
const props = location.state;
diff --git a/src/pages/viewkey/viewKey.jsx b/src/pages/viewkey/viewKey.jsx
index a9e0975..9d975e9 100644
--- a/src/pages/viewkey/viewKey.jsx
+++ b/src/pages/viewkey/viewKey.jsx
@@ -12,8 +12,10 @@ import copyToClipboard from "../../functions/copyToClipboard";
import useThemeColors from "../../hooks/useThemeColors";
import ThemeText from "../../components/themeText/themeText";
import { useThemeContext } from "../../contexts/themeContext";
+import { useOverlay } from "../../contexts/overlayContext";
-export default function ViewMnemoinc({ openOverlay }) {
+export default function ViewMnemoinc() {
+ const { openOverlay } = useOverlay();
const navigate = useNavigate();
const location = useLocation();
const props = location.state;
diff --git a/src/pages/wallet/components/balanceContainer/userBalanceContainer.jsx b/src/pages/wallet/components/balanceContainer/userBalanceContainer.jsx
index dd1f48c..e5825a7 100644
--- a/src/pages/wallet/components/balanceContainer/userBalanceContainer.jsx
+++ b/src/pages/wallet/components/balanceContainer/userBalanceContainer.jsx
@@ -43,6 +43,7 @@ export default function UserBalance() {
diff --git a/src/pages/wallet/components/lrc20Assets/index.jsx b/src/pages/wallet/components/lrc20Assets/index.jsx
index 9c9b26b..53524b2 100644
--- a/src/pages/wallet/components/lrc20Assets/index.jsx
+++ b/src/pages/wallet/components/lrc20Assets/index.jsx
@@ -1,12 +1,8 @@
import React, { useMemo, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
-import { useNavigate } from "react-router-dom";
-
import { useTranslation } from "react-i18next";
-import "./style.css"; // ✅ External CSS
+import "./style.css";
import ThemeText from "../../../../components/themeText/themeText";
-import ThemeImage from "../../../../components/ThemeImage/themeImage";
-import { smallArrowLeft } from "../../../../constants/icons";
import { formatTokensNumber } from "../../../../functions/lrc20/formatTokensBalance";
import {
getContrastingTextColor,
@@ -18,8 +14,11 @@ import CustomInput from "../../../../components/customInput/customInput";
import formatBalanceAmount from "../../../../functions/formatNumber";
import { useSpark } from "../../../../contexts/sparkContext";
import { Colors } from "../../../../constants/theme";
+import { useOverlay } from "../../../../contexts/overlayContext";
+import { ArrowLeft } from "lucide-react";
-export default function LRC20Assets({ openOverlay }) {
+export default function LRC20Assets() {
+ const { openOverlay } = useOverlay();
const { darkModeType, theme } = useThemeContext();
const { sparkInformation } = useSpark();
const { textColor } = useThemeColors();
@@ -132,9 +131,11 @@ export default function LRC20Assets({ openOverlay }) {
animate={{ rotate: isExpanded ? 90 : -90 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
>
-
diff --git a/src/pages/wallet/components/nav/nav.css b/src/pages/wallet/components/nav/nav.css
index 7b624ca..d01e2cf 100644
--- a/src/pages/wallet/components/nav/nav.css
+++ b/src/pages/wallet/components/nav/nav.css
@@ -1,6 +1,5 @@
.walletNavBar {
width: 100%;
- padding-bottom: 10px;
min-height: 40px;
display: flex;
justify-content: space-between;
@@ -9,11 +8,6 @@
z-index: 99;
align-items: center;
}
-.walletNavBar img {
- width: 30px;
- height: 30px;
- cursor: pointer;
-}
.walletNavBar .themeContainer {
display: flex;
diff --git a/src/pages/wallet/components/nav/nav.jsx b/src/pages/wallet/components/nav/nav.jsx
index 86a7b8c..9fa5198 100644
--- a/src/pages/wallet/components/nav/nav.jsx
+++ b/src/pages/wallet/components/nav/nav.jsx
@@ -4,45 +4,46 @@ import { useCallback, useState } from "react";
import { useSpark } from "../../../../contexts/sparkContext";
import { fullRestoreSparkState } from "../../../../functions/spark/restore";
import { useThemeContext } from "../../../../contexts/themeContext";
-import ThemeImage from "../../../../components/ThemeImage/themeImage";
import useThemeColors from "../../../../hooks/useThemeColors";
import { useActiveCustodyAccount } from "../../../../contexts/activeAccount";
-import {
- darkMode,
- lightMode,
- lightModeWhite,
- refresh,
- refreshWhite,
- settingsIcon,
- settingsWhite,
-} from "../../../../constants/icons";
import { Moon, Sun, RefreshCw, Settings } from "lucide-react";
+import NavBarProfileImage from "../../../../components/navBar/profileImage";
+import { useOverlay } from "../../../../contexts/overlayContext";
+import {
+ SPARK_TX_UPDATE_ENVENT_NAME,
+ sparkTransactionsEventEmitter,
+} from "../../../../functions/spark/transactions";
-export default function WalletNavBar({ openOverlay, didEnabledLrc20 }) {
- const navigate = useNavigate();
- const location = useLocation();
+export default function WalletNavBar({ didEnabledLrc20 }) {
const { theme, toggleTheme, darkModeType } = useThemeContext();
- const { backgroundColor } = useThemeColors();
+ const { backgroundColor, backgroundOffset } = useThemeColors();
const [isRefreshing, setIsRefreshing] = useState(false);
- const { sparkInformation } = useSpark();
+ const { sparkInformation, isSendingPaymentRef } = useSpark();
const { currentWalletMnemoinc } = useActiveCustodyAccount();
const handleRefresh = useCallback(async () => {
- setIsRefreshing(true);
-
- await fullRestoreSparkState({
- sparkAddress: sparkInformation.sparkAddress,
- batchSize: 5,
- isSendingPayment: false,
- mnemonic: currentWalletMnemoinc,
- identityPubKey: sparkInformation.identityPubKey,
- });
+ try {
+ setIsRefreshing(true);
- setIsRefreshing(false);
- openOverlay({
- for: "error",
- errorMessage: "Your wallet was successfully refreshed.",
- });
- }, []);
+ const response = await fullRestoreSparkState({
+ sparkAddress: sparkInformation.sparkAddress,
+ batchSize: 2,
+ isSendingPayment: isSendingPaymentRef.current,
+ mnemonic: currentWalletMnemoinc,
+ identityPubKey: sparkInformation.identityPubKey,
+ isInitialRestore: false,
+ });
+ if (!response) {
+ sparkTransactionsEventEmitter.emit(
+ SPARK_TX_UPDATE_ENVENT_NAME,
+ "fullUpdate"
+ );
+ }
+ } catch (err) {
+ console.log(err);
+ } finally {
+ setIsRefreshing(false);
+ }
+ }, [sparkInformation, currentWalletMnemoinc]);
return (
toggleTheme(!theme)}>
@@ -64,11 +65,7 @@ export default function WalletNavBar({ openOverlay, didEnabledLrc20 }) {
className={`${isRefreshing ? "spinningAnimation" : ""}`}
/>
-
navigate("/settings")}
- />
+
);
}
diff --git a/src/pages/wallet/components/sendAndRequestBTNS/sendAndRequstBtns.jsx b/src/pages/wallet/components/sendAndRequestBTNS/sendAndRequstBtns.jsx
index a98655b..32ed1e9 100644
--- a/src/pages/wallet/components/sendAndRequestBTNS/sendAndRequstBtns.jsx
+++ b/src/pages/wallet/components/sendAndRequestBTNS/sendAndRequstBtns.jsx
@@ -1,14 +1,13 @@
import { useNavigate } from "react-router-dom";
-import sendRequestImage from "../../../../assets/sendRequestImage.png";
-import Qr from "../../../../assets/scanQRCodeLight.png";
import "./style.css";
-import ThemeImage from "../../../../components/ThemeImage/themeImage";
import { Colors } from "../../../../constants/theme";
import { useThemeContext } from "../../../../contexts/themeContext";
import useThemeColors from "../../../../hooks/useThemeColors";
import { ArrowDown, ArrowUp, ScanLine } from "lucide-react";
+import { useOverlay } from "../../../../contexts/overlayContext";
-export default function SendAndRequestBtns({ openOverlay }) {
+export default function SendAndRequestBtns() {
+ const { openOverlay } = useOverlay();
const { theme, darkModeType } = useThemeContext();
const naigate = useNavigate();
const { backgroundOffset, backgroundColor } = useThemeColors();
@@ -36,11 +35,6 @@ export default function SendAndRequestBtns({ openOverlay }) {
}
size={25}
/>
- {/*
*/}
{
- const darkIcon =
- item === "img"
- ? ImagesIconDark
- : item === "clipboard"
- ? clipboardDark
- : editIcon;
+ const iconName =
+ item === "img" ? "Image" : item === "clipboard" ? "Clipboard" : "Edit";
+
+ const IconElement = LucidIcons[iconName];
const itemText =
item === "img"
@@ -101,25 +94,11 @@ export default function HalfModalSendOptions({ openOverlay, onClose }) {
}}
>
- {item === "manual" ? (
-
-
-
- ) : (
-
- )}
+
@@ -130,10 +109,11 @@ export default function HalfModalSendOptions({ openOverlay, onClose }) {
{sendOptionElements}
- {/* {decodedAddedContacts.length !== 0 && (
+ {decodedAddedContacts.length !== 0 && (
- )} */}
+ )}
{
+ openOverlay({
+ for: "informationPopup",
+ textContent: t(
+ "wallet.homeLightning.manualEnterSendAddress.paymentTypesDesc"
+ ),
+ buttonText: t("constants.understandText"),
+ });
+ };
+
return (
-
-
{
- openOverlay({
- for: "informationPopup",
- textContent: t(
- "wallet.homeLightning.manualEnterSendAddress.paymentTypesDesc"
- ),
- buttonText: t("constants.understandText"),
- });
- }}
- className="image"
- styles={{ width: 20, height: 20 }}
- icon={aboutIcon}
+
diff --git a/src/pages/wallet/wallet.css b/src/pages/wallet/wallet.css
index e25415c..315b270 100644
--- a/src/pages/wallet/wallet.css
+++ b/src/pages/wallet/wallet.css
@@ -1,16 +1,24 @@
#walletHomeContainer {
width: 100%;
height: 100%;
+ /* max-width: 800px; */
padding-bottom: 65px;
overflow-y: scroll;
margin: 0 auto;
}
-
+.customWalletHomeStyle {
+ max-width: calc(800px * 1.06);
+}
.lrc20Overlay {
- padding: 20px 2.5% 0;
+ padding: 20px 0 0;
border-bottom-left-radius: 30px;
border-bottom-right-radius: 30px;
}
+.lrc20Overlay .lrc20ContentContiner {
+ width: 95%;
+ max-width: 800px;
+ margin: 0 auto;
+}
.txsContainer {
width: 90%;
margin: 20px auto 15px;
diff --git a/src/pages/wallet/wallet.jsx b/src/pages/wallet/wallet.jsx
index c072df6..5fb4d5a 100644
--- a/src/pages/wallet/wallet.jsx
+++ b/src/pages/wallet/wallet.jsx
@@ -9,8 +9,10 @@ import { useGlobalContextProvider } from "../../contexts/masterInfoObject";
import useThemeColors from "../../hooks/useThemeColors";
import SafeAreaComponent from "../../components/safeAreaContainer";
import LRC20Assets from "./components/lrc20Assets";
+import { useOverlay } from "../../contexts/overlayContext";
-export default function WalletHome({ openOverlay }) {
+export default function WalletHome() {
+ const { openOverlay } = useOverlay();
const { masterInfoObject } = useGlobalContextProvider();
const { backgroundColor, backgroundOffset } = useThemeColors();
const { toggleDidGetToHomepage } = useAppStatus();
@@ -20,9 +22,10 @@ export default function WalletHome({ openOverlay }) {
useEffect(() => {
toggleDidGetToHomepage(true);
}, []);
- console.log(masterInfoObject);
+
return (
@@ -33,13 +36,15 @@ export default function WalletHome({ openOverlay }) {
}}
className="lrc20Overlay"
>
-
-
-
- {didEnabledLrc20 && }
+
+
+
+
+ {didEnabledLrc20 && }
+
diff --git a/src/tabs/tabs.jsx b/src/tabs/tabs.jsx
index 6a0f850..0156247 100644
--- a/src/tabs/tabs.jsx
+++ b/src/tabs/tabs.jsx
@@ -29,10 +29,10 @@ export default function BottomTabs({ setValue, value, Link }) {