From b72cc6e395f0f196b4858fd37b3e63381db6c2b8 Mon Sep 17 00:00:00 2001 From: Dread <34528298+islandbitcoin@users.noreply.github.com> Date: Mon, 14 Jul 2025 20:33:09 -0400 Subject: [PATCH 01/95] add transfer feature mvp (topup with card) --- app/components/home-screen/Buttons.tsx | 5 + app/i18n/en/index.ts | 44 ++- app/i18n/i18n-types.ts | 298 +++++++++++++++++- app/i18n/raw-i18n/source/en.json | 50 ++- app/navigation/root-navigator.tsx | 30 ++ app/navigation/stack-param-lists.ts | 16 + app/screens/index.ts | 1 + .../card-payment-screen.tsx | 222 +++++++++++++ .../card-payment-screen/index.ts | 1 + .../fygaro-webview-screen.tsx | 180 +++++++++++ .../fygaro-webview-screen/index.ts | 1 + app/screens/transfer-screen/index.ts | 5 + .../payment-success-screen/index.ts | 1 + .../payment-success-screen.tsx | 155 +++++++++ .../transfer-screen/topup-screen/index.ts | 1 + .../topup-screen/topup-screen.tsx | 122 +++++++ .../transfer-screen/transfer-screen.tsx | 122 +++++++ ios/LNFlash.xcodeproj/project.pbxproj | 48 +-- ios/Podfile.lock | 138 ++++---- 19 files changed, 1337 insertions(+), 103 deletions(-) create mode 100644 app/screens/transfer-screen/card-payment-screen/card-payment-screen.tsx create mode 100644 app/screens/transfer-screen/card-payment-screen/index.ts create mode 100644 app/screens/transfer-screen/fygaro-webview-screen/fygaro-webview-screen.tsx create mode 100644 app/screens/transfer-screen/fygaro-webview-screen/index.ts create mode 100644 app/screens/transfer-screen/index.ts create mode 100644 app/screens/transfer-screen/payment-success-screen/index.ts create mode 100644 app/screens/transfer-screen/payment-success-screen/payment-success-screen.tsx create mode 100644 app/screens/transfer-screen/topup-screen/index.ts create mode 100644 app/screens/transfer-screen/topup-screen/topup-screen.tsx create mode 100644 app/screens/transfer-screen/transfer-screen.tsx diff --git a/app/components/home-screen/Buttons.tsx b/app/components/home-screen/Buttons.tsx index 61d274f40..f8f343bbe 100644 --- a/app/components/home-screen/Buttons.tsx +++ b/app/components/home-screen/Buttons.tsx @@ -58,6 +58,11 @@ const Buttons: React.FC = ({ setModalVisible, setDefaultAccountModalVisib target: "receiveBitcoin", icon: "down", }, + { + title: LL.HomeScreen.transfer(), + target: "transfer", + icon: "swap", + }, ] if (persistentState.isAdvanceMode) { diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index 56b4f62f8..2c3327229 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -537,6 +537,7 @@ const en: BaseTranslation = { balance: "Refresh Balance", showQrCode: "Topup via QR", send: "Send", + transfer: "Transfer", sweep: "Sweep to Wallet", pay: "Pay", title: "Home", @@ -565,6 +566,43 @@ const en: BaseTranslation = { backupTitle: "Backup your BTC wallet", backupDesc: "Backup and secure your Bitcoin wallet using recovery phrase." }, + TopUpScreen: { + title: "Top Up", + bankTransfer: "Bank Transfer", + bankTransferDesc: "Transfer funds from your bank account", + debitCreditCard: "Debit/Credit Card", + debitCreditCardDesc: "Pay with your card via Fygaro" + }, + CardPaymentScreen: { + title: "Card Payment", + email: "Email", + emailPlaceholder: "Enter your email address", + wallet: "Wallet", + walletPlaceholder: "Select wallet", + amount: "Amount (USD)", + amountPlaceholder: "Enter amount", + continue: "Continue", + usdWallet: "USD Wallet", + btcWallet: "BTC Wallet", + invalidEmail: "Please enter a valid email address", + invalidAmount: "Please enter a valid amount", + minimumAmount: "Minimum amount is $1.00" + }, + FygaroWebViewScreen: { + title: "Fygaro Payment", + loading: "Loading payment page...", + error: "Failed to load payment page", + retry: "Retry" + }, + PaymentSuccessScreen: { + title: "Payment Successful", + successMessage: "Your payment has been processed successfully", + amountSent: "Amount Sent", + depositedTo: "Deposited to", + transactionId: "Transaction ID", + done: "Done", + viewTransaction: "View Transaction" + }, PinScreen: { attemptsRemaining: "Incorrect PIN. {attemptsRemaining: number} attempts remaining.", oneAttemptRemaining: "Incorrect PIN. 1 attempt remaining.", @@ -1085,6 +1123,10 @@ const en: BaseTranslation = { TransferScreen: { title: "Transfer", percentageToConvert: "% to convert", + topUp: "Top Up", + topUpDesc: "Add funds to your wallet", + settle: "Settle", + settleDesc: "Settle pending transactions" }, UpgradeAccountModal: { title: "Upgrade your account", @@ -1336,7 +1378,7 @@ const en: BaseTranslation = { email: "Email", enjoyingApp: "Enjoying the app?", statusPage: "Status Page", - //telegram: "Telegram", + // telegram: "Telegram", discord: "Discord", mattermost: "Mattermost", thankYouText: "Thank you for the feedback, would you like to suggest an improvement?", diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index 46b2ebf99..fc3ed5393 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -332,7 +332,7 @@ type RootTranslation = { */ youreConverting: string /** - * Sending account + * S​e​n​d​i​n​g​ ​a​c​c​o​u​n​t */ sendingAccount: string /** @@ -340,7 +340,7 @@ type RootTranslation = { */ receivingAccount: string /** - * Conversion Fee + * C​o​n​v​e​r​s​i​o​n​ ​F​e​e */ conversionFee: string } @@ -1667,6 +1667,10 @@ type RootTranslation = { * S​e​n​d */ send: string + /** + * T​r​a​n​s​f​e​r + */ + transfer: string /** * S​w​e​e​p​ ​t​o​ ​W​a​l​l​e​t */ @@ -1777,6 +1781,130 @@ type RootTranslation = { */ backupDesc: string } + TopUpScreen: { + /** + * T​o​p​ ​U​p + */ + title: string + /** + * B​a​n​k​ ​T​r​a​n​s​f​e​r + */ + bankTransfer: string + /** + * T​r​a​n​s​f​e​r​ ​f​u​n​d​s​ ​f​r​o​m​ ​y​o​u​r​ ​b​a​n​k​ ​a​c​c​o​u​n​t + */ + bankTransferDesc: string + /** + * D​e​b​i​t​/​C​r​e​d​i​t​ ​C​a​r​d + */ + debitCreditCard: string + /** + * P​a​y​ ​w​i​t​h​ ​y​o​u​r​ ​c​a​r​d​ ​v​i​a​ ​F​y​g​a​r​o + */ + debitCreditCardDesc: string + } + CardPaymentScreen: { + /** + * C​a​r​d​ ​P​a​y​m​e​n​t + */ + title: string + /** + * E​m​a​i​l + */ + email: string + /** + * E​n​t​e​r​ ​y​o​u​r​ ​e​m​a​i​l​ ​a​d​d​r​e​s​s + */ + emailPlaceholder: string + /** + * W​a​l​l​e​t + */ + wallet: string + /** + * S​e​l​e​c​t​ ​w​a​l​l​e​t + */ + walletPlaceholder: string + /** + * A​m​o​u​n​t​ ​(​U​S​D​) + */ + amount: string + /** + * E​n​t​e​r​ ​a​m​o​u​n​t + */ + amountPlaceholder: string + /** + * C​o​n​t​i​n​u​e + */ + 'continue': string + /** + * U​S​D​ ​W​a​l​l​e​t + */ + usdWallet: string + /** + * B​T​C​ ​W​a​l​l​e​t + */ + btcWallet: string + /** + * P​l​e​a​s​e​ ​e​n​t​e​r​ ​a​ ​v​a​l​i​d​ ​e​m​a​i​l​ ​a​d​d​r​e​s​s + */ + invalidEmail: string + /** + * P​l​e​a​s​e​ ​e​n​t​e​r​ ​a​ ​v​a​l​i​d​ ​a​m​o​u​n​t + */ + invalidAmount: string + /** + * M​i​n​i​m​u​m​ ​a​m​o​u​n​t​ ​i​s​ ​$​1​.​0​0 + */ + minimumAmount: string + } + FygaroWebViewScreen: { + /** + * F​y​g​a​r​o​ ​P​a​y​m​e​n​t + */ + title: string + /** + * L​o​a​d​i​n​g​ ​p​a​y​m​e​n​t​ ​p​a​g​e​.​.​. + */ + loading: string + /** + * F​a​i​l​e​d​ ​t​o​ ​l​o​a​d​ ​p​a​y​m​e​n​t​ ​p​a​g​e + */ + error: string + /** + * R​e​t​r​y + */ + retry: string + } + PaymentSuccessScreen: { + /** + * P​a​y​m​e​n​t​ ​S​u​c​c​e​s​s​f​u​l + */ + title: string + /** + * Y​o​u​r​ ​p​a​y​m​e​n​t​ ​h​a​s​ ​b​e​e​n​ ​p​r​o​c​e​s​s​e​d​ ​s​u​c​c​e​s​s​f​u​l​l​y + */ + successMessage: string + /** + * A​m​o​u​n​t​ ​S​e​n​t + */ + amountSent: string + /** + * D​e​p​o​s​i​t​e​d​ ​t​o + */ + depositedTo: string + /** + * T​r​a​n​s​a​c​t​i​o​n​ ​I​D + */ + transactionId: string + /** + * D​o​n​e + */ + done: string + /** + * V​i​e​w​ ​T​r​a​n​s​a​c​t​i​o​n + */ + viewTransaction: string + } PinScreen: { /** * I​n​c​o​r​r​e​c​t​ ​P​I​N​.​ ​{​a​t​t​e​m​p​t​s​R​e​m​a​i​n​i​n​g​}​ ​a​t​t​e​m​p​t​s​ ​r​e​m​a​i​n​i​n​g​. @@ -2237,7 +2365,7 @@ type RootTranslation = { */ setPin: string /** - * To set a PIN code, please back up your account by adding a phone number. + * T​o​ ​s​e​t​ ​a​ ​P​I​N​ ​c​o​d​e​,​ ​p​l​e​a​s​e​ ​b​a​c​k​ ​u​p​ ​y​o​u​r​ ​a​c​c​o​u​n​t​ ​b​y​ ​a​d​d​i​n​g​ ​a​ ​p​h​o​n​e​ ​n​u​m​b​e​r​. */ backupDescription: string } @@ -3504,6 +3632,22 @@ type RootTranslation = { * %​ ​t​o​ ​c​o​n​v​e​r​t */ percentageToConvert: string + /** + * T​o​p​ ​U​p + */ + topUp: string + /** + * A​d​d​ ​f​u​n​d​s​ ​t​o​ ​y​o​u​r​ ​w​a​l​l​e​t + */ + topUpDesc: string + /** + * S​e​t​t​l​e + */ + settle: string + /** + * S​e​t​t​l​e​ ​p​e​n​d​i​n​g​ ​t​r​a​n​s​a​c​t​i​o​n​s + */ + settleDesc: string } UpgradeAccountModal: { /** @@ -3576,7 +3720,7 @@ type RootTranslation = { */ invalidCharacter: string /** - * Your username must begin with an uppercase or lowercase letter. + * Y​o​u​r​ ​u​s​e​r​n​a​m​e​ ​m​u​s​t​ ​b​e​g​i​n​ ​w​i​t​h​ ​a​n​ ​u​p​p​e​r​c​a​s​e​ ​o​r​ ​l​o​w​e​r​c​a​s​e​ ​l​e​t​t​e​r */ startsWithNumber: string /** @@ -6357,6 +6501,10 @@ export type TranslationFunctions = { * Send */ send: () => LocalizedString + /** + * Transfer + */ + transfer: () => LocalizedString /** * Sweep to Wallet */ @@ -6467,6 +6615,130 @@ export type TranslationFunctions = { */ backupDesc: () => LocalizedString } + TopUpScreen: { + /** + * Top Up + */ + title: () => LocalizedString + /** + * Bank Transfer + */ + bankTransfer: () => LocalizedString + /** + * Transfer funds from your bank account + */ + bankTransferDesc: () => LocalizedString + /** + * Debit/Credit Card + */ + debitCreditCard: () => LocalizedString + /** + * Pay with your card via Fygaro + */ + debitCreditCardDesc: () => LocalizedString + } + CardPaymentScreen: { + /** + * Card Payment + */ + title: () => LocalizedString + /** + * Email + */ + email: () => LocalizedString + /** + * Enter your email address + */ + emailPlaceholder: () => LocalizedString + /** + * Wallet + */ + wallet: () => LocalizedString + /** + * Select wallet + */ + walletPlaceholder: () => LocalizedString + /** + * Amount (USD) + */ + amount: () => LocalizedString + /** + * Enter amount + */ + amountPlaceholder: () => LocalizedString + /** + * Continue + */ + 'continue': () => LocalizedString + /** + * USD Wallet + */ + usdWallet: () => LocalizedString + /** + * BTC Wallet + */ + btcWallet: () => LocalizedString + /** + * Please enter a valid email address + */ + invalidEmail: () => LocalizedString + /** + * Please enter a valid amount + */ + invalidAmount: () => LocalizedString + /** + * Minimum amount is $1.00 + */ + minimumAmount: () => LocalizedString + } + FygaroWebViewScreen: { + /** + * Fygaro Payment + */ + title: () => LocalizedString + /** + * Loading payment page... + */ + loading: () => LocalizedString + /** + * Failed to load payment page + */ + error: () => LocalizedString + /** + * Retry + */ + retry: () => LocalizedString + } + PaymentSuccessScreen: { + /** + * Payment Successful + */ + title: () => LocalizedString + /** + * Your payment has been processed successfully + */ + successMessage: () => LocalizedString + /** + * Amount Sent + */ + amountSent: () => LocalizedString + /** + * Deposited to + */ + depositedTo: () => LocalizedString + /** + * Transaction ID + */ + transactionId: () => LocalizedString + /** + * Done + */ + done: () => LocalizedString + /** + * View Transaction + */ + viewTransaction: () => LocalizedString + } PinScreen: { /** * Incorrect PIN. {attemptsRemaining} attempts remaining. @@ -7199,7 +7471,7 @@ export type TranslationFunctions = { /** * The amount on the invoice is less than minimum amount {amount} */ - minAmountInvoiceError: (arg: { amount: number| string }) => LocalizedString + minAmountInvoiceError: (arg: { amount: number }) => LocalizedString /** * The amount on the invoice is greater than maximum amount {amount} */ @@ -8145,6 +8417,22 @@ export type TranslationFunctions = { * % to convert */ percentageToConvert: () => LocalizedString + /** + * Top Up + */ + topUp: () => LocalizedString + /** + * Add funds to your wallet + */ + topUpDesc: () => LocalizedString + /** + * Settle + */ + settle: () => LocalizedString + /** + * Settle pending transactions + */ + settleDesc: () => LocalizedString } UpgradeAccountModal: { /** diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index 911abb752..58d742ecf 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -86,7 +86,9 @@ "ConversionConfirmationScreen": { "title": "Review conversion", "youreConverting": "You're converting", - "receivingAccount": "Receiving account" + "sendingAccount": "Sending account", + "receivingAccount": "Receiving account", + "conversionFee": "Conversion Fee" }, "ConversionSuccessScreen": { "title": "Conversion Success", @@ -504,6 +506,7 @@ "balance": "Refresh Balance", "showQrCode": "Topup via QR", "send": "Send", + "transfer": "Transfer", "sweep": "Sweep to Wallet", "pay": "Pay", "title": "Home", @@ -532,6 +535,43 @@ "backupTitle": "Backup your BTC wallet", "backupDesc": "Backup and secure your Bitcoin wallet using recovery phrase." }, + "TopUpScreen": { + "title": "Top Up", + "bankTransfer": "Bank Transfer", + "bankTransferDesc": "Transfer funds from your bank account", + "debitCreditCard": "Debit/Credit Card", + "debitCreditCardDesc": "Pay with your card via Fygaro" + }, + "CardPaymentScreen": { + "title": "Card Payment", + "email": "Email", + "emailPlaceholder": "Enter your email address", + "wallet": "Wallet", + "walletPlaceholder": "Select wallet", + "amount": "Amount (USD)", + "amountPlaceholder": "Enter amount", + "continue": "Continue", + "usdWallet": "USD Wallet", + "btcWallet": "BTC Wallet", + "invalidEmail": "Please enter a valid email address", + "invalidAmount": "Please enter a valid amount", + "minimumAmount": "Minimum amount is $1.00" + }, + "FygaroWebViewScreen": { + "title": "Fygaro Payment", + "loading": "Loading payment page...", + "error": "Failed to load payment page", + "retry": "Retry" + }, + "PaymentSuccessScreen": { + "title": "Payment Successful", + "successMessage": "Your payment has been processed successfully", + "amountSent": "Amount Sent", + "depositedTo": "Deposited to", + "transactionId": "Transaction ID", + "done": "Done", + "viewTransaction": "View Transaction" + }, "PinScreen": { "attemptsRemaining": "Incorrect PIN. {attemptsRemaining: number} attempts remaining.", "oneAttemptRemaining": "Incorrect PIN. 1 attempt remaining.", @@ -652,7 +692,6 @@ "pinTitle": "PIN Code", "setPin": "Set PIN", "backupDescription": "To set a PIN code, please back up your account by adding a phone number." - }, "SendBitcoinConfirmationScreen": { "amountLabel": "Amount:", @@ -702,7 +741,6 @@ "amount": "Amount", "MinOnChainLimit": "Minimum amount for this transaction is US$2.00", "MinOnChainSatLimit": "Minimum amount for this transaction is 5,500 sats", - "MinFlashcardLimit": "Minimum amount when reloading from flashcard is 100 sats", "amountExceed": "Amount exceeds your balance of {balance: string}", "amountExceedsLimit": "Amount exceeds your remaining daily limit of {limit: string}", "upgradeAccountToIncreaseLimit": "Upgrade your account to increase your limit", @@ -1010,7 +1048,11 @@ }, "TransferScreen": { "title": "Transfer", - "percentageToConvert": "% to convert" + "percentageToConvert": "% to convert", + "topUp": "Top Up", + "topUpDesc": "Add funds to your wallet", + "settle": "Settle", + "settleDesc": "Settle pending transactions" }, "UpgradeAccountModal": { "title": "Upgrade your account", diff --git a/app/navigation/root-navigator.tsx b/app/navigation/root-navigator.tsx index 9cdd68d31..b623b8b89 100644 --- a/app/navigation/root-navigator.tsx +++ b/app/navigation/root-navigator.tsx @@ -86,6 +86,11 @@ import { BackupOptions, TransactionHistoryTabs, USDTransactionHistory, + TransferScreen, + TopUpScreen, + CardPaymentScreen, + FygaroWebViewScreen, + PaymentSuccessScreen, } from "@app/screens" import { usePersistentStateContext } from "@app/store/persistent-state" import { NotificationSettingsScreen } from "@app/screens/settings-screen/notifications-screen" @@ -560,6 +565,31 @@ export const RootStack = () => { component={CashoutSuccess} options={{ headerShown: false }} /> + + + + + +} + +const CardPaymentScreen: React.FC = () => { + const { colors } = useTheme().theme + const { LL } = useI18nContext() + const navigation = useNavigation>() + const isAuthed = useIsAuthed() + const styles = useStyles() + + const [email, setEmail] = useState("") + const [selectedWallet, setSelectedWallet] = useState("USD") + const [amount, setAmount] = useState("") + const [isLoading, setIsLoading] = useState(false) + + const { data } = useHomeAuthedQuery({ + skip: !isAuthed, + fetchPolicy: "cache-first", + }) + + useEffect(() => { + // Pre-populate email if available + if (data?.me?.email?.address) { + setEmail(data.me.email.address) + } + }, [data]) + + if (!isAuthed) { + navigation.goBack() + return null + } + + const validateEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + return emailRegex.test(email) + } + + const validateAmount = (amount: string): boolean => { + const numAmount = parseFloat(amount) + return !isNaN(numAmount) && numAmount >= 1.0 + } + + const handleContinue = async () => { + if (!validateEmail(email)) { + Alert.alert("Invalid Email", LL.CardPaymentScreen.invalidEmail()) + return + } + + if (!validateAmount(amount)) { + Alert.alert("Invalid Amount", LL.CardPaymentScreen.minimumAmount()) + return + } + + setIsLoading(true) + + try { + // Mock API call to initiate payment + const username = data?.me?.username || "user" + const paymentUrl = `https://fygaro.com/en/pb/bd4a34c1-3d24-4315-a2b8-627518f70916?amount=${amount}&client_reference=${username}` + const sessionId = `session_${Date.now()}` + + // Simulate API delay + await new Promise((resolve) => { + setTimeout(() => resolve(), 1000) + }) + + navigation.navigate("fygaroWebView", { + amount: parseFloat(amount), + email, + wallet: selectedWallet, + sessionId, + paymentUrl, + username, + }) + } catch (error) { + Alert.alert("Error", "Failed to initiate payment. Please try again.") + } finally { + setIsLoading(false) + } + } + + const walletButtons = [ + { + id: "USD", + text: LL.CardPaymentScreen.usdWallet(), + icon: { + selected: , + normal: , + }, + }, + { + id: "BTC", + text: LL.CardPaymentScreen.btcWallet(), + icon: { + selected: , + normal: , + }, + }, + ] + + return ( + + + + + {LL.CardPaymentScreen.title()} + + + + + + {LL.CardPaymentScreen.email()} + + + + + + + {LL.CardPaymentScreen.wallet()} + + + + + + + {LL.CardPaymentScreen.amount()} + + + + + + + + + + ) +} + +const useStyles = makeStyles(({ colors }) => ({ + scrollView: { + flex: 1, + }, + container: { + flex: 1, + padding: 20, + }, + title: { + textAlign: "center", + marginBottom: 40, + }, + formContainer: { + flex: 1, + justifyContent: "center", + }, + fieldContainer: { + marginBottom: 24, + }, + label: { + marginBottom: 8, + fontWeight: "600", + }, + input: { + borderWidth: 1, + borderColor: colors.grey3, + borderRadius: 12, + padding: 16, + fontSize: 16, + backgroundColor: colors.white, + color: colors.black, + }, + buttonGroup: { + marginTop: 8, + }, + continueButton: { + marginTop: 32, + }, +})) + +export default CardPaymentScreen diff --git a/app/screens/transfer-screen/card-payment-screen/index.ts b/app/screens/transfer-screen/card-payment-screen/index.ts new file mode 100644 index 000000000..4f289d965 --- /dev/null +++ b/app/screens/transfer-screen/card-payment-screen/index.ts @@ -0,0 +1 @@ +export { default as CardPaymentScreen } from "./card-payment-screen" diff --git a/app/screens/transfer-screen/fygaro-webview-screen/fygaro-webview-screen.tsx b/app/screens/transfer-screen/fygaro-webview-screen/fygaro-webview-screen.tsx new file mode 100644 index 000000000..ac41f8946 --- /dev/null +++ b/app/screens/transfer-screen/fygaro-webview-screen/fygaro-webview-screen.tsx @@ -0,0 +1,180 @@ +import React, { useState, useRef } from "react" +import { View, ActivityIndicator, Alert } from "react-native" +import { useNavigation, useRoute, RouteProp } from "@react-navigation/native" +import { StackNavigationProp } from "@react-navigation/stack" +import { Text, makeStyles, useTheme } from "@rneui/themed" +import { WebView } from "react-native-webview" +import { Screen } from "@app/components/screen" +import { useI18nContext } from "@app/i18n/i18n-react" +import { RootStackParamList } from "@app/navigation/stack-param-lists" +import { PrimaryBtn } from "@app/components/buttons" + +type FygaroWebViewScreenProps = { + navigation: StackNavigationProp + route: RouteProp +} + +const FygaroWebViewScreen: React.FC = () => { + const { colors } = useTheme().theme + const { LL } = useI18nContext() + const navigation = useNavigation>() + const route = useRoute>() + const webViewRef = useRef(null) + const styles = useStyles() + + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(false) + + const { amount, wallet, paymentUrl } = route.params + + const handleNavigationStateChange = (navState: { url: string }) => { + const { url } = navState + + // Check for success URL pattern + if (url.includes("success") || url.includes("payment_success")) { + // Mock successful payment response + const mockTransactionId = `txn_${Date.now()}` + + navigation.navigate("paymentSuccess", { + amount, + wallet, + transactionId: mockTransactionId, + }) + } + + // Check for error/failure URLs + if (url.includes("error") || url.includes("failed") || url.includes("cancelled")) { + Alert.alert("Payment Failed", "Your payment was not completed. Please try again.", [ + { text: "OK", onPress: () => navigation.goBack() }, + ]) + } + } + + const handleLoadEnd = () => { + setIsLoading(false) + setError(false) + } + + const handleError = () => { + setIsLoading(false) + setError(true) + } + + const handleRetry = () => { + setError(false) + setIsLoading(true) + if (webViewRef.current) { + webViewRef.current.reload() + } + } + + const isAllowedDomain = (url: string): boolean => { + // Security check: only allow Fygaro domains + const allowedDomains = [ + "fygaro.com", + "www.fygaro.com", + "api.fygaro.com", + "checkout.fygaro.com", + ] + + try { + const domain = new URL(url).hostname + return allowedDomains.includes(domain) + } catch { + return false + } + } + + const handleShouldStartLoadWithRequest = (request: { url: string }) => { + return isAllowedDomain(request.url) + } + + if (error) { + return ( + + + + {LL.FygaroWebViewScreen.error()} + + + + + ) + } + + return ( + + + {isLoading && ( + + + + {LL.FygaroWebViewScreen.loading()} + + + )} + + + + + ) +} + +const useStyles = makeStyles(() => ({ + container: { + flex: 1, + }, + centerContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: 20, + }, + loadingContainer: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: "center", + alignItems: "center", + backgroundColor: "rgba(255, 255, 255, 0.9)", + zIndex: 1, + }, + loadingText: { + marginTop: 16, + textAlign: "center", + }, + errorText: { + textAlign: "center", + marginBottom: 24, + }, + retryButton: { + marginTop: 16, + }, + webView: { + flex: 1, + }, +})) + +export default FygaroWebViewScreen diff --git a/app/screens/transfer-screen/fygaro-webview-screen/index.ts b/app/screens/transfer-screen/fygaro-webview-screen/index.ts new file mode 100644 index 000000000..30f60f9e8 --- /dev/null +++ b/app/screens/transfer-screen/fygaro-webview-screen/index.ts @@ -0,0 +1 @@ +export { default as FygaroWebViewScreen } from "./fygaro-webview-screen" diff --git a/app/screens/transfer-screen/index.ts b/app/screens/transfer-screen/index.ts new file mode 100644 index 000000000..135b5025f --- /dev/null +++ b/app/screens/transfer-screen/index.ts @@ -0,0 +1,5 @@ +export { default as TransferScreen } from "./transfer-screen" +export * from "./topup-screen" +export * from "./card-payment-screen" +export * from "./fygaro-webview-screen" +export * from "./payment-success-screen" diff --git a/app/screens/transfer-screen/payment-success-screen/index.ts b/app/screens/transfer-screen/payment-success-screen/index.ts new file mode 100644 index 000000000..0b449afa7 --- /dev/null +++ b/app/screens/transfer-screen/payment-success-screen/index.ts @@ -0,0 +1 @@ +export { default as PaymentSuccessScreen } from "./payment-success-screen" diff --git a/app/screens/transfer-screen/payment-success-screen/payment-success-screen.tsx b/app/screens/transfer-screen/payment-success-screen/payment-success-screen.tsx new file mode 100644 index 000000000..d6f1cb23e --- /dev/null +++ b/app/screens/transfer-screen/payment-success-screen/payment-success-screen.tsx @@ -0,0 +1,155 @@ +import React from "react" +import { View } from "react-native" +import { useNavigation, useRoute, RouteProp } from "@react-navigation/native" +import { StackNavigationProp } from "@react-navigation/stack" +import { Text, makeStyles, useTheme } from "@rneui/themed" +import { Screen } from "@app/components/screen" +import { useI18nContext } from "@app/i18n/i18n-react" +import { RootStackParamList } from "@app/navigation/stack-param-lists" +import { PrimaryBtn } from "@app/components/buttons" + +type PaymentSuccessScreenProps = { + navigation: StackNavigationProp + route: RouteProp +} + +const PaymentSuccessScreen: React.FC = () => { + const { colors } = useTheme().theme + const { LL } = useI18nContext() + const navigation = useNavigation>() + const route = useRoute>() + const styles = useStyles() + + const { amount, wallet, transactionId } = route.params + + const handleDone = () => { + // Navigate back to home screen + navigation.navigate("Primary") + } + + const handleViewTransaction = () => { + // TODO: Navigate to transaction details + console.log("Navigate to transaction details:", transactionId) + handleDone() + } + + return ( + + + + + + + {LL.PaymentSuccessScreen.title()} + + + + {LL.PaymentSuccessScreen.successMessage()} + + + + + + {LL.PaymentSuccessScreen.amountSent()}: + + + ${amount.toFixed(2)} + + + + + + {LL.PaymentSuccessScreen.depositedTo()}: + + + {wallet} Wallet + + + + + + {LL.PaymentSuccessScreen.transactionId()}: + + + {transactionId} + + + + + + + + + + + + + ) +} + +const useStyles = makeStyles(({ colors }) => ({ + container: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: 20, + }, + successContainer: { + alignItems: "center", + width: "100%", + }, + successIcon: { + fontSize: 80, + marginBottom: 20, + textAlign: "center", + }, + title: { + textAlign: "center", + marginBottom: 16, + }, + message: { + textAlign: "center", + marginBottom: 32, + }, + detailsContainer: { + width: "100%", + backgroundColor: colors.grey5, + borderRadius: 16, + padding: 20, + marginBottom: 32, + }, + detailRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginBottom: 12, + }, + detailLabel: { + flex: 1, + }, + detailValue: { + fontWeight: "600", + textAlign: "right", + }, + buttonContainer: { + width: "100%", + gap: 16, + }, + primaryButton: { + marginTop: 8, + }, + secondaryButton: { + marginTop: 8, + }, +})) + +export default PaymentSuccessScreen diff --git a/app/screens/transfer-screen/topup-screen/index.ts b/app/screens/transfer-screen/topup-screen/index.ts new file mode 100644 index 000000000..15cde2106 --- /dev/null +++ b/app/screens/transfer-screen/topup-screen/index.ts @@ -0,0 +1 @@ +export { default as TopUpScreen } from "./topup-screen" diff --git a/app/screens/transfer-screen/topup-screen/topup-screen.tsx b/app/screens/transfer-screen/topup-screen/topup-screen.tsx new file mode 100644 index 000000000..d9c4d350e --- /dev/null +++ b/app/screens/transfer-screen/topup-screen/topup-screen.tsx @@ -0,0 +1,122 @@ +import React from "react" +import { View } from "react-native" +import { useNavigation } from "@react-navigation/native" +import { StackNavigationProp } from "@react-navigation/stack" +import { Text, makeStyles, useTheme } from "@rneui/themed" +import { Screen } from "@app/components/screen" +import { useI18nContext } from "@app/i18n/i18n-react" +import { RootStackParamList } from "@app/navigation/stack-param-lists" +import { useIsAuthed } from "@app/graphql/is-authed-context" +import { PrimaryBtn } from "@app/components/buttons" + +type TopUpScreenProps = { + navigation: StackNavigationProp +} + +const TopUpScreen: React.FC = () => { + const { colors } = useTheme().theme + const { LL } = useI18nContext() + const navigation = useNavigation>() + const isAuthed = useIsAuthed() + const styles = useStyles() + + if (!isAuthed) { + navigation.goBack() + return null + } + + const handleBankTransfer = () => { + // TODO: Implement bank transfer functionality + console.log("Bank transfer functionality not implemented yet") + } + + const handleCardPayment = () => { + navigation.navigate("cardPayment") + } + + return ( + + + + {LL.TopUpScreen.title()} + + + + + + {LL.TopUpScreen.bankTransfer()} + + + {LL.TopUpScreen.bankTransferDesc()} + + + + + + + {LL.TopUpScreen.debitCreditCard()} + + + {LL.TopUpScreen.debitCreditCardDesc()} + + + + + + + ) +} + +const useStyles = makeStyles(({ colors }) => ({ + container: { + flex: 1, + padding: 20, + }, + title: { + textAlign: "center", + marginBottom: 40, + }, + optionsContainer: { + flex: 1, + justifyContent: "center", + gap: 20, + }, + optionCard: { + backgroundColor: colors.white, + borderRadius: 16, + padding: 24, + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + optionTitle: { + textAlign: "center", + marginBottom: 8, + }, + optionDescription: { + textAlign: "center", + marginBottom: 24, + }, + primaryButton: { + marginTop: 8, + }, + secondaryButton: { + marginTop: 8, + }, +})) + +export default TopUpScreen diff --git a/app/screens/transfer-screen/transfer-screen.tsx b/app/screens/transfer-screen/transfer-screen.tsx new file mode 100644 index 000000000..d7a4b9359 --- /dev/null +++ b/app/screens/transfer-screen/transfer-screen.tsx @@ -0,0 +1,122 @@ +import React from "react" +import { View } from "react-native" +import { useNavigation } from "@react-navigation/native" +import { StackNavigationProp } from "@react-navigation/stack" +import { Text, makeStyles, useTheme } from "@rneui/themed" +import { Screen } from "@app/components/screen" +import { useI18nContext } from "@app/i18n/i18n-react" +import { RootStackParamList } from "@app/navigation/stack-param-lists" +import { useIsAuthed } from "@app/graphql/is-authed-context" +import { PrimaryBtn } from "@app/components/buttons" + +type TransferScreenProps = { + navigation: StackNavigationProp +} + +const TransferScreen: React.FC = () => { + const { colors } = useTheme().theme + const { LL } = useI18nContext() + const navigation = useNavigation>() + const isAuthed = useIsAuthed() + const styles = useStyles() + + if (!isAuthed) { + navigation.goBack() + return null + } + + const handleTopUp = () => { + navigation.navigate("topUp") + } + + const handleSettle = () => { + // TODO: Implement settle functionality + console.log("Settle functionality not implemented yet") + } + + return ( + + + + {LL.TransferScreen.title()} + + + + + + {LL.TransferScreen.topUp()} + + + {LL.TransferScreen.topUpDesc()} + + + + + + + {LL.TransferScreen.settle()} + + + {LL.TransferScreen.settleDesc()} + + + + + + + ) +} + +const useStyles = makeStyles(({ colors }) => ({ + container: { + flex: 1, + padding: 20, + }, + title: { + textAlign: "center", + marginBottom: 40, + }, + optionsContainer: { + flex: 1, + justifyContent: "center", + gap: 20, + }, + optionCard: { + backgroundColor: colors.white, + borderRadius: 16, + padding: 24, + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + optionTitle: { + textAlign: "center", + marginBottom: 8, + }, + optionDescription: { + textAlign: "center", + marginBottom: 24, + }, + primaryButton: { + marginTop: 8, + }, + secondaryButton: { + marginTop: 8, + }, +})) + +export default TransferScreen diff --git a/ios/LNFlash.xcodeproj/project.pbxproj b/ios/LNFlash.xcodeproj/project.pbxproj index f9c1063ae..f84fbd3d0 100644 --- a/ios/LNFlash.xcodeproj/project.pbxproj +++ b/ios/LNFlash.xcodeproj/project.pbxproj @@ -14,9 +14,9 @@ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 21AC428275D3136988EC92FB /* libPods-LNFlash-Alt.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 11FB4E445D02B5ABCF13F611 /* libPods-LNFlash-Alt.a */; }; - 3152A8AF830F40A7AB689E42 /* (null) in Resources */ = {isa = PBXBuildFile; }; - 3E3C92C2FC91412D8DFDE94D /* (null) in Resources */ = {isa = PBXBuildFile; }; - 560415592C5B4119B9F91553 /* (null) in Resources */ = {isa = PBXBuildFile; }; + 3152A8AF830F40A7AB689E42 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + 3E3C92C2FC91412D8DFDE94D /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + 560415592C5B4119B9F91553 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; 8647B9C22DFD831600E2F160 /* AppDelegate.h in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FAF1A68108700A75B9A /* AppDelegate.h */; }; 8647B9C32DFD831600E2F160 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; @@ -27,19 +27,19 @@ 8647B9CB2DFD831600E2F160 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 8647B9CC2DFD831600E2F160 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 86FB60FB2BC40BBC0088C78C /* PrivacyInfo.xcprivacy */; }; 8647B9CD2DFD831600E2F160 /* coins.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0147E0DC2BAE64B90071CDF2 /* coins.mp3 */; }; - 8647B9CE2DFD831600E2F160 /* (null) in Resources */ = {isa = PBXBuildFile; }; - 8647B9CF2DFD831600E2F160 /* (null) in Resources */ = {isa = PBXBuildFile; }; - 8647B9D02DFD831600E2F160 /* (null) in Resources */ = {isa = PBXBuildFile; }; - 8647B9D12DFD831600E2F160 /* (null) in Resources */ = {isa = PBXBuildFile; }; - 8647B9D22DFD831600E2F160 /* (null) in Resources */ = {isa = PBXBuildFile; }; - 8647B9D32DFD831600E2F160 /* (null) in Resources */ = {isa = PBXBuildFile; }; + 8647B9CE2DFD831600E2F160 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + 8647B9CF2DFD831600E2F160 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + 8647B9D02DFD831600E2F160 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + 8647B9D12DFD831600E2F160 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + 8647B9D22DFD831600E2F160 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + 8647B9D32DFD831600E2F160 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; 8647B9ED2DFEAFE500E2F160 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 8647B9EC2DFEAFE500E2F160 /* GoogleService-Info.plist */; }; 8647B9EF2DFEAFF900E2F160 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 8647B9EE2DFEAFF900E2F160 /* GoogleService-Info.plist */; }; 86FB60FC2BC40BBC0088C78C /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 86FB60FB2BC40BBC0088C78C /* PrivacyInfo.xcprivacy */; }; 922869E2611560E0BABDD066 /* libPods-LNFlash.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F675C724C850DCE831172994 /* libPods-LNFlash.a */; }; - 94D9F3C84EB547D68D41C50F /* (null) in Resources */ = {isa = PBXBuildFile; }; - BD157A9851974C298EB06CB7 /* (null) in Resources */ = {isa = PBXBuildFile; }; - C426C81D58C8450C878B6086 /* (null) in Resources */ = {isa = PBXBuildFile; }; + 94D9F3C84EB547D68D41C50F /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + BD157A9851974C298EB06CB7 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + C426C81D58C8450C878B6086 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; C61EE0AD23C530E30054100C /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C61EE0AC23C530E30054100C /* AuthenticationServices.framework */; }; F1D71F3628CE5C9A00636277 /* AppDelegate.h in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FAF1A68108700A75B9A /* AppDelegate.h */; }; /* End PBXBuildFile section */ @@ -287,12 +287,12 @@ 86FB60FC2BC40BBC0088C78C /* PrivacyInfo.xcprivacy in Resources */, 0147E0DD2BAE64B90071CDF2 /* coins.mp3 in Resources */, 8647B9ED2DFEAFE500E2F160 /* GoogleService-Info.plist in Resources */, - 560415592C5B4119B9F91553 /* (null) in Resources */, - 3E3C92C2FC91412D8DFDE94D /* (null) in Resources */, - BD157A9851974C298EB06CB7 /* (null) in Resources */, - 3152A8AF830F40A7AB689E42 /* (null) in Resources */, - 94D9F3C84EB547D68D41C50F /* (null) in Resources */, - C426C81D58C8450C878B6086 /* (null) in Resources */, + 560415592C5B4119B9F91553 /* BuildFile in Resources */, + 3E3C92C2FC91412D8DFDE94D /* BuildFile in Resources */, + BD157A9851974C298EB06CB7 /* BuildFile in Resources */, + 3152A8AF830F40A7AB689E42 /* BuildFile in Resources */, + 94D9F3C84EB547D68D41C50F /* BuildFile in Resources */, + C426C81D58C8450C878B6086 /* BuildFile in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -306,12 +306,12 @@ 8647B9CC2DFD831600E2F160 /* PrivacyInfo.xcprivacy in Resources */, 8647B9CD2DFD831600E2F160 /* coins.mp3 in Resources */, 8647B9EF2DFEAFF900E2F160 /* GoogleService-Info.plist in Resources */, - 8647B9CE2DFD831600E2F160 /* (null) in Resources */, - 8647B9CF2DFD831600E2F160 /* (null) in Resources */, - 8647B9D02DFD831600E2F160 /* (null) in Resources */, - 8647B9D12DFD831600E2F160 /* (null) in Resources */, - 8647B9D22DFD831600E2F160 /* (null) in Resources */, - 8647B9D32DFD831600E2F160 /* (null) in Resources */, + 8647B9CE2DFD831600E2F160 /* BuildFile in Resources */, + 8647B9CF2DFD831600E2F160 /* BuildFile in Resources */, + 8647B9D02DFD831600E2F160 /* BuildFile in Resources */, + 8647B9D12DFD831600E2F160 /* BuildFile in Resources */, + 8647B9D22DFD831600E2F160 /* BuildFile in Resources */, + 8647B9D32DFD831600E2F160 /* BuildFile in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f66a6a66a..0e2e58bd6 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1063,10 +1063,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f boost: 7dcd2de282d72e344012f7d6564d024930a6a440 - breez_sdk_liquid: 13fbdfa9027b06bb0381309c51ee73740ae2e716 + breez_sdk_liquid: 681814c73de091675e84b09ebad132c00484e519 breez_sdk_liquidFFI: f77b037f9cc3f8d8e93fbb1ff4c34063e1f05bfd BreezSDKLiquid: d6b9c3b6815b77c98ae379f0e8b4e9b73750e4ae - BVLinearGradient: 880f91a7854faff2df62518f0281afb1c60d49a3 + BVLinearGradient: cb006ba232a1f3e4f341bb62c42d1098c284da70 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: 5fbbff1d7734827299274638deb8ba3024f6c597 FBReactNativeSpec: 638095fe8a01506634d77b260ef8a322019ac671 @@ -1098,48 +1098,48 @@ SPEC CHECKSUMS: OpenSSL-Universal: 6082b0bf950e5636fe0d78def171184e2b3899c2 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 - RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 + RCT-Folly: 8dc08ca5a393b48b1c523ab6220dfdcc0fe000ad RCTRequired: 83bca1c184feb4d2e51c72c8369b83d641443f95 RCTTypeSafety: 13c4a87a16d7db6cd66006ce9759f073402ef85b React: e67aa9f99957c7611c392b5e49355d877d6525e2 React-callinvoker: 2790c09d964c2e5404b5410cde91b152e3746b7b - React-Codegen: e6e05e105ca7cdb990f4d609985a2a689d8d0653 - React-Core: 9283f1e7d0d5e3d33ad298547547b1b43912534c - React-CoreModules: 6312c9b2fec4329d9ae6a2b8c350032d1664c51b - React-cxxreact: 7da72565656c8ac7f97c9a031d0b199bbdec0640 + React-Codegen: 89173b1974099c3082e50c83e9d04113ede45792 + React-Core: 27990a32ca0cfc04872600440f618365b7c35433 + React-CoreModules: 2a1850a46d60b901cceef4e64bcf5bf6a0130206 + React-cxxreact: 03d370d58a083a1c8b5a69b9095c1ac9f57b2f94 React-debug: 4accb2b9dc09b575206d2c42f4082990a52ae436 - React-hermes: 1299a94f255f59a72d5baa54a2ca2e1eee104947 - React-jsi: 2208de64c3a41714ac04e86975386fc49116ea13 - React-jsiexecutor: c49502e5d02112247ee4526bc3ccfc891ae3eb9b + React-hermes: 0a9e25fbf4dbcd8ca89de9a89a0cce2fce45989f + React-jsi: 0c473d4292f9a10469b3755767bf28d0b35fbeb6 + React-jsiexecutor: 00fdf7bd0e99ab878109ce1b51cb6212d76683e4 React-jsinspector: 8baadae51f01d867c3921213a25ab78ab4fbcd91 - React-logger: 8edc785c47c8686c7962199a307015e2ce9a0e4f - react-native-aes: c75c46aa744bef7c2415fdf7f5b2dcb75ca4364d - react-native-config: 86038147314e2e6d10ea9972022aa171e6b1d4d8 - react-native-date-picker: 06a4d96ab525a163c7a90bccd68833d136b0bb13 - react-native-document-picker: b4f4a23b73f864ce17965b284c0757648993805b - react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe - react-native-geetest-module: ed6a20774a7975640b79a2f639327c674a488cb5 - react-native-get-random-values: 384787fd76976f5aec9465aff6fa9e9129af1e74 - react-native-html-to-pdf: 4c5c6e26819fe202971061594058877aa9b25265 - react-native-image-picker: 5e076db26cd81660cfb6db5bcf517cfa12054d45 - react-native-in-app-review: db8bb167a5f238e7ceca5c242d6b36ce8c4404a4 - react-native-maps: 084fccedd6785bd41e85a13a26e8e6252a45b594 - react-native-nfc-manager: 2a87d561c4fa832e6597a5f2f7c197808c019ab8 - react-native-pager-view: e26d35e382d86950c936f8917e3beb9188115ccc - react-native-quick-crypto: b859e7bc40b1fdd0d9f4b0a1475304fd3e2e216c - react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846 - react-native-safe-area-context: 0ee144a6170530ccc37a0fd9388e28d06f516a89 - react-native-secure-key-store: 910e6df6bc33cb790aba6ee24bc7818df1fe5898 - react-native-slider: cc89964e1432fa31aa9db7a0fa9b21e26b5d5152 - react-native-video: c26780b224543c62d5e1b2a7244a5cd1b50e8253 - react-native-view-shot: 6b7ed61d77d88580fed10954d45fad0eb2d47688 - react-native-webview: bdc091de8cf7f8397653e30182efcd9f772e03b3 - React-NativeModulesApple: b6868ee904013a7923128892ee4a032498a1024a + React-logger: 61efd44da84482aabbbbb478a49b893c7c912f99 + react-native-aes: bed3ca6c47c5a5ebd5bac683efdf737c874f6d3f + react-native-config: 136f9755ccc991cc6438053a44363259ad4c7813 + react-native-date-picker: 585252087d4820b4cd8f2cf80068f6e8f5b72413 + react-native-document-picker: 74f5ca4179532f9ff205275990af514d1f2e22d8 + react-native-fingerprint-scanner: 91bf6825709dd7bd3abc4588a4772eb097a2b2d8 + react-native-geetest-module: cecd5dfca2c7f815a8e724c11137b35c92e900d3 + react-native-get-random-values: ce0b8796c99e2b85e3202bd500b1ef286a17a02e + react-native-html-to-pdf: 7a49e6c58ac5221bcc093027b195f4b214f27a9d + react-native-image-picker: 7a3502135a13fc56d406f5213b7346de6bc5f38b + react-native-in-app-review: b3d1eed3d1596ebf6539804778272c4c65e4a400 + react-native-maps: 2173cbaddcef764af9a8ed56883b7672d6fc8337 + react-native-nfc-manager: ab799bdeecbb12b199bccdb0065cbb4d3271c1e4 + react-native-pager-view: 8f36f88437684bf5ea86f9172a91c266d99b975f + react-native-quick-crypto: 1daacdde8771548da81d783a1778aba55a7bbf8c + react-native-randombytes: 3c8f3e89d12487fd03a2f966c288d495415fc116 + react-native-safe-area-context: bf9d9d58f0f6726d4a6257088044c2595017579d + react-native-secure-key-store: eb45b44bdec3f48e9be5cdfca0f49ddf64892ea6 + react-native-slider: 2ee855f44d8024139690ad4581cec2d51c616456 + react-native-video: 2aad0d963bf3952bd9ebb2f53fab799338e8e202 + react-native-view-shot: d1a701eb0719c6dccbd20b4bb43b1069f304cb70 + react-native-webview: 11105d80264df1a56fbbb0c774311a52bb287388 + React-NativeModulesApple: 2f7a355e9b4c83b9509bf6dd798dc5f63ab8bc7d React-perflogger: 31ea61077185eb1428baf60c0db6e2886f141a5a React-RCTActionSheet: 392090a3abc8992eb269ef0eaa561750588fc39d React-RCTAnimation: 4b3cc6a29474bc0d78c4f04b52ab59bf760e8a9b - React-RCTAppDelegate: 89b015b29885109addcabecdf3b2e833905437c7 - React-RCTBlob: 3e23dcbe6638897b5605e46d0d62955d78e8d27b + React-RCTAppDelegate: b6febbe1109554fee87d3fea1c50cca511429fec + React-RCTBlob: 76113160e3cdc0f678795823c1a7c9d69b2db099 React-RCTImage: 8a5d339d614a90a183fc1b8b6a7eb44e2e703943 React-RCTLinking: b37dfbf646d77c326f9eae094b1fcd575b1c24c7 React-RCTNetwork: 8bed9b2461c7d8a7d14e63df9b16181c448beebc @@ -1148,42 +1148,42 @@ SPEC CHECKSUMS: React-RCTVibration: d1b78ca38f61ea4b3e9ebb2ddbd0b5662631d99b React-rncore: bfc2f6568b6fecbae6f2f774e95c60c3c9e95bf2 React-runtimeexecutor: 47b0a2d5bbb416db65ef881a6f7bdcfefa0001ab - React-runtimescheduler: 7649c3b46c8dee1853691ecf60146a16ae59253c - React-utils: 56838edeaaf651220d1e53cd0b8934fb8ce68415 - ReactCommon: 5f704096ccf7733b390f59043b6fa9cc180ee4f6 - RNBootSplash: 85f6b879c080e958afdb4c62ee04497b05fd7552 - RNCAsyncStorage: 618d03a5f52fbccb3d7010076bc54712844c18ef - RNCClipboard: 60fed4b71560d7bfe40e9d35dea9762b024da86d - RNDateTimePicker: 65e1d202799460b286ff5e741d8baf54695e8abd - RNDeviceInfo: db5c64a060e66e5db3102d041ebe3ef307a85120 - RNFBAnalytics: 8d1705b9076df1ed0b72a165d78c303e8569807b - RNFBApp: fa5825b36d8362ce9b993cac8bf070d116848640 - RNFBAppCheck: f98d1bd525efe4e22f28c6070796434c9baf6e9a - RNFBCrashlytics: 14dcfb073e7d9f41189400128e203d5314a8c606 - RNFBMessaging: 32a107c0463048f1c03df06ab235bf56e976299a - RNFBPerf: a3f48919b73a6355113735a463f048a772fc79a4 - RNFBRemoteConfig: f2d4de1a288ede64e777f3229c7746ed4127ccb3 - RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592 - RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 - RNGestureHandler: 32a01c29ecc9bb0b5bf7bc0a33547f61b4dc2741 - RNImageCropPicker: 14fe1c29298fb4018f3186f455c475ab107da332 - RNInAppBrowser: e36d6935517101ccba0e875bac8ad7b0cb655364 - RNKeychain: a65256b6ca6ba6976132cc4124b238a5b13b3d9c - RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 - RNNotifee: 8e2d3df3f0e9ce8f5d1fe4c967431138190b6175 - RNPermissions: 294531ede5a64d1528463e88fffef05675842042 - RNQrGenerator: 1676221c08bfabec978242989c733810dad20959 - RNRate: ef3bcff84f39bb1d1e41c5593d3eea4aab2bd73a - RNReactNativeHapticFeedback: ec56a5f81c3941206fd85625fa669ffc7b4545f9 - RNReanimated: fdbaa9c964bbab7fac50c90862b6cc5f041679b9 - RNScreens: 3c5b9f4a9dcde752466854b6109b79c0e205dad3 - RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef - RNShare: 0fad69ae2d71de9d1f7b9a43acf876886a6cb99c - RNSVG: ba3e7232f45e34b7b47e74472386cf4e1a676d0a - RNVectorIcons: 64e6a523ac30a3241efa9baf1ffbcc5e76ff747a + React-runtimescheduler: d12a963f61390fcd1b957a9c9ebee3c0f775dede + React-utils: 22f94a6e85b1323ffb1b9a747a1c03c5e6eaead6 + ReactCommon: ef602e9cfb8940ad7c08aa4cdc228d802e194e5c + RNBootSplash: 21095c4567847829470786b03b6892c5efed5299 + RNCAsyncStorage: a03b770a50541a761447cea9c24536047832124d + RNCClipboard: 4abb037e8fe3b98a952564c9e0474f91c492df6d + RNDateTimePicker: 47b54bf36a41c29d75ac62a05af1b38a9a721631 + RNDeviceInfo: addb9b427c2822a2d8e94c87a136a224e0af738c + RNFBAnalytics: 81e00e4209b0a6268c2a8b262d7e451493bda824 + RNFBApp: 2b2bb0f17eb6732e2e90d9c57bfde443cd7fc681 + RNFBAppCheck: 6e2df9110387283d00ff126d3903c9f79987d1c8 + RNFBCrashlytics: 266758adee95705af20f106c767e19588a5de665 + RNFBMessaging: 4627e84e9e363953357dd122543e4223c49e6bc1 + RNFBPerf: 594a4c7bb12fb68e920e101192539da748973da8 + RNFBRemoteConfig: 4842e7c1b0bb8d2f9c2acc3b811e6395eddfe550 + RNFileViewer: 4b5d83358214347e4ab2d4ca8d5c1c90d869e251 + RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8 + RNGestureHandler: dc1acdc779554be3aa733f4a9a19bb782ec3a48c + RNImageCropPicker: 874e26cbf0ce9d06a11002cbadf29c8b7f2f5565 + RNInAppBrowser: 6d3eb68d471b9834335c664704719b8be1bfdb20 + RNKeychain: df33ae4d27df06622fc14190b790ba8749f6be76 + RNLocalize: 8bf466de4c92d4721b254aabe1ff0a1456e7b9f4 + RNNotifee: 8768d065bf1e2f9f8f347b4bd79147431c7eacd6 + RNPermissions: 4f7b81b0d457c8ec4e892cc36ef2376734a4b22c + RNQrGenerator: 60eab4f7c9e3f09db78029636fe356dca5cb585f + RNRate: 7641919330e0d6688ad885a985b4bd697ed7d14c + RNReactNativeHapticFeedback: a6fb5b7a981683bf58af43e3fb827d4b7ed87f83 + RNReanimated: 49a1e0d191bdaefe1b394eb258bc52a42bcf704c + RNScreens: a425ae50ad66d024a6e936121bf5c9fbe6a5cdc6 + RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc + RNShare: 694e19d7f74ac4c04de3a8af0649e9ccc03bd8b1 + RNSVG: 6d5ed33b6635ed6d6ecb50744dcf127580c39ed5 + RNVectorIcons: 2bb1ff267624f4e79188d65908c959fd284c5003 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 - VisionCamera: 1910a51e4c6f6b049650086d343090f267b4c260 + VisionCamera: 29095ffe0a146b6254c3db34636d10298b169f36 Yoga: 4c3aa327e4a6a23eeacd71f61c81df1bcdf677d5 ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 From 38fcc5d7b498699693dbf72f8bf77956b3bb97b7 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Sat, 19 Jul 2025 13:51:48 +0500 Subject: [PATCH 02/95] redesign and refactor transfer-screen, topup-screen and card-payment-screen --- app/navigation/root-navigator.tsx | 26 ++- .../card-payment-screen.tsx | 158 ++++++++---------- .../topup-screen/topup-screen.tsx | 113 +++++-------- .../transfer-screen/transfer-screen.tsx | 123 ++++++-------- 4 files changed, 168 insertions(+), 252 deletions(-) diff --git a/app/navigation/root-navigator.tsx b/app/navigation/root-navigator.tsx index 620e4abb8..f2b2e4a82 100644 --- a/app/navigation/root-navigator.tsx +++ b/app/navigation/root-navigator.tsx @@ -567,21 +567,7 @@ export const RootStack = () => { component={CashoutSuccess} options={{ headerShown: false }} /> - - - + { cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS, }} /> + + + + + ) } diff --git a/app/screens/transfer-screen/card-payment-screen/card-payment-screen.tsx b/app/screens/transfer-screen/card-payment-screen/card-payment-screen.tsx index 0bf36a9ba..d79aa8734 100644 --- a/app/screens/transfer-screen/card-payment-screen/card-payment-screen.tsx +++ b/app/screens/transfer-screen/card-payment-screen/card-payment-screen.tsx @@ -1,16 +1,21 @@ import React, { useState, useEffect } from "react" -import { View, TextInput, Alert, ScrollView } from "react-native" -import { useNavigation } from "@react-navigation/native" -import { StackNavigationProp } from "@react-navigation/stack" +import { View, TextInput, Alert } from "react-native" import { Text, makeStyles, useTheme } from "@rneui/themed" -import { Screen } from "@app/components/screen" -import { useI18nContext } from "@app/i18n/i18n-react" +import { StackNavigationProp } from "@react-navigation/stack" import { RootStackParamList } from "@app/navigation/stack-param-lists" -import { useIsAuthed } from "@app/graphql/is-authed-context" -import { useHomeAuthedQuery } from "@app/graphql/generated" + +// components +import { Screen } from "@app/components/screen" import { PrimaryBtn } from "@app/components/buttons" import { ButtonGroup } from "@app/components/button-group" +// hooks +import { useI18nContext } from "@app/i18n/i18n-react" +import { useNavigation } from "@react-navigation/native" +import { useHomeAuthedQuery } from "@app/graphql/generated" +import { useIsAuthed } from "@app/graphql/is-authed-context" +import { useSafeAreaInsets } from "react-native-safe-area-context" + // assets import Cash from "@app/assets/icons/cash.svg" import Bitcoin from "@app/assets/icons/bitcoin.svg" @@ -19,10 +24,10 @@ type CardPaymentScreenProps = { navigation: StackNavigationProp } -const CardPaymentScreen: React.FC = () => { +const CardPaymentScreen: React.FC = ({ navigation }) => { + const { bottom } = useSafeAreaInsets() const { colors } = useTheme().theme const { LL } = useI18nContext() - const navigation = useNavigation>() const isAuthed = useIsAuthed() const styles = useStyles() @@ -37,17 +42,11 @@ const CardPaymentScreen: React.FC = () => { }) useEffect(() => { - // Pre-populate email if available if (data?.me?.email?.address) { setEmail(data.me.email.address) } }, [data]) - if (!isAuthed) { - navigation.goBack() - return null - } - const validateEmail = (email: string): boolean => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ return emailRegex.test(email) @@ -102,106 +101,89 @@ const CardPaymentScreen: React.FC = () => { id: "USD", text: LL.CardPaymentScreen.usdWallet(), icon: { - selected: , - normal: , + selected: , + normal: , }, }, { id: "BTC", text: LL.CardPaymentScreen.btcWallet(), icon: { - selected: , - normal: , + selected: , + normal: , }, }, ] return ( - - - - {LL.CardPaymentScreen.title()} + + + {LL.CardPaymentScreen.title()} + + + + {LL.CardPaymentScreen.email()} + + - - - - {LL.CardPaymentScreen.email()} - - - - - - - {LL.CardPaymentScreen.wallet()} - - - - - - - {LL.CardPaymentScreen.amount()} - - - - - - + + + {LL.CardPaymentScreen.wallet()} + + + + + + + {LL.CardPaymentScreen.amount()} + + - + + ) } const useStyles = makeStyles(({ colors }) => ({ - scrollView: { - flex: 1, - }, container: { flex: 1, - padding: 20, + paddingHorizontal: 20, }, title: { textAlign: "center", - marginBottom: 40, - }, - formContainer: { - flex: 1, - justifyContent: "center", + marginBottom: 30, }, fieldContainer: { marginBottom: 24, }, - label: { - marginBottom: 8, - fontWeight: "600", - }, input: { borderWidth: 1, borderColor: colors.grey3, @@ -210,13 +192,11 @@ const useStyles = makeStyles(({ colors }) => ({ fontSize: 16, backgroundColor: colors.white, color: colors.black, + marginTop: 8, }, buttonGroup: { marginTop: 8, }, - continueButton: { - marginTop: 32, - }, })) export default CardPaymentScreen diff --git a/app/screens/transfer-screen/topup-screen/topup-screen.tsx b/app/screens/transfer-screen/topup-screen/topup-screen.tsx index d9c4d350e..43aed9218 100644 --- a/app/screens/transfer-screen/topup-screen/topup-screen.tsx +++ b/app/screens/transfer-screen/topup-screen/topup-screen.tsx @@ -1,29 +1,21 @@ import React from "react" -import { View } from "react-native" -import { useNavigation } from "@react-navigation/native" +import { TouchableOpacity, View } from "react-native" +import { RootStackParamList } from "@app/navigation/stack-param-lists" +import { Icon, Text, makeStyles, useTheme } from "@rneui/themed" import { StackNavigationProp } from "@react-navigation/stack" -import { Text, makeStyles, useTheme } from "@rneui/themed" -import { Screen } from "@app/components/screen" import { useI18nContext } from "@app/i18n/i18n-react" -import { RootStackParamList } from "@app/navigation/stack-param-lists" -import { useIsAuthed } from "@app/graphql/is-authed-context" -import { PrimaryBtn } from "@app/components/buttons" + +// components +import { Screen } from "@app/components/screen" type TopUpScreenProps = { navigation: StackNavigationProp } -const TopUpScreen: React.FC = () => { - const { colors } = useTheme().theme - const { LL } = useI18nContext() - const navigation = useNavigation>() - const isAuthed = useIsAuthed() +const TopUpScreen: React.FC = ({ navigation }) => { const styles = useStyles() - - if (!isAuthed) { - navigation.goBack() - return null - } + const { LL } = useI18nContext() + const { colors } = useTheme().theme const handleBankTransfer = () => { // TODO: Implement bank transfer functionality @@ -37,40 +29,30 @@ const TopUpScreen: React.FC = () => { return ( - + {LL.TopUpScreen.title()} + + - - - - {LL.TopUpScreen.bankTransfer()} - - + + {LL.TopUpScreen.bankTransfer()} + {LL.TopUpScreen.bankTransferDesc()} - - - - - {LL.TopUpScreen.debitCreditCard()} - - + + + + + + {LL.TopUpScreen.debitCreditCard()} + {LL.TopUpScreen.debitCreditCardDesc()} - - + + ) @@ -79,43 +61,26 @@ const TopUpScreen: React.FC = () => { const useStyles = makeStyles(({ colors }) => ({ container: { flex: 1, - padding: 20, + paddingHorizontal: 20, }, title: { textAlign: "center", - marginBottom: 40, + marginBottom: 30, }, - optionsContainer: { - flex: 1, - justifyContent: "center", - gap: 20, - }, - optionCard: { - backgroundColor: colors.white, - borderRadius: 16, - padding: 24, - shadowColor: "#000", - shadowOffset: { - width: 0, - height: 2, - }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, + btn: { + flexDirection: "row", + alignItems: "center", + borderRadius: 20, + borderWidth: 1, + borderColor: "#dedede", + marginBottom: 20, + minHeight: 100, + paddingHorizontal: 20, }, - optionTitle: { - textAlign: "center", - marginBottom: 8, - }, - optionDescription: { - textAlign: "center", - marginBottom: 24, - }, - primaryButton: { - marginTop: 8, - }, - secondaryButton: { - marginTop: 8, + btnTextWrapper: { + flex: 1, + rowGap: 5, + marginHorizontal: 15, }, })) diff --git a/app/screens/transfer-screen/transfer-screen.tsx b/app/screens/transfer-screen/transfer-screen.tsx index d7a4b9359..11d64aa0d 100644 --- a/app/screens/transfer-screen/transfer-screen.tsx +++ b/app/screens/transfer-screen/transfer-screen.tsx @@ -1,29 +1,21 @@ import React from "react" -import { View } from "react-native" -import { useNavigation } from "@react-navigation/native" +import { TouchableOpacity, View } from "react-native" +import { RootStackParamList } from "@app/navigation/stack-param-lists" +import { Icon, Text, makeStyles, useTheme } from "@rneui/themed" import { StackNavigationProp } from "@react-navigation/stack" -import { Text, makeStyles, useTheme } from "@rneui/themed" -import { Screen } from "@app/components/screen" import { useI18nContext } from "@app/i18n/i18n-react" -import { RootStackParamList } from "@app/navigation/stack-param-lists" -import { useIsAuthed } from "@app/graphql/is-authed-context" -import { PrimaryBtn } from "@app/components/buttons" + +// components +import { Screen } from "@app/components/screen" type TransferScreenProps = { navigation: StackNavigationProp } -const TransferScreen: React.FC = () => { - const { colors } = useTheme().theme - const { LL } = useI18nContext() - const navigation = useNavigation>() - const isAuthed = useIsAuthed() +const TransferScreen: React.FC = ({ navigation }) => { const styles = useStyles() - - if (!isAuthed) { - navigation.goBack() - return null - } + const { LL } = useI18nContext() + const { colors } = useTheme().theme const handleTopUp = () => { navigation.navigate("topUp") @@ -37,40 +29,40 @@ const TransferScreen: React.FC = () => { return ( - + {LL.TransferScreen.title()} - - - - - {LL.TransferScreen.topUp()} - - + + + + {LL.TransferScreen.topUp()} + {LL.TransferScreen.topUpDesc()} - + + + + - - - {LL.TransferScreen.settle()} - - + + {LL.TransferScreen.settle()} + {LL.TransferScreen.settleDesc()} - - + + ) @@ -79,43 +71,26 @@ const TransferScreen: React.FC = () => { const useStyles = makeStyles(({ colors }) => ({ container: { flex: 1, - padding: 20, + paddingHorizontal: 20, }, title: { textAlign: "center", - marginBottom: 40, + marginBottom: 30, }, - optionsContainer: { - flex: 1, - justifyContent: "center", - gap: 20, - }, - optionCard: { - backgroundColor: colors.white, - borderRadius: 16, - padding: 24, - shadowColor: "#000", - shadowOffset: { - width: 0, - height: 2, - }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, + btn: { + flexDirection: "row", + alignItems: "center", + borderRadius: 20, + borderWidth: 1, + borderColor: "#dedede", + marginBottom: 20, + minHeight: 100, + paddingHorizontal: 20, }, - optionTitle: { - textAlign: "center", - marginBottom: 8, - }, - optionDescription: { - textAlign: "center", - marginBottom: 24, - }, - primaryButton: { - marginTop: 8, - }, - secondaryButton: { - marginTop: 8, + btnTextWrapper: { + flex: 1, + rowGap: 5, + marginHorizontal: 15, }, })) From 4223024f6373ebf741928a65186602d1dedd42ff Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Tue, 22 Jul 2025 09:57:04 +0500 Subject: [PATCH 03/95] BankTransfer screen is implemented and added in the root navigation --- app/navigation/root-navigator.tsx | 2 + app/navigation/stack-param-lists.ts | 1 + .../bank-transfer-screen.tsx | 145 ++++++++++++++++++ .../bank-transfer-screen/index.ts | 1 + app/screens/transfer-screen/index.ts | 1 + 5 files changed, 150 insertions(+) create mode 100644 app/screens/transfer-screen/bank-transfer-screen/bank-transfer-screen.tsx create mode 100644 app/screens/transfer-screen/bank-transfer-screen/index.ts diff --git a/app/navigation/root-navigator.tsx b/app/navigation/root-navigator.tsx index f2b2e4a82..b83aa576f 100644 --- a/app/navigation/root-navigator.tsx +++ b/app/navigation/root-navigator.tsx @@ -92,6 +92,7 @@ import { CardPaymentScreen, FygaroWebViewScreen, PaymentSuccessScreen, + BankTransferScreen, } from "@app/screens" import { usePersistentStateContext } from "@app/store/persistent-state" import { NotificationSettingsScreen } from "@app/screens/settings-screen/notifications-screen" @@ -600,6 +601,7 @@ export const RootStack = () => { + ) diff --git a/app/navigation/stack-param-lists.ts b/app/navigation/stack-param-lists.ts index e08958a94..cf9fcb2b2 100644 --- a/app/navigation/stack-param-lists.ts +++ b/app/navigation/stack-param-lists.ts @@ -151,6 +151,7 @@ export type RootStackParamList = { transfer: undefined topUp: undefined cardPayment: undefined + bankTransfer: undefined fygaroWebView: { amount: number email: string diff --git a/app/screens/transfer-screen/bank-transfer-screen/bank-transfer-screen.tsx b/app/screens/transfer-screen/bank-transfer-screen/bank-transfer-screen.tsx new file mode 100644 index 000000000..ee087e49c --- /dev/null +++ b/app/screens/transfer-screen/bank-transfer-screen/bank-transfer-screen.tsx @@ -0,0 +1,145 @@ +import React from "react" +import { View } from "react-native" +import { makeStyles, Text } from "@rneui/themed" +import { useSafeAreaInsets } from "react-native-safe-area-context" + +// components +import { Screen } from "@app/components/screen" +import { PrimaryBtn } from "@app/components/buttons" + +const BankTransferScreen = () => { + const styles = useStyles() + const { bottom } = useSafeAreaInsets() + + return ( + + + Bank Transfer + + + Your order has been created. To complete the order, please transfer $102 USD to + the bank details provided below. + + + Use UUM7MJRD as the reference description. This unique code will help us associate + the payment with your Flash account and process the Bitcoin transfer. + + + After we have received your payment, you will be credited with $100 USD in your + Cash wallet, with a $2 USD fee deducted. You can then choose when you convert + those USD to Bitcoin on your own using the Convert functionality in the mobile + app. + + + + Account Type + + Checking + + + + Destination Bank + + Banco Hipotecario + + + + Account Number + + 00210312362 + + + + Type of Client + + Corporate + + + + Receiver's Name + + BBW SA de CV + + + + Email + + fiat@blink.sv + + + + Amount + + 102 USD + + + + Unique Code + + UUM7MJRD + + + + Fees + + 2 USD + + + + + After payment completion on your end you can send us an email to fiat@blink.sv + with a screenshot of your payment confirmation. + + + Your payment will be processed even if we don't receive this email, but having + this confirmation can help accelerate the order. + + {}} + btnStyle={{ marginBottom: bottom + 20, marginTop: 20 }} + /> + + ) +} + +export default BankTransferScreen + +const useStyles = makeStyles(({ colors }) => ({ + container: { + flex: 1, + paddingHorizontal: 20, + }, + title: { + textAlign: "center", + marginBottom: 30, + }, + desc: { + marginBottom: 15, + }, + bankDetails: { + marginVertical: 20, + }, + fieldContainer: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + backgroundColor: colors.grey5, + padding: 20, + borderRadius: 10, + marginBottom: 15, + }, + input: { + borderWidth: 1, + borderColor: colors.grey3, + borderRadius: 12, + padding: 16, + fontSize: 16, + backgroundColor: colors.white, + color: colors.black, + marginTop: 8, + }, + buttonGroup: { + marginTop: 8, + }, +})) diff --git a/app/screens/transfer-screen/bank-transfer-screen/index.ts b/app/screens/transfer-screen/bank-transfer-screen/index.ts new file mode 100644 index 000000000..7c592e663 --- /dev/null +++ b/app/screens/transfer-screen/bank-transfer-screen/index.ts @@ -0,0 +1 @@ +export { default as BankTransferScreen } from "./bank-transfer-screen" diff --git a/app/screens/transfer-screen/index.ts b/app/screens/transfer-screen/index.ts index 135b5025f..bcd4b14c5 100644 --- a/app/screens/transfer-screen/index.ts +++ b/app/screens/transfer-screen/index.ts @@ -3,3 +3,4 @@ export * from "./topup-screen" export * from "./card-payment-screen" export * from "./fygaro-webview-screen" export * from "./payment-success-screen" +export * from "./bank-transfer-screen" From f5015048dd1d3747942e2a7484becb3aafefe583 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Tue, 22 Jul 2025 11:12:25 +0500 Subject: [PATCH 04/95] restructure buy-sell-bitcoin flow screens --- app/components/home-screen/Buttons.tsx | 2 +- app/i18n/en/index.ts | 3 +- app/i18n/i18n-types.ts | 12 +- app/i18n/raw-i18n/source/en.json | 3 +- app/navigation/root-navigator.tsx | 31 ++-- app/navigation/stack-param-lists.ts | 8 +- .../BankTransfer.tsx} | 8 +- .../BuyBitcoin.tsx} | 33 ++--- .../BuyBitcoinDetails.tsx} | 35 +++-- .../buy-sell-bitcoin.tsx} | 12 +- .../fygaro-webview-screen.tsx | 0 app/screens/buy-bitcoin-flow/index.ts | 6 + .../payment-success-screen.tsx | 0 app/screens/index.ts | 1 - .../bank-transfer-screen/index.ts | 1 - .../card-payment-screen/index.ts | 1 - .../fygaro-webview-screen/index.ts | 1 - app/screens/transfer-screen/index.ts | 6 - .../payment-success-screen/index.ts | 1 - .../transfer-screen/topup-screen/index.ts | 1 - ios/Podfile.lock | 138 +++++++++--------- 21 files changed, 148 insertions(+), 155 deletions(-) rename app/screens/{transfer-screen/bank-transfer-screen/bank-transfer-screen.tsx => buy-bitcoin-flow/BankTransfer.tsx} (93%) rename app/screens/{transfer-screen/topup-screen/topup-screen.tsx => buy-bitcoin-flow/BuyBitcoin.tsx} (75%) rename app/screens/{transfer-screen/card-payment-screen/card-payment-screen.tsx => buy-bitcoin-flow/BuyBitcoinDetails.tsx} (83%) rename app/screens/{transfer-screen/transfer-screen.tsx => buy-bitcoin-flow/buy-sell-bitcoin.tsx} (89%) rename app/screens/{transfer-screen/fygaro-webview-screen => buy-bitcoin-flow}/fygaro-webview-screen.tsx (100%) create mode 100644 app/screens/buy-bitcoin-flow/index.ts rename app/screens/{transfer-screen/payment-success-screen => buy-bitcoin-flow}/payment-success-screen.tsx (100%) delete mode 100644 app/screens/transfer-screen/bank-transfer-screen/index.ts delete mode 100644 app/screens/transfer-screen/card-payment-screen/index.ts delete mode 100644 app/screens/transfer-screen/fygaro-webview-screen/index.ts delete mode 100644 app/screens/transfer-screen/index.ts delete mode 100644 app/screens/transfer-screen/payment-success-screen/index.ts delete mode 100644 app/screens/transfer-screen/topup-screen/index.ts diff --git a/app/components/home-screen/Buttons.tsx b/app/components/home-screen/Buttons.tsx index f8f343bbe..a177347a0 100644 --- a/app/components/home-screen/Buttons.tsx +++ b/app/components/home-screen/Buttons.tsx @@ -60,7 +60,7 @@ const Buttons: React.FC = ({ setModalVisible, setDefaultAccountModalVisib }, { title: LL.HomeScreen.transfer(), - target: "transfer", + target: "BuySellBitcoin", icon: "swap", }, ] diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index b72cd133d..b436a140a 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -573,8 +573,9 @@ const en: BaseTranslation = { debitCreditCard: "Debit/Credit Card", debitCreditCardDesc: "Pay with your card via Fygaro" }, - CardPaymentScreen: { + BuyBitcoinDetails: { title: "Card Payment", + bankTransfer: "Bank Transfer", email: "Email", emailPlaceholder: "Enter your email address", wallet: "Wallet", diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index 3fd1eeb08..80f71ed5c 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -1803,11 +1803,15 @@ type RootTranslation = { */ debitCreditCardDesc: string } - CardPaymentScreen: { + BuyBitcoinDetails: { /** * C​a​r​d​ ​P​a​y​m​e​n​t */ title: string + /** + * Bank Transfer + */ + bankTransfer: string /** * E​m​a​i​l */ @@ -6679,11 +6683,15 @@ export type TranslationFunctions = { */ debitCreditCardDesc: () => LocalizedString } - CardPaymentScreen: { + BuyBitcoinDetails: { /** * Card Payment */ title: () => LocalizedString + /** + * Bank Transfer + */ + bankTransfer: () => LocalizedString /** * Email */ diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index 9debcf3d4..eae85ec14 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -542,8 +542,9 @@ "debitCreditCard": "Debit/Credit Card", "debitCreditCardDesc": "Pay with your card via Fygaro" }, - "CardPaymentScreen": { + "BuyBitcoinDetails": { "title": "Card Payment", + "bankTransfer": "Bank Transfer", "email": "Email", "emailPlaceholder": "Enter your email address", "wallet": "Wallet", diff --git a/app/navigation/root-navigator.tsx b/app/navigation/root-navigator.tsx index b83aa576f..881959cfc 100644 --- a/app/navigation/root-navigator.tsx +++ b/app/navigation/root-navigator.tsx @@ -87,12 +87,6 @@ import { TransactionHistoryTabs, USDTransactionHistory, SignInViaQRCode, - TransferScreen, - TopUpScreen, - CardPaymentScreen, - FygaroWebViewScreen, - PaymentSuccessScreen, - BankTransferScreen, } from "@app/screens" import { usePersistentStateContext } from "@app/store/persistent-state" import { NotificationSettingsScreen } from "@app/screens/settings-screen/notifications-screen" @@ -122,6 +116,12 @@ import { } from "@app/screens/cashout-screen" import { NostrSettingsScreen } from "@app/screens/settings-screen/nostr-settings/nostr-settings-screen" import ContactDetailsScreen from "@app/screens/nip17-chat/contactDetailsScreen" +import { + BankTransfer, + BuyBitcoin, + BuyBitcoinDetails, + BuySellBitcoin, +} from "@app/screens/buy-bitcoin-flow" const useStyles = makeStyles(({ colors }) => ({ bottomNavigatorStyle: { @@ -568,17 +568,6 @@ export const RootStack = () => { component={CashoutSuccess} options={{ headerShown: false }} /> - - - { headerShadowVisible: false, }} > - - - - + + + + ) diff --git a/app/navigation/stack-param-lists.ts b/app/navigation/stack-param-lists.ts index cf9fcb2b2..21c82484b 100644 --- a/app/navigation/stack-param-lists.ts +++ b/app/navigation/stack-param-lists.ts @@ -148,10 +148,10 @@ export type RootStackParamList = { EditNostrProfile: undefined NostrSettingsScreen: undefined SignInViaQRCode: undefined - transfer: undefined - topUp: undefined - cardPayment: undefined - bankTransfer: undefined + BuySellBitcoin: undefined + BuyBitcoin: undefined + BuyBitcoinDetails: { paymentType: "card" | "bankTransfer" } + BankTransfer: undefined fygaroWebView: { amount: number email: string diff --git a/app/screens/transfer-screen/bank-transfer-screen/bank-transfer-screen.tsx b/app/screens/buy-bitcoin-flow/BankTransfer.tsx similarity index 93% rename from app/screens/transfer-screen/bank-transfer-screen/bank-transfer-screen.tsx rename to app/screens/buy-bitcoin-flow/BankTransfer.tsx index ee087e49c..d40e3cb27 100644 --- a/app/screens/transfer-screen/bank-transfer-screen/bank-transfer-screen.tsx +++ b/app/screens/buy-bitcoin-flow/BankTransfer.tsx @@ -1,13 +1,17 @@ import React from "react" import { View } from "react-native" import { makeStyles, Text } from "@rneui/themed" +import { StackScreenProps } from "@react-navigation/stack" import { useSafeAreaInsets } from "react-native-safe-area-context" +import { RootStackParamList } from "@app/navigation/stack-param-lists" // components import { Screen } from "@app/components/screen" import { PrimaryBtn } from "@app/components/buttons" -const BankTransferScreen = () => { +type Props = StackScreenProps + +const BankTransfer: React.FC = ({ navigation }) => { const styles = useStyles() const { bottom } = useSafeAreaInsets() @@ -103,7 +107,7 @@ const BankTransferScreen = () => { ) } -export default BankTransferScreen +export default BankTransfer const useStyles = makeStyles(({ colors }) => ({ container: { diff --git a/app/screens/transfer-screen/topup-screen/topup-screen.tsx b/app/screens/buy-bitcoin-flow/BuyBitcoin.tsx similarity index 75% rename from app/screens/transfer-screen/topup-screen/topup-screen.tsx rename to app/screens/buy-bitcoin-flow/BuyBitcoin.tsx index 43aed9218..bc19c84ca 100644 --- a/app/screens/transfer-screen/topup-screen/topup-screen.tsx +++ b/app/screens/buy-bitcoin-flow/BuyBitcoin.tsx @@ -2,37 +2,31 @@ import React from "react" import { TouchableOpacity, View } from "react-native" import { RootStackParamList } from "@app/navigation/stack-param-lists" import { Icon, Text, makeStyles, useTheme } from "@rneui/themed" -import { StackNavigationProp } from "@react-navigation/stack" +import { StackScreenProps } from "@react-navigation/stack" import { useI18nContext } from "@app/i18n/i18n-react" // components import { Screen } from "@app/components/screen" -type TopUpScreenProps = { - navigation: StackNavigationProp -} +type Props = StackScreenProps -const TopUpScreen: React.FC = ({ navigation }) => { +const BuyBitcoin: React.FC = ({ navigation }) => { const styles = useStyles() const { LL } = useI18nContext() const { colors } = useTheme().theme - const handleBankTransfer = () => { - // TODO: Implement bank transfer functionality - console.log("Bank transfer functionality not implemented yet") - } - - const handleCardPayment = () => { - navigation.navigate("cardPayment") - } - return ( {LL.TopUpScreen.title()} - + + navigation.navigate("BuyBitcoinDetails", { paymentType: "bankTransfer" }) + } + > @@ -43,7 +37,12 @@ const TopUpScreen: React.FC = ({ navigation }) => { - + + navigation.navigate("BuyBitcoinDetails", { paymentType: "card" }) + } + > {LL.TopUpScreen.debitCreditCard()} @@ -84,4 +83,4 @@ const useStyles = makeStyles(({ colors }) => ({ }, })) -export default TopUpScreen +export default BuyBitcoin diff --git a/app/screens/transfer-screen/card-payment-screen/card-payment-screen.tsx b/app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx similarity index 83% rename from app/screens/transfer-screen/card-payment-screen/card-payment-screen.tsx rename to app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx index d79aa8734..6a84b5931 100644 --- a/app/screens/transfer-screen/card-payment-screen/card-payment-screen.tsx +++ b/app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react" import { View, TextInput, Alert } from "react-native" import { Text, makeStyles, useTheme } from "@rneui/themed" -import { StackNavigationProp } from "@react-navigation/stack" +import { StackNavigationProp, StackScreenProps } from "@react-navigation/stack" import { RootStackParamList } from "@app/navigation/stack-param-lists" // components @@ -11,7 +11,6 @@ import { ButtonGroup } from "@app/components/button-group" // hooks import { useI18nContext } from "@app/i18n/i18n-react" -import { useNavigation } from "@react-navigation/native" import { useHomeAuthedQuery } from "@app/graphql/generated" import { useIsAuthed } from "@app/graphql/is-authed-context" import { useSafeAreaInsets } from "react-native-safe-area-context" @@ -20,11 +19,9 @@ import { useSafeAreaInsets } from "react-native-safe-area-context" import Cash from "@app/assets/icons/cash.svg" import Bitcoin from "@app/assets/icons/bitcoin.svg" -type CardPaymentScreenProps = { - navigation: StackNavigationProp -} +type Props = StackScreenProps -const CardPaymentScreen: React.FC = ({ navigation }) => { +const BuyBitcoinDetails: React.FC = ({ navigation, route }) => { const { bottom } = useSafeAreaInsets() const { colors } = useTheme().theme const { LL } = useI18nContext() @@ -59,12 +56,12 @@ const CardPaymentScreen: React.FC = ({ navigation }) => const handleContinue = async () => { if (!validateEmail(email)) { - Alert.alert("Invalid Email", LL.CardPaymentScreen.invalidEmail()) + Alert.alert("Invalid Email", LL.BuyBitcoinDetails.invalidEmail()) return } if (!validateAmount(amount)) { - Alert.alert("Invalid Amount", LL.CardPaymentScreen.minimumAmount()) + Alert.alert("Invalid Amount", LL.BuyBitcoinDetails.minimumAmount()) return } @@ -99,7 +96,7 @@ const CardPaymentScreen: React.FC = ({ navigation }) => const walletButtons = [ { id: "USD", - text: LL.CardPaymentScreen.usdWallet(), + text: LL.BuyBitcoinDetails.usdWallet(), icon: { selected: , normal: , @@ -107,7 +104,7 @@ const CardPaymentScreen: React.FC = ({ navigation }) => }, { id: "BTC", - text: LL.CardPaymentScreen.btcWallet(), + text: LL.BuyBitcoinDetails.btcWallet(), icon: { selected: , normal: , @@ -119,15 +116,17 @@ const CardPaymentScreen: React.FC = ({ navigation }) => - {LL.CardPaymentScreen.title()} + {route.params.paymentType === "card" + ? LL.BuyBitcoinDetails.title() + : LL.BuyBitcoinDetails.bankTransfer()} - {LL.CardPaymentScreen.email()} + {LL.BuyBitcoinDetails.email()} = ({ navigation }) => - {LL.CardPaymentScreen.wallet()} + {LL.BuyBitcoinDetails.wallet()} = ({ navigation }) => - {LL.CardPaymentScreen.amount()} + {LL.BuyBitcoinDetails.amount()} = ({ navigation }) => ({ }, })) -export default CardPaymentScreen +export default BuyBitcoinDetails diff --git a/app/screens/transfer-screen/transfer-screen.tsx b/app/screens/buy-bitcoin-flow/buy-sell-bitcoin.tsx similarity index 89% rename from app/screens/transfer-screen/transfer-screen.tsx rename to app/screens/buy-bitcoin-flow/buy-sell-bitcoin.tsx index 11d64aa0d..7c71e99eb 100644 --- a/app/screens/transfer-screen/transfer-screen.tsx +++ b/app/screens/buy-bitcoin-flow/buy-sell-bitcoin.tsx @@ -2,23 +2,21 @@ import React from "react" import { TouchableOpacity, View } from "react-native" import { RootStackParamList } from "@app/navigation/stack-param-lists" import { Icon, Text, makeStyles, useTheme } from "@rneui/themed" -import { StackNavigationProp } from "@react-navigation/stack" +import { StackScreenProps } from "@react-navigation/stack" import { useI18nContext } from "@app/i18n/i18n-react" // components import { Screen } from "@app/components/screen" -type TransferScreenProps = { - navigation: StackNavigationProp -} +type Props = StackScreenProps -const TransferScreen: React.FC = ({ navigation }) => { +const BuySellBitcoin: React.FC = ({ navigation }) => { const styles = useStyles() const { LL } = useI18nContext() const { colors } = useTheme().theme const handleTopUp = () => { - navigation.navigate("topUp") + navigation.navigate("BuyBitcoin") } const handleSettle = () => { @@ -94,4 +92,4 @@ const useStyles = makeStyles(({ colors }) => ({ }, })) -export default TransferScreen +export default BuySellBitcoin diff --git a/app/screens/transfer-screen/fygaro-webview-screen/fygaro-webview-screen.tsx b/app/screens/buy-bitcoin-flow/fygaro-webview-screen.tsx similarity index 100% rename from app/screens/transfer-screen/fygaro-webview-screen/fygaro-webview-screen.tsx rename to app/screens/buy-bitcoin-flow/fygaro-webview-screen.tsx diff --git a/app/screens/buy-bitcoin-flow/index.ts b/app/screens/buy-bitcoin-flow/index.ts new file mode 100644 index 000000000..08f46aa85 --- /dev/null +++ b/app/screens/buy-bitcoin-flow/index.ts @@ -0,0 +1,6 @@ +import BuySellBitcoin from "./buy-sell-bitcoin" +import BuyBitcoin from "./BuyBitcoin" +import BuyBitcoinDetails from "./BuyBitcoinDetails" +import BankTransfer from "./BankTransfer" + +export { BuySellBitcoin, BuyBitcoin, BuyBitcoinDetails, BankTransfer } diff --git a/app/screens/transfer-screen/payment-success-screen/payment-success-screen.tsx b/app/screens/buy-bitcoin-flow/payment-success-screen.tsx similarity index 100% rename from app/screens/transfer-screen/payment-success-screen/payment-success-screen.tsx rename to app/screens/buy-bitcoin-flow/payment-success-screen.tsx diff --git a/app/screens/index.ts b/app/screens/index.ts index e8aca008f..35a694751 100644 --- a/app/screens/index.ts +++ b/app/screens/index.ts @@ -1,4 +1,3 @@ export * from "./backup-screen" export * from "./import-wallet-screen" export * from "./transaction-history" -export * from "./transfer-screen" diff --git a/app/screens/transfer-screen/bank-transfer-screen/index.ts b/app/screens/transfer-screen/bank-transfer-screen/index.ts deleted file mode 100644 index 7c592e663..000000000 --- a/app/screens/transfer-screen/bank-transfer-screen/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as BankTransferScreen } from "./bank-transfer-screen" diff --git a/app/screens/transfer-screen/card-payment-screen/index.ts b/app/screens/transfer-screen/card-payment-screen/index.ts deleted file mode 100644 index 4f289d965..000000000 --- a/app/screens/transfer-screen/card-payment-screen/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as CardPaymentScreen } from "./card-payment-screen" diff --git a/app/screens/transfer-screen/fygaro-webview-screen/index.ts b/app/screens/transfer-screen/fygaro-webview-screen/index.ts deleted file mode 100644 index 30f60f9e8..000000000 --- a/app/screens/transfer-screen/fygaro-webview-screen/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as FygaroWebViewScreen } from "./fygaro-webview-screen" diff --git a/app/screens/transfer-screen/index.ts b/app/screens/transfer-screen/index.ts deleted file mode 100644 index bcd4b14c5..000000000 --- a/app/screens/transfer-screen/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { default as TransferScreen } from "./transfer-screen" -export * from "./topup-screen" -export * from "./card-payment-screen" -export * from "./fygaro-webview-screen" -export * from "./payment-success-screen" -export * from "./bank-transfer-screen" diff --git a/app/screens/transfer-screen/payment-success-screen/index.ts b/app/screens/transfer-screen/payment-success-screen/index.ts deleted file mode 100644 index 0b449afa7..000000000 --- a/app/screens/transfer-screen/payment-success-screen/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as PaymentSuccessScreen } from "./payment-success-screen" diff --git a/app/screens/transfer-screen/topup-screen/index.ts b/app/screens/transfer-screen/topup-screen/index.ts deleted file mode 100644 index 15cde2106..000000000 --- a/app/screens/transfer-screen/topup-screen/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as TopUpScreen } from "./topup-screen" diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0e2e58bd6..f66a6a66a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1063,10 +1063,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f boost: 7dcd2de282d72e344012f7d6564d024930a6a440 - breez_sdk_liquid: 681814c73de091675e84b09ebad132c00484e519 + breez_sdk_liquid: 13fbdfa9027b06bb0381309c51ee73740ae2e716 breez_sdk_liquidFFI: f77b037f9cc3f8d8e93fbb1ff4c34063e1f05bfd BreezSDKLiquid: d6b9c3b6815b77c98ae379f0e8b4e9b73750e4ae - BVLinearGradient: cb006ba232a1f3e4f341bb62c42d1098c284da70 + BVLinearGradient: 880f91a7854faff2df62518f0281afb1c60d49a3 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: 5fbbff1d7734827299274638deb8ba3024f6c597 FBReactNativeSpec: 638095fe8a01506634d77b260ef8a322019ac671 @@ -1098,48 +1098,48 @@ SPEC CHECKSUMS: OpenSSL-Universal: 6082b0bf950e5636fe0d78def171184e2b3899c2 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 - RCT-Folly: 8dc08ca5a393b48b1c523ab6220dfdcc0fe000ad + RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 RCTRequired: 83bca1c184feb4d2e51c72c8369b83d641443f95 RCTTypeSafety: 13c4a87a16d7db6cd66006ce9759f073402ef85b React: e67aa9f99957c7611c392b5e49355d877d6525e2 React-callinvoker: 2790c09d964c2e5404b5410cde91b152e3746b7b - React-Codegen: 89173b1974099c3082e50c83e9d04113ede45792 - React-Core: 27990a32ca0cfc04872600440f618365b7c35433 - React-CoreModules: 2a1850a46d60b901cceef4e64bcf5bf6a0130206 - React-cxxreact: 03d370d58a083a1c8b5a69b9095c1ac9f57b2f94 + React-Codegen: e6e05e105ca7cdb990f4d609985a2a689d8d0653 + React-Core: 9283f1e7d0d5e3d33ad298547547b1b43912534c + React-CoreModules: 6312c9b2fec4329d9ae6a2b8c350032d1664c51b + React-cxxreact: 7da72565656c8ac7f97c9a031d0b199bbdec0640 React-debug: 4accb2b9dc09b575206d2c42f4082990a52ae436 - React-hermes: 0a9e25fbf4dbcd8ca89de9a89a0cce2fce45989f - React-jsi: 0c473d4292f9a10469b3755767bf28d0b35fbeb6 - React-jsiexecutor: 00fdf7bd0e99ab878109ce1b51cb6212d76683e4 + React-hermes: 1299a94f255f59a72d5baa54a2ca2e1eee104947 + React-jsi: 2208de64c3a41714ac04e86975386fc49116ea13 + React-jsiexecutor: c49502e5d02112247ee4526bc3ccfc891ae3eb9b React-jsinspector: 8baadae51f01d867c3921213a25ab78ab4fbcd91 - React-logger: 61efd44da84482aabbbbb478a49b893c7c912f99 - react-native-aes: bed3ca6c47c5a5ebd5bac683efdf737c874f6d3f - react-native-config: 136f9755ccc991cc6438053a44363259ad4c7813 - react-native-date-picker: 585252087d4820b4cd8f2cf80068f6e8f5b72413 - react-native-document-picker: 74f5ca4179532f9ff205275990af514d1f2e22d8 - react-native-fingerprint-scanner: 91bf6825709dd7bd3abc4588a4772eb097a2b2d8 - react-native-geetest-module: cecd5dfca2c7f815a8e724c11137b35c92e900d3 - react-native-get-random-values: ce0b8796c99e2b85e3202bd500b1ef286a17a02e - react-native-html-to-pdf: 7a49e6c58ac5221bcc093027b195f4b214f27a9d - react-native-image-picker: 7a3502135a13fc56d406f5213b7346de6bc5f38b - react-native-in-app-review: b3d1eed3d1596ebf6539804778272c4c65e4a400 - react-native-maps: 2173cbaddcef764af9a8ed56883b7672d6fc8337 - react-native-nfc-manager: ab799bdeecbb12b199bccdb0065cbb4d3271c1e4 - react-native-pager-view: 8f36f88437684bf5ea86f9172a91c266d99b975f - react-native-quick-crypto: 1daacdde8771548da81d783a1778aba55a7bbf8c - react-native-randombytes: 3c8f3e89d12487fd03a2f966c288d495415fc116 - react-native-safe-area-context: bf9d9d58f0f6726d4a6257088044c2595017579d - react-native-secure-key-store: eb45b44bdec3f48e9be5cdfca0f49ddf64892ea6 - react-native-slider: 2ee855f44d8024139690ad4581cec2d51c616456 - react-native-video: 2aad0d963bf3952bd9ebb2f53fab799338e8e202 - react-native-view-shot: d1a701eb0719c6dccbd20b4bb43b1069f304cb70 - react-native-webview: 11105d80264df1a56fbbb0c774311a52bb287388 - React-NativeModulesApple: 2f7a355e9b4c83b9509bf6dd798dc5f63ab8bc7d + React-logger: 8edc785c47c8686c7962199a307015e2ce9a0e4f + react-native-aes: c75c46aa744bef7c2415fdf7f5b2dcb75ca4364d + react-native-config: 86038147314e2e6d10ea9972022aa171e6b1d4d8 + react-native-date-picker: 06a4d96ab525a163c7a90bccd68833d136b0bb13 + react-native-document-picker: b4f4a23b73f864ce17965b284c0757648993805b + react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe + react-native-geetest-module: ed6a20774a7975640b79a2f639327c674a488cb5 + react-native-get-random-values: 384787fd76976f5aec9465aff6fa9e9129af1e74 + react-native-html-to-pdf: 4c5c6e26819fe202971061594058877aa9b25265 + react-native-image-picker: 5e076db26cd81660cfb6db5bcf517cfa12054d45 + react-native-in-app-review: db8bb167a5f238e7ceca5c242d6b36ce8c4404a4 + react-native-maps: 084fccedd6785bd41e85a13a26e8e6252a45b594 + react-native-nfc-manager: 2a87d561c4fa832e6597a5f2f7c197808c019ab8 + react-native-pager-view: e26d35e382d86950c936f8917e3beb9188115ccc + react-native-quick-crypto: b859e7bc40b1fdd0d9f4b0a1475304fd3e2e216c + react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846 + react-native-safe-area-context: 0ee144a6170530ccc37a0fd9388e28d06f516a89 + react-native-secure-key-store: 910e6df6bc33cb790aba6ee24bc7818df1fe5898 + react-native-slider: cc89964e1432fa31aa9db7a0fa9b21e26b5d5152 + react-native-video: c26780b224543c62d5e1b2a7244a5cd1b50e8253 + react-native-view-shot: 6b7ed61d77d88580fed10954d45fad0eb2d47688 + react-native-webview: bdc091de8cf7f8397653e30182efcd9f772e03b3 + React-NativeModulesApple: b6868ee904013a7923128892ee4a032498a1024a React-perflogger: 31ea61077185eb1428baf60c0db6e2886f141a5a React-RCTActionSheet: 392090a3abc8992eb269ef0eaa561750588fc39d React-RCTAnimation: 4b3cc6a29474bc0d78c4f04b52ab59bf760e8a9b - React-RCTAppDelegate: b6febbe1109554fee87d3fea1c50cca511429fec - React-RCTBlob: 76113160e3cdc0f678795823c1a7c9d69b2db099 + React-RCTAppDelegate: 89b015b29885109addcabecdf3b2e833905437c7 + React-RCTBlob: 3e23dcbe6638897b5605e46d0d62955d78e8d27b React-RCTImage: 8a5d339d614a90a183fc1b8b6a7eb44e2e703943 React-RCTLinking: b37dfbf646d77c326f9eae094b1fcd575b1c24c7 React-RCTNetwork: 8bed9b2461c7d8a7d14e63df9b16181c448beebc @@ -1148,42 +1148,42 @@ SPEC CHECKSUMS: React-RCTVibration: d1b78ca38f61ea4b3e9ebb2ddbd0b5662631d99b React-rncore: bfc2f6568b6fecbae6f2f774e95c60c3c9e95bf2 React-runtimeexecutor: 47b0a2d5bbb416db65ef881a6f7bdcfefa0001ab - React-runtimescheduler: d12a963f61390fcd1b957a9c9ebee3c0f775dede - React-utils: 22f94a6e85b1323ffb1b9a747a1c03c5e6eaead6 - ReactCommon: ef602e9cfb8940ad7c08aa4cdc228d802e194e5c - RNBootSplash: 21095c4567847829470786b03b6892c5efed5299 - RNCAsyncStorage: a03b770a50541a761447cea9c24536047832124d - RNCClipboard: 4abb037e8fe3b98a952564c9e0474f91c492df6d - RNDateTimePicker: 47b54bf36a41c29d75ac62a05af1b38a9a721631 - RNDeviceInfo: addb9b427c2822a2d8e94c87a136a224e0af738c - RNFBAnalytics: 81e00e4209b0a6268c2a8b262d7e451493bda824 - RNFBApp: 2b2bb0f17eb6732e2e90d9c57bfde443cd7fc681 - RNFBAppCheck: 6e2df9110387283d00ff126d3903c9f79987d1c8 - RNFBCrashlytics: 266758adee95705af20f106c767e19588a5de665 - RNFBMessaging: 4627e84e9e363953357dd122543e4223c49e6bc1 - RNFBPerf: 594a4c7bb12fb68e920e101192539da748973da8 - RNFBRemoteConfig: 4842e7c1b0bb8d2f9c2acc3b811e6395eddfe550 - RNFileViewer: 4b5d83358214347e4ab2d4ca8d5c1c90d869e251 - RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8 - RNGestureHandler: dc1acdc779554be3aa733f4a9a19bb782ec3a48c - RNImageCropPicker: 874e26cbf0ce9d06a11002cbadf29c8b7f2f5565 - RNInAppBrowser: 6d3eb68d471b9834335c664704719b8be1bfdb20 - RNKeychain: df33ae4d27df06622fc14190b790ba8749f6be76 - RNLocalize: 8bf466de4c92d4721b254aabe1ff0a1456e7b9f4 - RNNotifee: 8768d065bf1e2f9f8f347b4bd79147431c7eacd6 - RNPermissions: 4f7b81b0d457c8ec4e892cc36ef2376734a4b22c - RNQrGenerator: 60eab4f7c9e3f09db78029636fe356dca5cb585f - RNRate: 7641919330e0d6688ad885a985b4bd697ed7d14c - RNReactNativeHapticFeedback: a6fb5b7a981683bf58af43e3fb827d4b7ed87f83 - RNReanimated: 49a1e0d191bdaefe1b394eb258bc52a42bcf704c - RNScreens: a425ae50ad66d024a6e936121bf5c9fbe6a5cdc6 - RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc - RNShare: 694e19d7f74ac4c04de3a8af0649e9ccc03bd8b1 - RNSVG: 6d5ed33b6635ed6d6ecb50744dcf127580c39ed5 - RNVectorIcons: 2bb1ff267624f4e79188d65908c959fd284c5003 + React-runtimescheduler: 7649c3b46c8dee1853691ecf60146a16ae59253c + React-utils: 56838edeaaf651220d1e53cd0b8934fb8ce68415 + ReactCommon: 5f704096ccf7733b390f59043b6fa9cc180ee4f6 + RNBootSplash: 85f6b879c080e958afdb4c62ee04497b05fd7552 + RNCAsyncStorage: 618d03a5f52fbccb3d7010076bc54712844c18ef + RNCClipboard: 60fed4b71560d7bfe40e9d35dea9762b024da86d + RNDateTimePicker: 65e1d202799460b286ff5e741d8baf54695e8abd + RNDeviceInfo: db5c64a060e66e5db3102d041ebe3ef307a85120 + RNFBAnalytics: 8d1705b9076df1ed0b72a165d78c303e8569807b + RNFBApp: fa5825b36d8362ce9b993cac8bf070d116848640 + RNFBAppCheck: f98d1bd525efe4e22f28c6070796434c9baf6e9a + RNFBCrashlytics: 14dcfb073e7d9f41189400128e203d5314a8c606 + RNFBMessaging: 32a107c0463048f1c03df06ab235bf56e976299a + RNFBPerf: a3f48919b73a6355113735a463f048a772fc79a4 + RNFBRemoteConfig: f2d4de1a288ede64e777f3229c7746ed4127ccb3 + RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592 + RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 + RNGestureHandler: 32a01c29ecc9bb0b5bf7bc0a33547f61b4dc2741 + RNImageCropPicker: 14fe1c29298fb4018f3186f455c475ab107da332 + RNInAppBrowser: e36d6935517101ccba0e875bac8ad7b0cb655364 + RNKeychain: a65256b6ca6ba6976132cc4124b238a5b13b3d9c + RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 + RNNotifee: 8e2d3df3f0e9ce8f5d1fe4c967431138190b6175 + RNPermissions: 294531ede5a64d1528463e88fffef05675842042 + RNQrGenerator: 1676221c08bfabec978242989c733810dad20959 + RNRate: ef3bcff84f39bb1d1e41c5593d3eea4aab2bd73a + RNReactNativeHapticFeedback: ec56a5f81c3941206fd85625fa669ffc7b4545f9 + RNReanimated: fdbaa9c964bbab7fac50c90862b6cc5f041679b9 + RNScreens: 3c5b9f4a9dcde752466854b6109b79c0e205dad3 + RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef + RNShare: 0fad69ae2d71de9d1f7b9a43acf876886a6cb99c + RNSVG: ba3e7232f45e34b7b47e74472386cf4e1a676d0a + RNVectorIcons: 64e6a523ac30a3241efa9baf1ffbcc5e76ff747a SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 - VisionCamera: 29095ffe0a146b6254c3db34636d10298b169f36 + VisionCamera: 1910a51e4c6f6b049650086d343090f267b4c260 Yoga: 4c3aa327e4a6a23eeacd71f61c81df1bcdf677d5 ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 From b23328a5823f185f247e49afd551a97180b1a2ec Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Tue, 22 Jul 2025 19:15:42 +0500 Subject: [PATCH 05/95] add BankTransfer texts on the i18n file --- app/i18n/en/index.ts | 18 ++++ app/i18n/i18n-types.ts | 142 +++++++++++++++++++++++++++++++ app/i18n/raw-i18n/source/en.json | 18 ++++ 3 files changed, 178 insertions(+) diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index b436a140a..eb581ee37 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -595,6 +595,24 @@ const en: BaseTranslation = { error: "Failed to load payment page", retry: "Retry" }, + BankTransfer: { + title: "Bank Transfer", + desc1: "Your order has been created. To complete the order, please transfer ${amount: number} USD to the bank details provided below.", + desc2: "Use {code: string} as the reference description. This unique code will help us associate the payment with your Flash account and process the Bitcoin transfer.", + desc3: "After we have received your payment, you will be credited with ${amount: number} USD in your Cash wallet, with a ${fee: number} USD fee deducted. You can then choose when you convert those USD to Bitcoin on your own using the Convert functionality in the mobile app.", + accountType: "Account Type", + destinationBank: "Destination Bank", + accountNumber: "Account Number", + typeOfClient: "Type of Client", + receiverName: "Receiver's Name", + email: "Email", + amount: "Amount", + uniqueCode: "Unique Code", + fees: "Fees", + desc4: "After payment completion on your end you can send us an email to {email: string} with a screenshot of your payment confirmation.", + desc5: "Your payment will be processed even if we don't receive this email, but having this confirmation can help accelerate the order.", + backHome: "Back to Home" + }, PaymentSuccessScreen: { title: "Payment Successful", successMessage: "Your payment has been processed successfully", diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index 80f71ed5c..716738f12 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -1879,6 +1879,77 @@ type RootTranslation = { */ retry: string } + BankTransfer: { + /** + * Bank Transfer + */ + title: string + /** + * Your order has been created. To complete the order, please transfer ${amount} USD to the bank details provided below. + * @param {number} amount + */ + desc1: string + /** + * Use {code} as the reference description. This unique code will help us associate the payment with your Flash account and process the Bitcoin transfer. + * @param {string} code + */ + desc2: string + /** + * After we have received your payment, you will be credited with ${amount} USD in your Cash wallet, with a ${fee} USD fee deducted. You can then choose when you convert those USD to Bitcoin on your own using the Convert functionality in the mobile app. + * @param {number} amount + * @param {number} fee + */ + desc3: string + /** + * Account Type + */ + accountType: string + /** + * Destination Bank + */ + destinationBank: string + /** + * Account Number + */ + accountNumber: string + /** + * Type of Client + */ + typeOfClient: string + /** + * Receiver's Name + */ + receiverName: string + /** + * Email + */ + email: string + /** + * Amount + */ + amount: string + /** + * Unique Code + */ + uniqueCode: string + /** + * Fees + */ + fees: string + /** + * After payment completion on your end you can send us an email to {email} with a screenshot of your payment confirmation. + * @param {number} email + */ + desc4: string + /** + * Your payment will be processed even if we don't receive this email, but having this confirmation can help accelerate the order. + */ + desc5: string + /** + * Back to Home + */ + backHome: string + } PaymentSuccessScreen: { /** * P​a​y​m​e​n​t​ ​S​u​c​c​e​s​s​f​u​l @@ -6759,6 +6830,77 @@ export type TranslationFunctions = { */ retry: () => LocalizedString } + BankTransfer: { + /** + * Bank Transfer + */ + title: () => LocalizedString + /** + * Your order has been created. To complete the order, please transfer ${amount} USD to the bank details provided below. + * @param {number} amount + */ + desc1: (arg: { amount: number }) => LocalizedString + /** + * Use {code} as the reference description. This unique code will help us associate the payment with your Flash account and process the Bitcoin transfer. + * @param {string} code + */ + desc2: (arg: { code: string }) => LocalizedString + /** + * After we have received your payment, you will be credited with ${amount} USD in your Cash wallet, with a ${fee} USD fee deducted. You can then choose when you convert those USD to Bitcoin on your own using the Convert functionality in the mobile app. + * @param {number} amount + * @param {number} fee + */ + desc3: (arg: { amount: number, fee: number }) => LocalizedString + /** + * Account Type + */ + accountType: () => LocalizedString + /** + * Destination Bank + */ + destinationBank: () => LocalizedString + /** + * Account Number + */ + accountNumber: () => LocalizedString + /** + * Type of Client + */ + typeOfClient: () => LocalizedString + /** + * Receiver's Name + */ + receiverName: () => LocalizedString + /** + * Email + */ + email: () => LocalizedString + /** + * Amount + */ + amount: () => LocalizedString + /** + * Unique Code + */ + uniqueCode: () => LocalizedString + /** + * Fees + */ + fees: () => LocalizedString + /** + * After payment completion on your end you can send us an email to {email} with a screenshot of your payment confirmation. + * @param {string} email + */ + desc4: (arg: { email: string }) => LocalizedString + /** + * Your payment will be processed even if we don't receive this email, but having this confirmation can help accelerate the order. + */ + desc5: () => LocalizedString + /** + * Back to Home + */ + backHome: () => LocalizedString + } PaymentSuccessScreen: { /** * Payment Successful diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index eae85ec14..de51ef8ff 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -564,6 +564,24 @@ "error": "Failed to load payment page", "retry": "Retry" }, + "BankTransfer": { + "title": "Bank Transfer", + "desc1": "Your order has been created. To complete the order, please transfer ${amount: number} USD to the bank details provided below.", + "desc2": "Use {code: string} as the reference description. This unique code will help us associate the payment with your Flash account and process the Bitcoin transfer.", + "desc3": "After we have received your payment, you will be credited with ${amount: number} USD in your Cash wallet, with a ${fee: number} USD fee deducted. You can then choose when you convert those USD to Bitcoin on your own using the Convert functionality in the mobile app.", + "accountType": "Account Type", + "destinationBank": "Destination Bank", + "accountNumber": "Account Number", + "typeOfClient": "Type of Client", + "receiverName": "Receiver's Name", + "email": "Email", + "amount": "Amount", + "uniqueCode": "Unique Code", + "fees": "Fees", + "desc4": "After payment completion on your end you can send us an email to {email: string} with a screenshot of your payment confirmation.", + "desc5": "Your payment will be processed even if we don't receive this email, but having this confirmation can help accelerate the order.", + "backHome": "Back to Home" + }, "PaymentSuccessScreen": { "title": "Payment Successful", "successMessage": "Your payment has been processed successfully", From 26c64adba08df611cfc174460ca9aed97b641fca Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Tue, 22 Jul 2025 19:16:56 +0500 Subject: [PATCH 06/95] refactor and clean up CardPayment webview screen --- app/navigation/root-navigator.tsx | 2 + ...aro-webview-screen.tsx => CardPayment.tsx} | 38 ++++++++++++------- app/screens/buy-bitcoin-flow/index.ts | 3 +- 3 files changed, 28 insertions(+), 15 deletions(-) rename app/screens/buy-bitcoin-flow/{fygaro-webview-screen.tsx => CardPayment.tsx} (85%) diff --git a/app/navigation/root-navigator.tsx b/app/navigation/root-navigator.tsx index 881959cfc..4f0de36e0 100644 --- a/app/navigation/root-navigator.tsx +++ b/app/navigation/root-navigator.tsx @@ -121,6 +121,7 @@ import { BuyBitcoin, BuyBitcoinDetails, BuySellBitcoin, + CardPayment, } from "@app/screens/buy-bitcoin-flow" const useStyles = makeStyles(({ colors }) => ({ @@ -591,6 +592,7 @@ export const RootStack = () => { + ) diff --git a/app/screens/buy-bitcoin-flow/fygaro-webview-screen.tsx b/app/screens/buy-bitcoin-flow/CardPayment.tsx similarity index 85% rename from app/screens/buy-bitcoin-flow/fygaro-webview-screen.tsx rename to app/screens/buy-bitcoin-flow/CardPayment.tsx index ac41f8946..ddad5856c 100644 --- a/app/screens/buy-bitcoin-flow/fygaro-webview-screen.tsx +++ b/app/screens/buy-bitcoin-flow/CardPayment.tsx @@ -1,31 +1,41 @@ import React, { useState, useRef } from "react" import { View, ActivityIndicator, Alert } from "react-native" -import { useNavigation, useRoute, RouteProp } from "@react-navigation/native" -import { StackNavigationProp } from "@react-navigation/stack" +import { StackScreenProps } from "@react-navigation/stack" import { Text, makeStyles, useTheme } from "@rneui/themed" +import { RootStackParamList } from "@app/navigation/stack-param-lists" import { WebView } from "react-native-webview" + +// components import { Screen } from "@app/components/screen" -import { useI18nContext } from "@app/i18n/i18n-react" -import { RootStackParamList } from "@app/navigation/stack-param-lists" import { PrimaryBtn } from "@app/components/buttons" -type FygaroWebViewScreenProps = { - navigation: StackNavigationProp - route: RouteProp -} +// hooks +import { useI18nContext } from "@app/i18n/i18n-react" +import { useHomeAuthedQuery } from "@app/graphql/generated" +import { useIsAuthed } from "@app/graphql/is-authed-context" -const FygaroWebViewScreen: React.FC = () => { +type Props = StackScreenProps + +const CardPayment: React.FC = ({ navigation, route }) => { + const isAuthed = useIsAuthed() + const styles = useStyles() const { colors } = useTheme().theme const { LL } = useI18nContext() - const navigation = useNavigation>() - const route = useRoute>() + const webViewRef = useRef(null) - const styles = useStyles() const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(false) - const { amount, wallet, paymentUrl } = route.params + const { data } = useHomeAuthedQuery({ + skip: !isAuthed, + fetchPolicy: "cache-first", + }) + + const { email, amount, wallet } = route.params + + const username = data?.me?.username || "user" + const paymentUrl = `https://fygaro.com/en/pb/bd4a34c1-3d24-4315-a2b8-627518f70916?amount=${amount}&client_reference=${username}` const handleNavigationStateChange = (navState: { url: string }) => { const { url } = navState @@ -177,4 +187,4 @@ const useStyles = makeStyles(() => ({ }, })) -export default FygaroWebViewScreen +export default CardPayment diff --git a/app/screens/buy-bitcoin-flow/index.ts b/app/screens/buy-bitcoin-flow/index.ts index 08f46aa85..b413f29b6 100644 --- a/app/screens/buy-bitcoin-flow/index.ts +++ b/app/screens/buy-bitcoin-flow/index.ts @@ -2,5 +2,6 @@ import BuySellBitcoin from "./buy-sell-bitcoin" import BuyBitcoin from "./BuyBitcoin" import BuyBitcoinDetails from "./BuyBitcoinDetails" import BankTransfer from "./BankTransfer" +import CardPayment from "./CardPayment" -export { BuySellBitcoin, BuyBitcoin, BuyBitcoinDetails, BankTransfer } +export { BuySellBitcoin, BuyBitcoin, BuyBitcoinDetails, BankTransfer, CardPayment } From 71f1e49e4931624dace1e3452a7ba1c55d71d625 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Tue, 22 Jul 2025 19:18:32 +0500 Subject: [PATCH 07/95] use texts from i18n on the BankTransfer screen and update BuyBitcoinDetails screen navigation logic --- app/navigation/stack-param-lists.ts | 11 ++-- app/screens/buy-bitcoin-flow/BankTransfer.tsx | 56 +++++++++---------- .../buy-bitcoin-flow/BuyBitcoinDetails.tsx | 33 +++++------ 3 files changed, 48 insertions(+), 52 deletions(-) diff --git a/app/navigation/stack-param-lists.ts b/app/navigation/stack-param-lists.ts index 21c82484b..3e595639e 100644 --- a/app/navigation/stack-param-lists.ts +++ b/app/navigation/stack-param-lists.ts @@ -151,14 +151,15 @@ export type RootStackParamList = { BuySellBitcoin: undefined BuyBitcoin: undefined BuyBitcoinDetails: { paymentType: "card" | "bankTransfer" } - BankTransfer: undefined - fygaroWebView: { + BankTransfer: { + email: string amount: number + wallet: string + } + CardPayment: { email: string + amount: number wallet: string - sessionId: string - paymentUrl: string - username: string } paymentSuccess: { amount: number diff --git a/app/screens/buy-bitcoin-flow/BankTransfer.tsx b/app/screens/buy-bitcoin-flow/BankTransfer.tsx index d40e3cb27..c5e4d96d9 100644 --- a/app/screens/buy-bitcoin-flow/BankTransfer.tsx +++ b/app/screens/buy-bitcoin-flow/BankTransfer.tsx @@ -2,105 +2,105 @@ import React from "react" import { View } from "react-native" import { makeStyles, Text } from "@rneui/themed" import { StackScreenProps } from "@react-navigation/stack" -import { useSafeAreaInsets } from "react-native-safe-area-context" import { RootStackParamList } from "@app/navigation/stack-param-lists" +// hooks +import { useI18nContext } from "@app/i18n/i18n-react" +import { useSafeAreaInsets } from "react-native-safe-area-context" + // components import { Screen } from "@app/components/screen" import { PrimaryBtn } from "@app/components/buttons" type Props = StackScreenProps -const BankTransfer: React.FC = ({ navigation }) => { +const BankTransfer: React.FC = ({ navigation, route }) => { const styles = useStyles() const { bottom } = useSafeAreaInsets() + const { LL } = useI18nContext() + + const { email, amount, wallet } = route.params + const fee = amount * 0.02 return ( - Bank Transfer + {LL.BankTransfer.title()} - Your order has been created. To complete the order, please transfer $102 USD to - the bank details provided below. + {LL.BankTransfer.desc1({ amount: amount + fee })} - Use UUM7MJRD as the reference description. This unique code will help us associate - the payment with your Flash account and process the Bitcoin transfer. + {LL.BankTransfer.desc2({ code: "UUM7MJRD" })} - After we have received your payment, you will be credited with $100 USD in your - Cash wallet, with a $2 USD fee deducted. You can then choose when you convert - those USD to Bitcoin on your own using the Convert functionality in the mobile - app. + {LL.BankTransfer.desc3({ amount: amount, fee: fee })} - Account Type + {LL.BankTransfer.accountType()} Checking - Destination Bank + {LL.BankTransfer.destinationBank()} Banco Hipotecario - Account Number + {LL.BankTransfer.accountNumber()} 00210312362 - Type of Client + {LL.BankTransfer.typeOfClient()} Corporate - Receiver's Name + {LL.BankTransfer.receiverName()} BBW SA de CV - Email + {LL.BankTransfer.email()} fiat@blink.sv - Amount + {LL.BankTransfer.amount()} - 102 USD + {`${amount + fee} USD`} - Unique Code + {LL.BankTransfer.uniqueCode()} UUM7MJRD - Fees + {LL.BankTransfer.fees()} - 2 USD + {`${fee} USD`} - After payment completion on your end you can send us an email to fiat@blink.sv - with a screenshot of your payment confirmation. + {LL.BankTransfer.desc4({ email: "fiat@blink.sv" })} - Your payment will be processed even if we don't receive this email, but having - this confirmation can help accelerate the order. + {LL.BankTransfer.desc5()} {}} + label={LL.BankTransfer.backHome()} + onPress={() => navigation.reset({ index: 0, routes: [{ name: "Primary" }] })} btnStyle={{ marginBottom: bottom + 20, marginTop: 20 }} /> diff --git a/app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx b/app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx index 6a84b5931..7ec9db11a 100644 --- a/app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx +++ b/app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react" import { View, TextInput, Alert } from "react-native" import { Text, makeStyles, useTheme } from "@rneui/themed" -import { StackNavigationProp, StackScreenProps } from "@react-navigation/stack" +import { StackScreenProps } from "@react-navigation/stack" import { RootStackParamList } from "@app/navigation/stack-param-lists" // components @@ -68,24 +68,19 @@ const BuyBitcoinDetails: React.FC = ({ navigation, route }) => { setIsLoading(true) try { - // Mock API call to initiate payment - const username = data?.me?.username || "user" - const paymentUrl = `https://fygaro.com/en/pb/bd4a34c1-3d24-4315-a2b8-627518f70916?amount=${amount}&client_reference=${username}` - const sessionId = `session_${Date.now()}` - - // Simulate API delay - await new Promise((resolve) => { - setTimeout(() => resolve(), 1000) - }) - - navigation.navigate("fygaroWebView", { - amount: parseFloat(amount), - email, - wallet: selectedWallet, - sessionId, - paymentUrl, - username, - }) + if (route.params.paymentType === "bankTransfer") { + navigation.navigate("BankTransfer", { + email, + amount: parseFloat(amount), + wallet: selectedWallet, + }) + } else { + navigation.navigate("CardPayment", { + email, + amount: parseFloat(amount), + wallet: selectedWallet, + }) + } } catch (error) { Alert.alert("Error", "Failed to initiate payment. Please try again.") } finally { From 35458a6bd7672948a25e13e477f4288d50703236 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Wed, 23 Jul 2025 18:45:21 +0500 Subject: [PATCH 08/95] update BuySellBitcoin screen icons and added navigation to cashout --- app/assets/icons/arrow-down-to-bracket.svg | 3 +++ app/assets/icons/arrow-up-from-bracket.svg | 3 +++ ...uy-sell-bitcoin.tsx => BuySellBitcoin.tsx} | 22 ++++++------------- app/screens/buy-bitcoin-flow/index.ts | 2 +- 4 files changed, 14 insertions(+), 16 deletions(-) create mode 100644 app/assets/icons/arrow-down-to-bracket.svg create mode 100644 app/assets/icons/arrow-up-from-bracket.svg rename app/screens/buy-bitcoin-flow/{buy-sell-bitcoin.tsx => BuySellBitcoin.tsx} (82%) diff --git a/app/assets/icons/arrow-down-to-bracket.svg b/app/assets/icons/arrow-down-to-bracket.svg new file mode 100644 index 000000000..b2fa9fff6 --- /dev/null +++ b/app/assets/icons/arrow-down-to-bracket.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/arrow-up-from-bracket.svg b/app/assets/icons/arrow-up-from-bracket.svg new file mode 100644 index 000000000..7ce3e68b0 --- /dev/null +++ b/app/assets/icons/arrow-up-from-bracket.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/screens/buy-bitcoin-flow/buy-sell-bitcoin.tsx b/app/screens/buy-bitcoin-flow/BuySellBitcoin.tsx similarity index 82% rename from app/screens/buy-bitcoin-flow/buy-sell-bitcoin.tsx rename to app/screens/buy-bitcoin-flow/BuySellBitcoin.tsx index 7c71e99eb..f2a4c3169 100644 --- a/app/screens/buy-bitcoin-flow/buy-sell-bitcoin.tsx +++ b/app/screens/buy-bitcoin-flow/BuySellBitcoin.tsx @@ -8,6 +8,10 @@ import { useI18nContext } from "@app/i18n/i18n-react" // components import { Screen } from "@app/components/screen" +// assets +import ArrowDown from "@app/assets/icons/arrow-down-to-bracket.svg" +import ArrowUp from "@app/assets/icons/arrow-up-from-bracket.svg" + type Props = StackScreenProps const BuySellBitcoin: React.FC = ({ navigation }) => { @@ -20,8 +24,7 @@ const BuySellBitcoin: React.FC = ({ navigation }) => { } const handleSettle = () => { - // TODO: Implement settle functionality - console.log("Settle functionality not implemented yet") + navigation.navigate("CashoutDetails") } return ( @@ -31,12 +34,7 @@ const BuySellBitcoin: React.FC = ({ navigation }) => { {LL.TransferScreen.title()} - + {LL.TransferScreen.topUp()} @@ -46,13 +44,7 @@ const BuySellBitcoin: React.FC = ({ navigation }) => { - - + {LL.TransferScreen.settle()} diff --git a/app/screens/buy-bitcoin-flow/index.ts b/app/screens/buy-bitcoin-flow/index.ts index b413f29b6..5d73c7892 100644 --- a/app/screens/buy-bitcoin-flow/index.ts +++ b/app/screens/buy-bitcoin-flow/index.ts @@ -1,4 +1,4 @@ -import BuySellBitcoin from "./buy-sell-bitcoin" +import BuySellBitcoin from "./BuySellBitcoin" import BuyBitcoin from "./BuyBitcoin" import BuyBitcoinDetails from "./BuyBitcoinDetails" import BankTransfer from "./BankTransfer" From 025c12b70401b56a45b6dc76a9482fd0cd9294f4 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Wed, 23 Jul 2025 18:46:14 +0500 Subject: [PATCH 09/95] ArrowUpDown icon btn is added --- app/assets/icons/arrow-up-down.svg | 3 +++ app/components/buttons/IconBtn.tsx | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 app/assets/icons/arrow-up-down.svg diff --git a/app/assets/icons/arrow-up-down.svg b/app/assets/icons/arrow-up-down.svg new file mode 100644 index 000000000..57c0bc041 --- /dev/null +++ b/app/assets/icons/arrow-up-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/components/buttons/IconBtn.tsx b/app/components/buttons/IconBtn.tsx index 638de9c5d..ea17a6588 100644 --- a/app/components/buttons/IconBtn.tsx +++ b/app/components/buttons/IconBtn.tsx @@ -10,6 +10,7 @@ import QR from "@app/assets/icons/qr-code-new.svg" import Setting from "@app/assets/icons/setting.svg" import CardRemove from "@app/assets/icons/card-remove.svg" import Dollar from "@app/assets/icons/dollar-new.svg" +import ArrowUpDown from "@app/assets/icons/arrow-up-down.svg" const icons = { up: ArrowUp, @@ -19,6 +20,7 @@ const icons = { setting: Setting, cardRemove: CardRemove, dollar: Dollar, + upDown: ArrowUpDown, } type IconNamesType = keyof typeof icons From 9c7a02f756c487aae1775e240a81618d4461491e Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Wed, 23 Jul 2025 19:01:32 +0500 Subject: [PATCH 10/95] add back cashout mutations --- app/graphql/front-end-mutations.ts | 59 ++++++----- app/graphql/generated.gql | 35 +++++++ app/graphql/generated.ts | 160 +++++++++++++++++++++++++++++ codegen.yml | 2 + 4 files changed, 226 insertions(+), 30 deletions(-) diff --git a/app/graphql/front-end-mutations.ts b/app/graphql/front-end-mutations.ts index 2969263c6..557f888b4 100644 --- a/app/graphql/front-end-mutations.ts +++ b/app/graphql/front-end-mutations.ts @@ -117,36 +117,35 @@ gql` } } - # mutation RequestCashout($input: RequestCashoutInput!) { - # requestCashout(input: $input) { - # errors { - # code - # message - # path - # } - # offer { - # exchangeRate - # expiresAt - # flashFee - # offerId - # receiveJmd - # receiveUsd - # send - # walletId - # } - # } - # } - - # mutation InitiateCashout($input: InitiateCashoutInput!) { - # initiateCashout(input: $input) { - # errors { - # path - # message - # code - # } - # success - # } - # } + mutation RequestCashout($input: RequestCashoutInput!) { + requestCashout(input: $input) { + errors { + code + message + path + } + offer { + expiresAt + flashFee + offerId + receiveJmd + receiveUsd + send + walletId + } + } + } + + mutation InitiateCashout($input: InitiateCashoutInput!) { + initiateCashout(input: $input) { + errors { + path + message + code + } + success + } + } mutation accountDelete { accountDelete { diff --git a/app/graphql/generated.gql b/app/graphql/generated.gql index 6c366bc74..5c04798b5 100644 --- a/app/graphql/generated.gql +++ b/app/graphql/generated.gql @@ -81,6 +81,19 @@ fragment TransactionList on TransactionConnection { __typename } +mutation InitiateCashout($input: InitiateCashoutInput!) { + initiateCashout(input: $input) { + errors { + path + message + code + __typename + } + success + __typename + } +} + mutation MerchantMapSuggest($input: MerchantMapSuggestInput!) { merchantMapSuggest(input: $input) { errors { @@ -106,6 +119,28 @@ mutation MerchantMapSuggest($input: MerchantMapSuggestInput!) { } } +mutation RequestCashout($input: RequestCashoutInput!) { + requestCashout(input: $input) { + errors { + code + message + path + __typename + } + offer { + expiresAt + flashFee + offerId + receiveJmd + receiveUsd + send + walletId + __typename + } + __typename + } +} + mutation accountDelete { accountDelete { errors { diff --git a/app/graphql/generated.ts b/app/graphql/generated.ts index bcfee8d28..ea6f4770f 100644 --- a/app/graphql/generated.ts +++ b/app/graphql/generated.ts @@ -40,6 +40,8 @@ export type Scalars = { FractionalCentAmount: { input: number; output: number; } /** Hex-encoded string of 32 bytes */ Hex32Bytes: { input: string; output: string; } + /** Amount in Jamaican cents */ + JMDCents: { input: number; output: number; } Language: { input: string; output: string; } LnPaymentPreImage: { input: string; output: string; } /** BOLT11 lightning invoice payment request with the amount included */ @@ -78,6 +80,8 @@ export type Scalars = { TotpRegistrationId: { input: string; output: string; } /** A secret to generate time-based one-time password */ TotpSecret: { input: string; output: string; } + /** Amount in USD cents */ + USDCents: { input: number; output: number; } /** Unique identifier of a user */ Username: { input: string; output: string; } /** Unique identifier of a wallet */ @@ -143,6 +147,7 @@ export type AccountEnableNotificationChannelInput = { export const AccountLevel = { One: 'ONE', + Three: 'THREE', Two: 'TWO', Zero: 'ZERO' } as const; @@ -284,6 +289,24 @@ export type CaptchaRequestAuthCodeInput = { readonly validationCode: Scalars['String']['input']; }; +export type CashoutOffer = { + readonly __typename: 'CashoutOffer'; + /** The time at which this offer is no longer accepted by Flash */ + readonly expiresAt: Scalars['Timestamp']['output']; + /** The amount that Flash is charging for it's services */ + readonly flashFee: Scalars['USDCents']['output']; + /** ID of the offer */ + readonly offerId: Scalars['ID']['output']; + /** The amount Flash owes to the user denominated in JMD as cents */ + readonly receiveJmd: Scalars['JMDCents']['output']; + /** The amount Flash owes to the user denominated in USD as cents */ + readonly receiveUsd: Scalars['USDCents']['output']; + /** The amount the user is sending to flash */ + readonly send: Scalars['USDCents']['output']; + /** ID for the users USD wallet to send from */ + readonly walletId: Scalars['WalletId']['output']; +}; + export type CentAmountPayload = { readonly __typename: 'CentAmountPayload'; readonly amount?: Maybe; @@ -418,6 +441,12 @@ export type GraphQlApplicationError = Error & { readonly path?: Maybe>>; }; +export type InitiateCashoutInput = { + /** The id of the offer being executed. */ + readonly offerId: Scalars['ID']['input']; + readonly walletId: Scalars['WalletId']['input']; +}; + export type InitiationVia = InitiationViaIntraLedger | InitiationViaLn | InitiationViaOnChain; export type InitiationViaIntraLedger = { @@ -704,6 +733,11 @@ export type Mutation = { readonly captchaRequestAuthCode: SuccessPayload; readonly deviceNotificationTokenCreate: SuccessPayload; readonly feedbackSubmit: SuccessPayload; + /** + * Start the Cashout process; + * User sends USD to Flash via Ibex and receives USD or JMD to bank account. + */ + readonly initiateCashout: SuccessPayload; /** * Actions a payment which is internal to the ledger e.g. it does * not use onchain/lightning. Returns payment status (success, @@ -786,6 +820,11 @@ export type Mutation = { readonly onChainUsdPaymentSend: PaymentSendPayload; readonly onChainUsdPaymentSendAsBtcDenominated: PaymentSendPayload; readonly quizCompleted: QuizCompletedPayload; + /** + * Returns an offer from Flash for a user to withdraw from their USD wallet (denominated in cents). + * The user can review this offer and then execute the withdrawal by calling the initiateCashout mutation. + */ + readonly requestCashout: RequestCashoutResponse; /** @deprecated will be moved to AccountContact */ readonly userContactUpdateAlias: UserContactUpdateAliasPayload; readonly userEmailDelete: UserEmailDeletePayload; @@ -864,6 +903,11 @@ export type MutationFeedbackSubmitArgs = { }; +export type MutationInitiateCashoutArgs = { + input: InitiateCashoutInput; +}; + + export type MutationIntraLedgerPaymentSendArgs = { input: IntraLedgerPaymentSendInput; }; @@ -979,6 +1023,11 @@ export type MutationQuizCompletedArgs = { }; +export type MutationRequestCashoutArgs = { + input: RequestCashoutInput; +}; + + export type MutationUserContactUpdateAliasArgs = { input: UserContactUpdateAliasInput; }; @@ -1428,6 +1477,19 @@ export type RealtimePricePayload = { readonly realtimePrice?: Maybe; }; +export type RequestCashoutInput = { + /** Amount in USD cents. */ + readonly amount: Scalars['USDCents']['input']; + /** ID for a USD wallet belonging to the current user. */ + readonly walletId: Scalars['WalletId']['input']; +}; + +export type RequestCashoutResponse = { + readonly __typename: 'RequestCashoutResponse'; + readonly errors: ReadonlyArray; + readonly offer?: Maybe; +}; + export type SatAmountPayload = { readonly __typename: 'SatAmountPayload'; readonly amount?: Maybe; @@ -2028,6 +2090,20 @@ export type MerchantMapSuggestMutationVariables = Exact<{ export type MerchantMapSuggestMutation = { readonly __typename: 'Mutation', readonly merchantMapSuggest: { readonly __typename: 'MerchantPayload', readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string, readonly path?: ReadonlyArray | null }>, readonly merchant?: { readonly __typename: 'Merchant', readonly createdAt: number, readonly id: string, readonly title: string, readonly username: string, readonly validated: boolean, readonly coordinates: { readonly __typename: 'Coordinates', readonly latitude: number, readonly longitude: number } } | null } }; +export type RequestCashoutMutationVariables = Exact<{ + input: RequestCashoutInput; +}>; + + +export type RequestCashoutMutation = { readonly __typename: 'Mutation', readonly requestCashout: { readonly __typename: 'RequestCashoutResponse', readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string, readonly path?: ReadonlyArray | null }>, readonly offer?: { readonly __typename: 'CashoutOffer', readonly expiresAt: number, readonly flashFee: number, readonly offerId: string, readonly receiveJmd: number, readonly receiveUsd: number, readonly send: number, readonly walletId: string } | null } }; + +export type InitiateCashoutMutationVariables = Exact<{ + input: InitiateCashoutInput; +}>; + + +export type InitiateCashoutMutation = { readonly __typename: 'Mutation', readonly initiateCashout: { readonly __typename: 'SuccessPayload', readonly success?: boolean | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly path?: ReadonlyArray | null, readonly message: string, readonly code?: string | null }> } }; + export type AccountDeleteMutationVariables = Exact<{ [key: string]: never; }>; @@ -3434,6 +3510,90 @@ export function useMerchantMapSuggestMutation(baseOptions?: Apollo.MutationHookO export type MerchantMapSuggestMutationHookResult = ReturnType; export type MerchantMapSuggestMutationResult = Apollo.MutationResult; export type MerchantMapSuggestMutationOptions = Apollo.BaseMutationOptions; +export const RequestCashoutDocument = gql` + mutation RequestCashout($input: RequestCashoutInput!) { + requestCashout(input: $input) { + errors { + code + message + path + } + offer { + expiresAt + flashFee + offerId + receiveJmd + receiveUsd + send + walletId + } + } +} + `; +export type RequestCashoutMutationFn = Apollo.MutationFunction; + +/** + * __useRequestCashoutMutation__ + * + * To run a mutation, you first call `useRequestCashoutMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useRequestCashoutMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [requestCashoutMutation, { data, loading, error }] = useRequestCashoutMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useRequestCashoutMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(RequestCashoutDocument, options); + } +export type RequestCashoutMutationHookResult = ReturnType; +export type RequestCashoutMutationResult = Apollo.MutationResult; +export type RequestCashoutMutationOptions = Apollo.BaseMutationOptions; +export const InitiateCashoutDocument = gql` + mutation InitiateCashout($input: InitiateCashoutInput!) { + initiateCashout(input: $input) { + errors { + path + message + code + } + success + } +} + `; +export type InitiateCashoutMutationFn = Apollo.MutationFunction; + +/** + * __useInitiateCashoutMutation__ + * + * To run a mutation, you first call `useInitiateCashoutMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useInitiateCashoutMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [initiateCashoutMutation, { data, loading, error }] = useInitiateCashoutMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useInitiateCashoutMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(InitiateCashoutDocument, options); + } +export type InitiateCashoutMutationHookResult = ReturnType; +export type InitiateCashoutMutationResult = Apollo.MutationResult; +export type InitiateCashoutMutationOptions = Apollo.BaseMutationOptions; export const AccountDeleteDocument = gql` mutation accountDelete { accountDelete { diff --git a/codegen.yml b/codegen.yml index daa9a13a8..2b4c7668f 100644 --- a/codegen.yml +++ b/codegen.yml @@ -68,3 +68,5 @@ generates: npub: "string" FractionalCentAmount: "number" JmdAmount: "string" + JMDCents: "number" + USDCents: "number" From 04b6a9a8461ae8f106b028485dd479a206618b17 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Wed, 23 Jul 2025 19:02:18 +0500 Subject: [PATCH 11/95] show Transfer button instead of Cashout button on the home screen --- app/components/home-screen/Buttons.tsx | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/app/components/home-screen/Buttons.tsx b/app/components/home-screen/Buttons.tsx index a177347a0..324232a11 100644 --- a/app/components/home-screen/Buttons.tsx +++ b/app/components/home-screen/Buttons.tsx @@ -58,11 +58,6 @@ const Buttons: React.FC = ({ setModalVisible, setDefaultAccountModalVisib target: "receiveBitcoin", icon: "down", }, - { - title: LL.HomeScreen.transfer(), - target: "BuySellBitcoin", - icon: "swap", - }, ] if (persistentState.isAdvanceMode) { @@ -73,13 +68,13 @@ const Buttons: React.FC = ({ setModalVisible, setDefaultAccountModalVisib }) } - // if (currentLevel === AccountLevel.Two) { - // buttons.push({ - // title: LL.Cashout.title(), - // target: "CashoutDetails", - // icon: "dollar", - // }) - // } + if (currentLevel === AccountLevel.Two) { + buttons.push({ + title: LL.HomeScreen.transfer(), + target: "BuySellBitcoin", + icon: "upDown", + }) + } return ( From b41ea81552f68b3ac709ac1d65fdf1fd0ab6d674 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Wed, 23 Jul 2025 19:02:43 +0500 Subject: [PATCH 12/95] update CashoutDetails screen --- app/screens/cashout-screen/CashoutDetails.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/screens/cashout-screen/CashoutDetails.tsx b/app/screens/cashout-screen/CashoutDetails.tsx index d1c419ed6..79ef9a0b1 100644 --- a/app/screens/cashout-screen/CashoutDetails.tsx +++ b/app/screens/cashout-screen/CashoutDetails.tsx @@ -24,12 +24,14 @@ import { import { getUsdWallet } from "@app/graphql/wallets-utils" import { View } from "react-native" import { PrimaryBtn } from "@app/components/buttons" +import { useSafeAreaInsets } from "react-native-safe-area-context" type Props = StackScreenProps const CashoutDetails = ({ navigation }: Props) => { const styles = useStyles() const { colors } = useTheme().theme + const { bottom } = useSafeAreaInsets() const { LL } = useI18nContext() const { zeroDisplayAmount } = useDisplayCurrency() const { convertMoneyAmount } = usePriceConversion() @@ -61,7 +63,7 @@ const CashoutDetails = ({ navigation }: Props) => { toggleActivityIndicator(true) const res = await requestCashout({ variables: { - input: { walletId: usdWallet.id, usdAmount: settlementSendAmount.amount }, + input: { walletId: usdWallet.id, amount: settlementSendAmount.amount }, }, }) console.log("Response: ", res.data?.requestCashout) @@ -116,7 +118,7 @@ const CashoutDetails = ({ navigation }: Props) => { @@ -132,8 +134,4 @@ const useStyles = makeStyles(() => ({ flexDirection: "column", margin: 20, }, - buttonContainer: { - marginHorizontal: 20, - marginBottom: 20, - }, })) From cccfcee3ced657e354052d0362b34faef4a72b33 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Fri, 25 Jul 2025 10:31:23 +0500 Subject: [PATCH 13/95] update cashout description text --- app/i18n/en/index.ts | 2 +- app/i18n/i18n-types.ts | 2 +- app/i18n/raw-i18n/source/en.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index eb581ee37..a31ca73bb 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -1146,7 +1146,7 @@ const en: BaseTranslation = { topUp: "Top Up", topUpDesc: "Add funds to your wallet", settle: "Settle", - settleDesc: "Settle pending transactions" + settleDesc: "Cashout funds from your wallet" }, UpgradeAccountModal: { title: "Upgrade your account", diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index 716738f12..426059446 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -8626,7 +8626,7 @@ export type TranslationFunctions = { */ settle: () => LocalizedString /** - * Settle pending transactions + * Cashout funds from your wallet */ settleDesc: () => LocalizedString } diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index de51ef8ff..b3a59d466 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -1071,7 +1071,7 @@ "topUp": "Top Up", "topUpDesc": "Add funds to your wallet", "settle": "Settle", - "settleDesc": "Settle pending transactions" + "settleDesc": "Cashout funds from your wallet" }, "UpgradeAccountModal": { "title": "Upgrade your account", From ba76a0cf3fdfe451bd03e388ec299b13168ebb83 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Fri, 25 Jul 2025 10:33:35 +0500 Subject: [PATCH 14/95] update CashoutConfirmation screen to show amount in display currency --- app/components/cashout-flow/CashoutCard.tsx | 2 +- .../cashout-screen/CashoutConfirmation.tsx | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/components/cashout-flow/CashoutCard.tsx b/app/components/cashout-flow/CashoutCard.tsx index e6054f974..a3d2ececd 100644 --- a/app/components/cashout-flow/CashoutCard.tsx +++ b/app/components/cashout-flow/CashoutCard.tsx @@ -4,7 +4,7 @@ import { makeStyles, Text } from "@rneui/themed" type Props = { title: string - detail: string | number + detail?: string | number } const CashoutCard: React.FC = ({ title, detail }) => { diff --git a/app/screens/cashout-screen/CashoutConfirmation.tsx b/app/screens/cashout-screen/CashoutConfirmation.tsx index 526f77f8c..5754dfcfe 100644 --- a/app/screens/cashout-screen/CashoutConfirmation.tsx +++ b/app/screens/cashout-screen/CashoutConfirmation.tsx @@ -11,6 +11,7 @@ import { CashoutCard, CashoutFromWallet } from "@app/components/cashout-flow" // hooks import { useI18nContext } from "@app/i18n/i18n-react" +import { useSafeAreaInsets } from "react-native-safe-area-context" import { useActivityIndicator, useDisplayCurrency } from "@app/hooks" import { useCashoutScreenQuery, useInitiateCashoutMutation } from "@app/graphql/generated" @@ -25,8 +26,9 @@ type Props = StackScreenProps const CashoutConfirmation: React.FC = ({ navigation, route }) => { const styles = useStyles() const { colors } = useTheme().theme + const { bottom } = useSafeAreaInsets() const { LL } = useI18nContext() - const { formatMoneyAmount } = useDisplayCurrency() + const { moneyAmountToDisplayCurrencyString, displayCurrency } = useDisplayCurrency() const { toggleActivityIndicator } = useActivityIndicator() const [errorMsg, setErrorMsg] = useState() @@ -64,15 +66,15 @@ const CashoutConfirmation: React.FC = ({ navigation, route }) => { toggleActivityIndicator(false) } - const formattedSendAmount = formatMoneyAmount({ + const formattedSendAmount = moneyAmountToDisplayCurrencyString({ moneyAmount: toUsdMoneyAmount(send ?? NaN), }) - const formattedReceiveUsdAmount = formatMoneyAmount({ + const formattedReceiveUsdAmount = moneyAmountToDisplayCurrencyString({ moneyAmount: toUsdMoneyAmount(receiveUsd ?? NaN), }) - const formattedFeeAmount = formatMoneyAmount({ + const formattedFeeAmount = moneyAmountToDisplayCurrencyString({ moneyAmount: toUsdMoneyAmount(flashFee ?? NaN), }) @@ -87,7 +89,9 @@ const CashoutConfirmation: React.FC = ({ navigation, route }) => { {!!errorMsg && ( @@ -98,7 +102,7 @@ const CashoutConfirmation: React.FC = ({ navigation, route }) => { @@ -107,13 +111,9 @@ const CashoutConfirmation: React.FC = ({ navigation, route }) => { export default CashoutConfirmation -const useStyles = makeStyles(({ colors }) => ({ +const useStyles = makeStyles(() => ({ valid: { alignSelf: "center", marginBottom: 10, }, - buttonContainer: { - marginHorizontal: 20, - marginBottom: 20, - }, })) From a01438e3966677b00367cc2a52e2958db8cac399 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Thu, 31 Jul 2025 10:08:17 +0500 Subject: [PATCH 15/95] add back exchangeRate to the requestCashout offer response and update the CashoutConfirmation screen accordingly --- app/graphql/front-end-mutations.ts | 1 + app/screens/cashout-screen/CashoutConfirmation.tsx | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/graphql/front-end-mutations.ts b/app/graphql/front-end-mutations.ts index 557f888b4..cf88eb87c 100644 --- a/app/graphql/front-end-mutations.ts +++ b/app/graphql/front-end-mutations.ts @@ -125,6 +125,7 @@ gql` path } offer { + exchangeRate expiresAt flashFee offerId diff --git a/app/screens/cashout-screen/CashoutConfirmation.tsx b/app/screens/cashout-screen/CashoutConfirmation.tsx index 5754dfcfe..3b3ecedbd 100644 --- a/app/screens/cashout-screen/CashoutConfirmation.tsx +++ b/app/screens/cashout-screen/CashoutConfirmation.tsx @@ -85,7 +85,10 @@ const CashoutConfirmation: React.FC = ({ navigation, route }) => { {LL.Cashout.valid({ time: moment(expiresAt).fromNow(true) })} - + Date: Thu, 31 Jul 2025 19:57:48 +0500 Subject: [PATCH 16/95] add back exchangeRate on the generated.gql and generated.ts files --- app/graphql/generated.gql | 1 + app/graphql/generated.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/graphql/generated.gql b/app/graphql/generated.gql index 5c04798b5..81185e570 100644 --- a/app/graphql/generated.gql +++ b/app/graphql/generated.gql @@ -128,6 +128,7 @@ mutation RequestCashout($input: RequestCashoutInput!) { __typename } offer { + exchangeRate expiresAt flashFee offerId diff --git a/app/graphql/generated.ts b/app/graphql/generated.ts index ea6f4770f..4a69fa3b6 100644 --- a/app/graphql/generated.ts +++ b/app/graphql/generated.ts @@ -291,6 +291,8 @@ export type CaptchaRequestAuthCodeInput = { export type CashoutOffer = { readonly __typename: 'CashoutOffer'; + /** The rate used when withdrawing to a JMD bank account */ + readonly exchangeRate: Scalars['JMDCents']['output']; /** The time at which this offer is no longer accepted by Flash */ readonly expiresAt: Scalars['Timestamp']['output']; /** The amount that Flash is charging for it's services */ @@ -309,8 +311,9 @@ export type CashoutOffer = { export type CentAmountPayload = { readonly __typename: 'CentAmountPayload'; - readonly amount?: Maybe; + readonly amount?: Maybe; readonly errors: ReadonlyArray; + readonly invoiceAmount?: Maybe; }; export type ConsumerAccount = Account & { @@ -2095,7 +2098,7 @@ export type RequestCashoutMutationVariables = Exact<{ }>; -export type RequestCashoutMutation = { readonly __typename: 'Mutation', readonly requestCashout: { readonly __typename: 'RequestCashoutResponse', readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string, readonly path?: ReadonlyArray | null }>, readonly offer?: { readonly __typename: 'CashoutOffer', readonly expiresAt: number, readonly flashFee: number, readonly offerId: string, readonly receiveJmd: number, readonly receiveUsd: number, readonly send: number, readonly walletId: string } | null } }; +export type RequestCashoutMutation = { readonly __typename: 'Mutation', readonly requestCashout: { readonly __typename: 'RequestCashoutResponse', readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string, readonly path?: ReadonlyArray | null }>, readonly offer?: { readonly __typename: 'CashoutOffer', readonly exchangeRate: number, readonly expiresAt: number, readonly flashFee: number, readonly offerId: string, readonly receiveJmd: number, readonly receiveUsd: number, readonly send: number, readonly walletId: string } | null } }; export type InitiateCashoutMutationVariables = Exact<{ input: InitiateCashoutInput; @@ -3519,6 +3522,7 @@ export const RequestCashoutDocument = gql` path } offer { + exchangeRate expiresAt flashFee offerId From 11a1ddbf1f65f75733a58545de974b168b5f6b4b Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Fri, 15 Aug 2025 18:19:56 +0500 Subject: [PATCH 17/95] BuyBitcoinSuccess screen is implemented and added in root navigation --- app/navigation/root-navigator.tsx | 6 ++ app/navigation/stack-param-lists.ts | 1 + .../buy-bitcoin-flow/BuyBitcoinSuccess.tsx | 78 +++++++++++++++++++ app/screens/buy-bitcoin-flow/index.ts | 10 ++- 4 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 app/screens/buy-bitcoin-flow/BuyBitcoinSuccess.tsx diff --git a/app/navigation/root-navigator.tsx b/app/navigation/root-navigator.tsx index 4f0de36e0..dc9a9d604 100644 --- a/app/navigation/root-navigator.tsx +++ b/app/navigation/root-navigator.tsx @@ -120,6 +120,7 @@ import { BankTransfer, BuyBitcoin, BuyBitcoinDetails, + BuyBitcoinSuccess, BuySellBitcoin, CardPayment, } from "@app/screens/buy-bitcoin-flow" @@ -593,6 +594,11 @@ export const RootStack = () => { + ) diff --git a/app/navigation/stack-param-lists.ts b/app/navigation/stack-param-lists.ts index 3e595639e..5928c6f8a 100644 --- a/app/navigation/stack-param-lists.ts +++ b/app/navigation/stack-param-lists.ts @@ -161,6 +161,7 @@ export type RootStackParamList = { amount: number wallet: string } + BuyBitcoinSuccess: undefined paymentSuccess: { amount: number wallet: string diff --git a/app/screens/buy-bitcoin-flow/BuyBitcoinSuccess.tsx b/app/screens/buy-bitcoin-flow/BuyBitcoinSuccess.tsx new file mode 100644 index 000000000..614daba8a --- /dev/null +++ b/app/screens/buy-bitcoin-flow/BuyBitcoinSuccess.tsx @@ -0,0 +1,78 @@ +import React from "react" +import { View } from "react-native" +import { makeStyles, Text, useTheme } from "@rneui/themed" +import { StackScreenProps } from "@react-navigation/stack" +import { RootStackParamList } from "@app/navigation/stack-param-lists" + +// components +import { + SuccessIconAnimation, + SuccessTextAnimation, +} from "@app/components/success-animation" +import { Screen } from "@app/components/screen" +import { PrimaryBtn } from "@app/components/buttons" +import { GaloyIcon } from "@app/components/atomic/galoy-icon" + +// hooks +import { useI18nContext } from "@app/i18n/i18n-react" +import { useSafeAreaInsets } from "react-native-safe-area-context" + +type Props = StackScreenProps + +const BuyBitcoinSuccess: React.FC = () => { + const styles = useStyles() + const { colors } = useTheme().theme + const { LL } = useI18nContext() + const { bottom } = useSafeAreaInsets() + + const onPressDone = () => {} + + return ( + + + + + + + + USD Wallet Topped Up + + + $200 + + + J$35.11 + + + + + + ) +} + +export default BuyBitcoinSuccess + +const useStyles = makeStyles(() => ({ + container: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + successText: { + textAlign: "center", + color: "#fff", + marginBottom: 8, + }, +})) diff --git a/app/screens/buy-bitcoin-flow/index.ts b/app/screens/buy-bitcoin-flow/index.ts index 5d73c7892..8c16ca65b 100644 --- a/app/screens/buy-bitcoin-flow/index.ts +++ b/app/screens/buy-bitcoin-flow/index.ts @@ -3,5 +3,13 @@ import BuyBitcoin from "./BuyBitcoin" import BuyBitcoinDetails from "./BuyBitcoinDetails" import BankTransfer from "./BankTransfer" import CardPayment from "./CardPayment" +import BuyBitcoinSuccess from "./BuyBitcoinSuccess" -export { BuySellBitcoin, BuyBitcoin, BuyBitcoinDetails, BankTransfer, CardPayment } +export { + BuySellBitcoin, + BuyBitcoin, + BuyBitcoinDetails, + BankTransfer, + CardPayment, + BuyBitcoinSuccess, +} From d47d60701271c9c25e302ed0f3c1a19288c086ce Mon Sep 17 00:00:00 2001 From: Dread <34528298+islandbitcoin@users.noreply.github.com> Date: Tue, 23 Sep 2025 21:26:21 -0400 Subject: [PATCH 18/95] enabled card payments via Fygaro - webhook wip --- app/components/home-screen/Buttons.tsx | 2 +- app/i18n/en/index.ts | 4 +- app/i18n/i18n-types.ts | 4 +- app/i18n/raw-i18n/source/en.json | 4 +- app/navigation/stack-param-lists.ts | 2 - app/screens/buy-bitcoin-flow/BuyBitcoin.tsx | 18 +- .../buy-bitcoin-flow/BuyBitcoinDetails.tsx | 57 +---- app/screens/buy-bitcoin-flow/CardPayment.tsx | 225 +++++++++++------- 8 files changed, 168 insertions(+), 148 deletions(-) diff --git a/app/components/home-screen/Buttons.tsx b/app/components/home-screen/Buttons.tsx index 324232a11..3fc8a696f 100644 --- a/app/components/home-screen/Buttons.tsx +++ b/app/components/home-screen/Buttons.tsx @@ -68,7 +68,7 @@ const Buttons: React.FC = ({ setModalVisible, setDefaultAccountModalVisib }) } - if (currentLevel === AccountLevel.Two) { + if (currentLevel === AccountLevel.Two || currentLevel === AccountLevel.Three) { buttons.push({ title: LL.HomeScreen.transfer(), target: "BuySellBitcoin", diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index 6241f9a3b..960982432 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -569,9 +569,9 @@ const en: BaseTranslation = { TopUpScreen: { title: "Top Up", bankTransfer: "Bank Transfer", - bankTransferDesc: "Transfer funds from your bank account", + bankTransferDesc: "Transfer funds from your bank", debitCreditCard: "Debit/Credit Card", - debitCreditCardDesc: "Pay with your card via Fygaro" + debitCreditCardDesc: "Pay with your debit or credit card" }, BuyBitcoinDetails: { title: "Card Payment", diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index a006a195d..a99a615cd 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -6742,7 +6742,7 @@ export type TranslationFunctions = { */ bankTransfer: () => LocalizedString /** - * Transfer funds from your bank account + * Transfer funds from your bank */ bankTransferDesc: () => LocalizedString /** @@ -6750,7 +6750,7 @@ export type TranslationFunctions = { */ debitCreditCard: () => LocalizedString /** - * Pay with your card via Fygaro + * Pay with your debit or credit card */ debitCreditCardDesc: () => LocalizedString } diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index 69cbbc522..e9de6c403 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -538,9 +538,9 @@ "TopUpScreen": { "title": "Top Up", "bankTransfer": "Bank Transfer", - "bankTransferDesc": "Transfer funds from your bank account", + "bankTransferDesc": "Transfer funds from your bank", "debitCreditCard": "Debit/Credit Card", - "debitCreditCardDesc": "Pay with your card via Fygaro" + "debitCreditCardDesc": "Pay with your debit or credit card" }, "BuyBitcoinDetails": { "title": "Card Payment", diff --git a/app/navigation/stack-param-lists.ts b/app/navigation/stack-param-lists.ts index 27cc92bdd..0987f5fe1 100644 --- a/app/navigation/stack-param-lists.ts +++ b/app/navigation/stack-param-lists.ts @@ -155,12 +155,10 @@ export type RootStackParamList = { BuyBitcoin: undefined BuyBitcoinDetails: { paymentType: "card" | "bankTransfer" } BankTransfer: { - email: string amount: number wallet: string } CardPayment: { - email: string amount: number wallet: string } diff --git a/app/screens/buy-bitcoin-flow/BuyBitcoin.tsx b/app/screens/buy-bitcoin-flow/BuyBitcoin.tsx index bc19c84ca..fc0f7b2f5 100644 --- a/app/screens/buy-bitcoin-flow/BuyBitcoin.tsx +++ b/app/screens/buy-bitcoin-flow/BuyBitcoin.tsx @@ -24,15 +24,14 @@ const BuyBitcoin: React.FC = ({ navigation }) => { - navigation.navigate("BuyBitcoinDetails", { paymentType: "bankTransfer" }) + navigation.navigate("BuyBitcoinDetails", { paymentType: "card" }) } > - - + - {LL.TopUpScreen.bankTransfer()} + {LL.TopUpScreen.debitCreditCard()} - {LL.TopUpScreen.bankTransferDesc()} + {LL.TopUpScreen.debitCreditCardDesc()} @@ -40,14 +39,15 @@ const BuyBitcoin: React.FC = ({ navigation }) => { - navigation.navigate("BuyBitcoinDetails", { paymentType: "card" }) + navigation.navigate("BuyBitcoinDetails", { paymentType: "bankTransfer" }) } > - + + - {LL.TopUpScreen.debitCreditCard()} + {LL.TopUpScreen.bankTransfer()} - {LL.TopUpScreen.debitCreditCardDesc()} + {LL.TopUpScreen.bankTransferDesc()} diff --git a/app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx b/app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx index 7ec9db11a..383e01784 100644 --- a/app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx +++ b/app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react" +import React, { useState } from "react" import { View, TextInput, Alert } from "react-native" import { Text, makeStyles, useTheme } from "@rneui/themed" import { StackScreenProps } from "@react-navigation/stack" @@ -11,8 +11,6 @@ import { ButtonGroup } from "@app/components/button-group" // hooks import { useI18nContext } from "@app/i18n/i18n-react" -import { useHomeAuthedQuery } from "@app/graphql/generated" -import { useIsAuthed } from "@app/graphql/is-authed-context" import { useSafeAreaInsets } from "react-native-safe-area-context" // assets @@ -22,44 +20,21 @@ import Bitcoin from "@app/assets/icons/bitcoin.svg" type Props = StackScreenProps const BuyBitcoinDetails: React.FC = ({ navigation, route }) => { - const { bottom } = useSafeAreaInsets() const { colors } = useTheme().theme const { LL } = useI18nContext() - const isAuthed = useIsAuthed() - const styles = useStyles() + const { bottom } = useSafeAreaInsets() + const styles = useStyles()({ bottom }) - const [email, setEmail] = useState("") const [selectedWallet, setSelectedWallet] = useState("USD") const [amount, setAmount] = useState("") const [isLoading, setIsLoading] = useState(false) - const { data } = useHomeAuthedQuery({ - skip: !isAuthed, - fetchPolicy: "cache-first", - }) - - useEffect(() => { - if (data?.me?.email?.address) { - setEmail(data.me.email.address) - } - }, [data]) - - const validateEmail = (email: string): boolean => { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ - return emailRegex.test(email) - } - const validateAmount = (amount: string): boolean => { const numAmount = parseFloat(amount) return !isNaN(numAmount) && numAmount >= 1.0 } const handleContinue = async () => { - if (!validateEmail(email)) { - Alert.alert("Invalid Email", LL.BuyBitcoinDetails.invalidEmail()) - return - } - if (!validateAmount(amount)) { Alert.alert("Invalid Amount", LL.BuyBitcoinDetails.minimumAmount()) return @@ -70,13 +45,11 @@ const BuyBitcoinDetails: React.FC = ({ navigation, route }) => { try { if (route.params.paymentType === "bankTransfer") { navigation.navigate("BankTransfer", { - email, amount: parseFloat(amount), wallet: selectedWallet, }) } else { navigation.navigate("CardPayment", { - email, amount: parseFloat(amount), wallet: selectedWallet, }) @@ -115,20 +88,6 @@ const BuyBitcoinDetails: React.FC = ({ navigation, route }) => { ? LL.BuyBitcoinDetails.title() : LL.BuyBitcoinDetails.bankTransfer()} - - - {LL.BuyBitcoinDetails.email()} - - - @@ -160,19 +119,19 @@ const BuyBitcoinDetails: React.FC = ({ navigation, route }) => { label={LL.BuyBitcoinDetails.continue()} onPress={handleContinue} loading={isLoading} - btnStyle={{ marginHorizontal: 20, marginBottom: bottom + 20 }} + btnStyle={styles.primaryButton} /> ) } -const useStyles = makeStyles(({ colors }) => ({ +const useStyles = makeStyles(({ colors }) => (props: { bottom: number }) => ({ container: { flex: 1, paddingHorizontal: 20, }, title: { - textAlign: "center", + textAlign: "center" as const, marginBottom: 30, }, fieldContainer: { @@ -191,6 +150,10 @@ const useStyles = makeStyles(({ colors }) => ({ buttonGroup: { marginTop: 8, }, + primaryButton: { + marginHorizontal: 20, + marginBottom: Math.max(20, props.bottom), + }, })) export default BuyBitcoinDetails diff --git a/app/screens/buy-bitcoin-flow/CardPayment.tsx b/app/screens/buy-bitcoin-flow/CardPayment.tsx index ddad5856c..da8865e31 100644 --- a/app/screens/buy-bitcoin-flow/CardPayment.tsx +++ b/app/screens/buy-bitcoin-flow/CardPayment.tsx @@ -1,15 +1,11 @@ import React, { useState, useRef } from "react" -import { View, ActivityIndicator, Alert } from "react-native" +import { View, ActivityIndicator, Alert, Platform } from "react-native" import { StackScreenProps } from "@react-navigation/stack" import { Text, makeStyles, useTheme } from "@rneui/themed" import { RootStackParamList } from "@app/navigation/stack-param-lists" import { WebView } from "react-native-webview" - -// components import { Screen } from "@app/components/screen" import { PrimaryBtn } from "@app/components/buttons" - -// hooks import { useI18nContext } from "@app/i18n/i18n-react" import { useHomeAuthedQuery } from "@app/graphql/generated" import { useIsAuthed } from "@app/graphql/is-authed-context" @@ -21,82 +17,73 @@ const CardPayment: React.FC = ({ navigation, route }) => { const styles = useStyles() const { colors } = useTheme().theme const { LL } = useI18nContext() - const webViewRef = useRef(null) - const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(false) - const { data } = useHomeAuthedQuery({ - skip: !isAuthed, - fetchPolicy: "cache-first", - }) - - const { email, amount, wallet } = route.params - + const { data } = useHomeAuthedQuery({ skip: !isAuthed, fetchPolicy: "cache-first" }) + const { amount, wallet } = route.params const username = data?.me?.username || "user" - const paymentUrl = `https://fygaro.com/en/pb/bd4a34c1-3d24-4315-a2b8-627518f70916?amount=${amount}&client_reference=${username}` - const handleNavigationStateChange = (navState: { url: string }) => { - const { url } = navState + // Build payment URL - user will enter email directly on Fygaro's form + const paymentUrl = `https://fygaro.com/en/pb/bd4a34c1-3d24-4315-a2b8-627518f70916?amount=${amount}&client_reference=${username}` - // Check for success URL pattern + const allowedDomains = [ + "fygaro.com", + "www.fygaro.com", + "api.fygaro.com", + "checkout.fygaro.com", + "www.paypal.com", + "checkout.paypal.com", + "paypalobjects.com", + "paypal-activation.com", + "paypal.com", + "paypal-cdn.com", + "paypal-experience.com", + "paypal-dynamic-assets.com", + "paypalcorp.com", + "stripe.com", + "checkout.stripe.com", + "js.stripe.com", + "m.stripe.com", + ] + + const handleNavigationStateChange = ({ url }: { url: string }) => { if (url.includes("success") || url.includes("payment_success")) { - // Mock successful payment response - const mockTransactionId = `txn_${Date.now()}` - navigation.navigate("paymentSuccess", { amount, wallet, - transactionId: mockTransactionId, + transactionId: `txn_${Date.now()}`, }) - } - - // Check for error/failure URLs - if (url.includes("error") || url.includes("failed") || url.includes("cancelled")) { + } else if ( + url.includes("error") || + url.includes("failed") || + url.includes("cancelled") + ) { Alert.alert("Payment Failed", "Your payment was not completed. Please try again.", [ { text: "OK", onPress: () => navigation.goBack() }, ]) } } - const handleLoadEnd = () => { - setIsLoading(false) - setError(false) - } - - const handleError = () => { - setIsLoading(false) - setError(true) - } - - const handleRetry = () => { - setError(false) - setIsLoading(true) - if (webViewRef.current) { - webViewRef.current.reload() - } - } - const isAllowedDomain = (url: string): boolean => { - // Security check: only allow Fygaro domains - const allowedDomains = [ - "fygaro.com", - "www.fygaro.com", - "api.fygaro.com", - "checkout.fygaro.com", - ] - try { const domain = new URL(url).hostname - return allowedDomains.includes(domain) + return allowedDomains.some( + (allowed) => + domain === allowed || + domain.endsWith(`.${allowed}`) || + allowed.endsWith(`.${domain}`), + ) } catch { return false } } - const handleShouldStartLoadWithRequest = (request: { url: string }) => { - return isAllowedDomain(request.url) + const handleRetry = () => { + setError(false) + setIsLoading(true) + webViewRef.current?.reload() } if (error) { @@ -127,23 +114,107 @@ const CardPayment: React.FC = ({ navigation, route }) => { )} - { + setIsLoading(false) + setError(false) + }} + onError={() => { + setIsLoading(false) + setError(true) + }} + onShouldStartLoadWithRequest={({ url }) => { + // Allow about: URLs (used for iframes) to prevent iOS warnings + if (url.startsWith("about:")) { + return true + } + + // Allow blob: and data: URLs for embedded content + if (url.startsWith("blob:") || url.startsWith("data:")) { + return true + } + + // iOS: Allow HTTPS and internal URLs + if (Platform.OS === "ios") { + return url.startsWith("https://") || !url.startsWith("http") + } + + // Android: Use domain whitelist + return isAllowedDomain(url) + }} style={styles.webView} - javaScriptEnabled={true} - domStorageEnabled={true} - startInLoadingState={true} - scalesPageToFit={true} + javaScriptEnabled + domStorageEnabled + startInLoadingState + scalesPageToFit={false} bounces={false} - scrollEnabled={true} - allowsBackForwardNavigationGestures={true} - userAgent="Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1" + scrollEnabled + allowsBackForwardNavigationGestures + allowsInlineMediaPlayback + allowsFullscreenVideo={false} + allowFileAccess={false} + allowUniversalAccessFromFileURLs={false} + mixedContentMode="never" + originWhitelist={["https://*", "http://*", "about:*", "data:*", "blob:*"]} + sharedCookiesEnabled + thirdPartyCookiesEnabled + cacheEnabled + incognito={false} + webviewDebuggingEnabled={__DEV__} + automaticallyAdjustContentInsets={false} + contentInsetAdjustmentBehavior="never" + allowsLinkPreview={false} + injectedJavaScriptForMainFrameOnly + userAgent={ + Platform.OS === "ios" + ? "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + : undefined + } + dataDetectorTypes="none" + injectedJavaScriptBeforeContentLoaded={`(function(){const forceViewport=()=>{const content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no';document.querySelectorAll('meta[name="viewport"]').forEach(m=>m.remove());const meta=document.createElement('meta');meta.name='viewport';meta.content=content;if(document.head){document.head.insertBefore(meta,document.head.firstChild)}else{document.documentElement.appendChild(meta)}};forceViewport();document.addEventListener('DOMContentLoaded',forceViewport);window.addEventListener('load',forceViewport);if(document.documentElement){document.documentElement.style.touchAction='pan-x pan-y'}true})();`} + injectedJavaScript={`(function(){ + // Zoom prevention code only + document.querySelectorAll('meta[name="viewport"]').forEach(m=>m.remove()); + const meta=document.createElement('meta'); + meta.name='viewport'; + meta.content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'; + document.head.insertBefore(meta,document.head.firstChild); + + const style=document.createElement('style'); + style.innerHTML='*{-webkit-text-size-adjust:100%!important;text-size-adjust:100%!important;-webkit-user-select:none!important;user-select:none!important;touch-action:manipulation!important}html,body{touch-action:pan-x pan-y!important;overflow-x:hidden!important}input,textarea,select,button{font-size:16px!important;transform:none!important;zoom:reset!important;-webkit-appearance:none!important}input:focus,textarea:focus,select:focus{font-size:16px!important;zoom:1!important}'; + document.head.appendChild(style); + + const preventZoom=e=>{if(e.target&&(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA')){e.target.style.fontSize='16px';e.target.style.transform='scale(1)';e.target.style.zoom='1';const oldMeta=document.querySelector('meta[name="viewport"]');if(oldMeta){oldMeta.content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'}}}; + document.addEventListener('focusin',preventZoom,true); + document.addEventListener('focus',preventZoom,true); + + // Watch for dynamically added inputs and apply zoom prevention + const observer=new MutationObserver(mutations=>{ + mutations.forEach(mutation=>{ + mutation.addedNodes.forEach(node=>{ + if(node.nodeType===1){ + const inputs=node.querySelectorAll?node.querySelectorAll('input, textarea, select'):[]; + inputs.forEach(input=>{ + input.style.fontSize='16px'; + input.addEventListener('focus',preventZoom,true); + }); + } + }) + }) + }); + + if(document.body){observer.observe(document.body,{childList:true,subtree:true})} + + document.querySelectorAll('input, textarea, select').forEach(el=>{el.style.fontSize='16px';el.addEventListener('focus',preventZoom,true)}); + + let lastTouchEnd=0; + document.addEventListener('touchend',e=>{const now=Date.now();if(now-lastTouchEnd<=300){e.preventDefault()}lastTouchEnd=now},{passive:false}); + + true + })();`} /> @@ -151,9 +222,7 @@ const CardPayment: React.FC = ({ navigation, route }) => { } const useStyles = makeStyles(() => ({ - container: { - flex: 1, - }, + container: { flex: 1 }, centerContainer: { flex: 1, justifyContent: "center", @@ -171,20 +240,10 @@ const useStyles = makeStyles(() => ({ backgroundColor: "rgba(255, 255, 255, 0.9)", zIndex: 1, }, - loadingText: { - marginTop: 16, - textAlign: "center", - }, - errorText: { - textAlign: "center", - marginBottom: 24, - }, - retryButton: { - marginTop: 16, - }, - webView: { - flex: 1, - }, + loadingText: { marginTop: 16, textAlign: "center" }, + errorText: { textAlign: "center", marginBottom: 24 }, + retryButton: { marginTop: 16 }, + webView: { flex: 1 }, })) export default CardPayment From 47046d04cd2949a0be9924914d38e95b8fb11099 Mon Sep 17 00:00:00 2001 From: Dread <34528298+islandbitcoin@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:09:24 -0400 Subject: [PATCH 19/95] add developer comments --- .../buy-bitcoin-flow/BuyBitcoinDetails.tsx | 53 ++++++- app/screens/buy-bitcoin-flow/CardPayment.tsx | 137 +++++++++++++++++- 2 files changed, 183 insertions(+), 7 deletions(-) diff --git a/app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx b/app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx index 383e01784..be83593e7 100644 --- a/app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx +++ b/app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx @@ -1,3 +1,18 @@ +/** + * BuyBitcoinDetails Component + * + * This screen collects payment details before initiating the topup flow. + * Users select: + * 1. Target wallet (USD or BTC) + * 2. Amount to topup + * + * Previously, this screen also collected email address, but that was removed + * to avoid double entry - users now enter email directly on Fygaro's form. + * + * The component supports both card payments (Fygaro) and bank transfers, + * routing to the appropriate flow based on the selected payment type. + */ + import React, { useState } from "react" import { View, TextInput, Alert } from "react-native" import { Text, makeStyles, useTheme } from "@rneui/themed" @@ -25,15 +40,40 @@ const BuyBitcoinDetails: React.FC = ({ navigation, route }) => { const { bottom } = useSafeAreaInsets() const styles = useStyles()({ bottom }) + /** + * Component state: + * - selectedWallet: Which wallet to credit (USD or BTC) + * - amount: Topup amount in USD + * - isLoading: Loading state for navigation + * + * NOTE: Email field was removed to prevent double entry. + * Users enter email on Fygaro's payment form instead. + */ const [selectedWallet, setSelectedWallet] = useState("USD") const [amount, setAmount] = useState("") const [isLoading, setIsLoading] = useState(false) + /** + * Validates the entered amount. + * Minimum topup amount is $1.00 to prevent micro-transactions + * that would be unprofitable due to processing fees. + */ const validateAmount = (amount: string): boolean => { const numAmount = parseFloat(amount) return !isNaN(numAmount) && numAmount >= 1.0 } + /** + * Handles the continue button press. + * + * Validates amount and navigates to the appropriate payment flow: + * - Card payment: Goes to CardPayment (WebView with Fygaro) + * - Bank transfer: Goes to BankTransfer screen + * + * The wallet type and amount are passed to the next screen. + * The wallet type will be included in the webhook metadata + * to ensure the correct wallet is credited. + */ const handleContinue = async () => { if (!validateAmount(amount)) { Alert.alert("Invalid Amount", LL.BuyBitcoinDetails.minimumAmount()) @@ -49,9 +89,10 @@ const BuyBitcoinDetails: React.FC = ({ navigation, route }) => { wallet: selectedWallet, }) } else { + // Card payment flow via Fygaro WebView navigation.navigate("CardPayment", { amount: parseFloat(amount), - wallet: selectedWallet, + wallet: selectedWallet, // Will be sent to webhook via metadata }) } } catch (error) { @@ -61,6 +102,16 @@ const BuyBitcoinDetails: React.FC = ({ navigation, route }) => { } } + /** + * Wallet selection buttons configuration. + * + * Users can choose to credit either: + * - USD wallet: Fiat balance for USD transactions + * - BTC wallet: Bitcoin balance (amount converted at current rate) + * + * The selected wallet type is passed through the payment flow + * and included in the webhook metadata to ensure correct crediting. + */ const walletButtons = [ { id: "USD", diff --git a/app/screens/buy-bitcoin-flow/CardPayment.tsx b/app/screens/buy-bitcoin-flow/CardPayment.tsx index da8865e31..1104a936b 100644 --- a/app/screens/buy-bitcoin-flow/CardPayment.tsx +++ b/app/screens/buy-bitcoin-flow/CardPayment.tsx @@ -1,3 +1,18 @@ +/** + * CardPayment Component + * + * This component handles the web-based card payment flow for topping up Flash wallets. + * It embeds external payment provider forms (Fygaro, PayPal, Stripe) in a WebView + * and manages the entire payment lifecycle. + * + * Key features: + * - Embeds Fygaro payment form (and other providers) in WebView + * - Prevents iOS zoom issues when users tap input fields + * - Domain whitelisting for security + * - Handles payment success/failure callbacks + * - Desktop user agent spoofing on iOS to prevent mobile-specific issues + */ + import React, { useState, useRef } from "react" import { View, ActivityIndicator, Alert, Platform } from "react-native" import { StackScreenProps } from "@react-navigation/stack" @@ -21,18 +36,41 @@ const CardPayment: React.FC = ({ navigation, route }) => { const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(false) + // Get authenticated user data to extract username for webhook processing const { data } = useHomeAuthedQuery({ skip: !isAuthed, fetchPolicy: "cache-first" }) const { amount, wallet } = route.params const username = data?.me?.username || "user" - // Build payment URL - user will enter email directly on Fygaro's form + /** + * Build Fygaro payment URL with critical parameters: + * - amount: The payment amount in USD + * - client_reference: Flash username (CRITICAL: used by webhook to identify user) + * + * NOTE: The user will enter their email directly on Fygaro's form to avoid + * double entry. The email is only for Fygaro's records, not for Flash processing. + */ const paymentUrl = `https://fygaro.com/en/pb/bd4a34c1-3d24-4315-a2b8-627518f70916?amount=${amount}&client_reference=${username}` + /** + * Domain whitelist for security. + * + * Only allows navigation to trusted payment provider domains. + * This prevents: + * - Phishing attacks via redirect + * - Malicious JavaScript injection + * - Data exfiltration to unauthorized domains + * + * Includes all necessary domains for: + * - Fygaro (primary provider) + * - PayPal (Fygaro uses PayPal for processing) + * - Stripe (future provider support) + */ const allowedDomains = [ "fygaro.com", "www.fygaro.com", "api.fygaro.com", "checkout.fygaro.com", + // PayPal domains (required for Fygaro's PayPal integration) "www.paypal.com", "checkout.paypal.com", "paypalobjects.com", @@ -42,40 +80,69 @@ const CardPayment: React.FC = ({ navigation, route }) => { "paypal-experience.com", "paypal-dynamic-assets.com", "paypalcorp.com", + // Stripe domains (for future integration) "stripe.com", "checkout.stripe.com", "js.stripe.com", "m.stripe.com", ] + /** + * Monitors URL changes to detect payment completion. + * + * Payment providers redirect to specific URLs on success/failure: + * - Success: URL contains "success" or "payment_success" + * - Failure: URL contains "error", "failed", or "cancelled" + * + * On success: Navigate to success screen (webhook will handle actual crediting) + * On failure: Show error alert and allow retry + * + * NOTE: The actual account crediting happens via webhook on the backend. + * This frontend handling is just for UX feedback. + */ const handleNavigationStateChange = ({ url }: { url: string }) => { if (url.includes("success") || url.includes("payment_success")) { + // Payment succeeded - navigate to success screen + // The webhook will handle the actual wallet credit navigation.navigate("paymentSuccess", { amount, wallet, - transactionId: `txn_${Date.now()}`, + transactionId: `txn_${Date.now()}`, // Temporary ID for UI }) } else if ( url.includes("error") || url.includes("failed") || url.includes("cancelled") ) { + // Payment failed - show error and allow retry Alert.alert("Payment Failed", "Your payment was not completed. Please try again.", [ { text: "OK", onPress: () => navigation.goBack() }, ]) } } + /** + * Validates if a URL is from an allowed domain. + * + * Security function that checks URLs against our whitelist. + * Handles: + * - Exact domain matches (paypal.com) + * - Subdomain matches (checkout.paypal.com) + * - Invalid URLs (returns false) + * + * This prevents navigation to untrusted domains. + */ const isAllowedDomain = (url: string): boolean => { try { const domain = new URL(url).hostname return allowedDomains.some( (allowed) => - domain === allowed || - domain.endsWith(`.${allowed}`) || - allowed.endsWith(`.${domain}`), + domain === allowed || // Exact match + domain.endsWith(`.${allowed}`) || // Subdomain match + allowed.endsWith(`.${domain}`), // Parent domain match ) } catch { + // Invalid URL - deny by default return false } } @@ -126,6 +193,24 @@ const CardPayment: React.FC = ({ navigation, route }) => { setIsLoading(false) setError(true) }} + /** + * URL navigation filter for security and iOS compatibility. + * + * This callback determines which URLs the WebView can navigate to. + * Platform-specific logic: + * + * iOS: + * - Allows about:, blob:, data: schemes (prevents warnings) + * - Allows all HTTPS URLs (PayPal requires many domains) + * - Blocks HTTP to enforce encryption + * + * Android: + * - Uses strict domain whitelist + * - More restrictive but more secure + * + * The iOS approach is less restrictive due to PayPal's complex + * redirect flow that uses many subdomains not in our whitelist. + */ onShouldStartLoadWithRequest={({ url }) => { // Allow about: URLs (used for iframes) to prevent iOS warnings if (url.startsWith("about:")) { @@ -138,11 +223,12 @@ const CardPayment: React.FC = ({ navigation, route }) => { } // iOS: Allow HTTPS and internal URLs + // Less restrictive due to PayPal's complex domain requirements if (Platform.OS === "ios") { return url.startsWith("https://") || !url.startsWith("http") } - // Android: Use domain whitelist + // Android: Use domain whitelist for stricter security return isAllowedDomain(url) }} style={styles.webView} @@ -168,13 +254,52 @@ const CardPayment: React.FC = ({ navigation, route }) => { contentInsetAdjustmentBehavior="never" allowsLinkPreview={false} injectedJavaScriptForMainFrameOnly + /** + * Custom User Agent for iOS to prevent zoom issues. + * + * CRITICAL: This desktop user agent prevents iOS WebView from: + * - Auto-zooming when users tap input fields + * - Showing mobile-specific layouts that break + * - Triggering viewport zoom on focus + * + * Without this, iOS WebView zooms in when users tap the + * payment form fields, creating a poor user experience. + * + * Android doesn't need this workaround. + */ userAgent={ Platform.OS === "ios" ? "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" : undefined } dataDetectorTypes="none" + /** + * Pre-content JavaScript injection for early zoom prevention. + * + * Executes BEFORE the page content loads to: + * - Force viewport meta tag with no zoom + * - Set touch-action CSS to prevent pinch zoom + * - Run on multiple events to catch dynamic content + * + * This is the first line of defense against iOS zoom issues. + */ injectedJavaScriptBeforeContentLoaded={`(function(){const forceViewport=()=>{const content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no';document.querySelectorAll('meta[name="viewport"]').forEach(m=>m.remove());const meta=document.createElement('meta');meta.name='viewport';meta.content=content;if(document.head){document.head.insertBefore(meta,document.head.firstChild)}else{document.documentElement.appendChild(meta)}};forceViewport();document.addEventListener('DOMContentLoaded',forceViewport);window.addEventListener('load',forceViewport);if(document.documentElement){document.documentElement.style.touchAction='pan-x pan-y'}true})();`} + /** + * Post-content JavaScript injection for comprehensive zoom prevention. + * + * This aggressive approach handles: + * 1. Viewport meta tag enforcement + * 2. CSS styles to prevent zoom (16px font size is key) + * 3. Focus event handlers to reset zoom on input focus + * 4. MutationObserver to handle dynamically added inputs + * 5. Double-tap prevention + * + * The 16px font size is critical - iOS auto-zooms on inputs + * with font size less than 16px. + * + * Combined with desktop user agent, this completely prevents + * the iOS zoom issue that occurs when users tap payment form fields. + */ injectedJavaScript={`(function(){ // Zoom prevention code only document.querySelectorAll('meta[name="viewport"]').forEach(m=>m.remove()); From d7a0bb6c0f52c223be4c2684b5c5da222fb683e3 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Wed, 22 Apr 2026 12:36:11 +0500 Subject: [PATCH 20/95] add cashout api integration changes in this branch --- app/graphql/front-end-mutations.ts | 6 +- app/graphql/generated.gql | 57 +++++-------------- app/graphql/generated.ts | 10 ++-- .../cashout-screen/CashoutConfirmation.tsx | 9 +-- package.json | 1 + yarn.lock | 9 ++- 6 files changed, 31 insertions(+), 61 deletions(-) diff --git a/app/graphql/front-end-mutations.ts b/app/graphql/front-end-mutations.ts index 5079ad046..ca7054a85 100644 --- a/app/graphql/front-end-mutations.ts +++ b/app/graphql/front-end-mutations.ts @@ -122,7 +122,6 @@ gql` errors { code message - path } offer { exchangeRate @@ -140,11 +139,10 @@ gql` mutation InitiateCashout($input: InitiateCashoutInput!) { initiateCashout(input: $input) { errors { - path - message code + message } - success + journalId } } diff --git a/app/graphql/generated.gql b/app/graphql/generated.gql index 16ad39f3c..18fce6782 100644 --- a/app/graphql/generated.gql +++ b/app/graphql/generated.gql @@ -97,12 +97,11 @@ mutation IdDocumentUploadUrlGenerate($input: IdDocumentUploadUrlGenerateInput!) mutation InitiateCashout($input: InitiateCashoutInput!) { initiateCashout(input: $input) { errors { - path - message code + message __typename } - success + journalId __typename } } @@ -137,7 +136,6 @@ mutation RequestCashout($input: RequestCashoutInput!) { errors { code message - path __typename } offer { @@ -166,9 +164,7 @@ mutation accountDelete { } } -mutation accountDisableNotificationCategory( - $input: AccountDisableNotificationCategoryInput! -) { +mutation accountDisableNotificationCategory($input: AccountDisableNotificationCategoryInput!) { accountDisableNotificationCategory(input: $input) { errors { message @@ -190,9 +186,7 @@ mutation accountDisableNotificationCategory( } } -mutation accountDisableNotificationChannel( - $input: AccountDisableNotificationChannelInput! -) { +mutation accountDisableNotificationChannel($input: AccountDisableNotificationChannelInput!) { accountDisableNotificationChannel(input: $input) { errors { message @@ -214,9 +208,7 @@ mutation accountDisableNotificationChannel( } } -mutation accountEnableNotificationCategory( - $input: AccountEnableNotificationCategoryInput! -) { +mutation accountEnableNotificationCategory($input: AccountEnableNotificationCategoryInput!) { accountEnableNotificationCategory(input: $input) { errors { message @@ -238,9 +230,7 @@ mutation accountEnableNotificationCategory( } } -mutation accountEnableNotificationChannel( - $input: AccountEnableNotificationChannelInput! -) { +mutation accountEnableNotificationChannel($input: AccountEnableNotificationChannelInput!) { accountEnableNotificationChannel(input: $input) { errors { message @@ -561,9 +551,7 @@ mutation onChainUsdPaymentSend($input: OnChainUsdPaymentSendInput!) { } } -mutation onChainUsdPaymentSendAsBtcDenominated( - $input: OnChainUsdPaymentSendAsBtcDenominatedInput! -) { +mutation onChainUsdPaymentSendAsBtcDenominated($input: OnChainUsdPaymentSendAsBtcDenominatedInput!) { onChainUsdPaymentSendAsBtcDenominated(input: $input) { errors { message @@ -1300,22 +1288,14 @@ query onChainTxFee($walletId: WalletId!, $address: OnChainAddress!, $amount: Sat } } -query onChainUsdTxFee( - $walletId: WalletId! - $address: OnChainAddress! - $amount: FractionalCentAmount! -) { +query onChainUsdTxFee($walletId: WalletId!, $address: OnChainAddress!, $amount: FractionalCentAmount!) { onChainUsdTxFee(walletId: $walletId, address: $address, amount: $amount) { amount __typename } } -query onChainUsdTxFeeAsBtcDenominated( - $walletId: WalletId! - $address: OnChainAddress! - $amount: SatAmount! -) { +query onChainUsdTxFeeAsBtcDenominated($walletId: WalletId!, $address: OnChainAddress!, $amount: SatAmount!) { onChainUsdTxFeeAsBtcDenominated( walletId: $walletId address: $address @@ -1648,13 +1628,7 @@ query transactionDetails($input: TransactionDetailsInput!) { } } -query transactionListForContact( - $username: Username! - $first: Int - $after: String - $last: Int - $before: String -) { +query transactionListForContact($username: Username!, $first: Int, $after: String, $last: Int, $before: String) { me { id contactByUsername(username: $username) { @@ -1668,12 +1642,7 @@ query transactionListForContact( } } -query transactionListForDefaultAccount( - $first: Int - $after: String - $last: Int - $before: String -) { +query transactionListForDefaultAccount($first: Int, $after: String, $last: Int, $before: String) { me { id defaultAccount { @@ -1771,7 +1740,7 @@ subscription myLnUpdates { } subscription realtimePriceWs($currency: DisplayCurrency!) { - realtimePrice(input: { currency: $currency }) { + realtimePrice(input: {currency: $currency}) { errors { message __typename @@ -1793,4 +1762,4 @@ subscription realtimePriceWs($currency: DisplayCurrency!) { } __typename } -} +} \ No newline at end of file diff --git a/app/graphql/generated.ts b/app/graphql/generated.ts index 7ad188767..f7475c95e 100644 --- a/app/graphql/generated.ts +++ b/app/graphql/generated.ts @@ -2277,14 +2277,14 @@ export type RequestCashoutMutationVariables = Exact<{ }>; -export type RequestCashoutMutation = { readonly __typename: 'Mutation', readonly requestCashout: { readonly __typename: 'RequestCashoutResponse', readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string, readonly path?: ReadonlyArray | null }>, readonly offer?: { readonly __typename: 'CashoutOffer', readonly exchangeRate: number, readonly expiresAt: number, readonly flashFee: number, readonly offerId: string, readonly receiveJmd: number, readonly receiveUsd: number, readonly send: number, readonly walletId: string } | null } }; +export type RequestCashoutMutation = { readonly __typename: 'Mutation', readonly requestCashout: { readonly __typename: 'RequestCashoutResponse', readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string }>, readonly offer?: { readonly __typename: 'CashoutOffer', readonly exchangeRate: number, readonly expiresAt: number, readonly flashFee: number, readonly offerId: string, readonly receiveJmd: number, readonly receiveUsd: number, readonly send: number, readonly walletId: string } | null } }; export type InitiateCashoutMutationVariables = Exact<{ input: InitiateCashoutInput; }>; -export type InitiateCashoutMutation = { readonly __typename: 'Mutation', readonly initiateCashout: { readonly __typename: 'SuccessPayload', readonly success?: boolean | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly path?: ReadonlyArray | null, readonly message: string, readonly code?: string | null }> } }; +export type InitiateCashoutMutation = { readonly __typename: 'Mutation', readonly initiateCashout: { readonly __typename: 'InitiatedCashoutResponse', readonly journalId?: string | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string }> } }; export type AccountDeleteMutationVariables = Exact<{ [key: string]: never; }>; @@ -3738,7 +3738,6 @@ export const RequestCashoutDocument = gql` errors { code message - path } offer { exchangeRate @@ -3783,11 +3782,10 @@ export const InitiateCashoutDocument = gql` mutation InitiateCashout($input: InitiateCashoutInput!) { initiateCashout(input: $input) { errors { - path - message code + message } - success + journalId } } `; diff --git a/app/screens/cashout-screen/CashoutConfirmation.tsx b/app/screens/cashout-screen/CashoutConfirmation.tsx index 3b3ecedbd..d129505f5 100644 --- a/app/screens/cashout-screen/CashoutConfirmation.tsx +++ b/app/screens/cashout-screen/CashoutConfirmation.tsx @@ -57,8 +57,7 @@ const CashoutConfirmation: React.FC = ({ navigation, route }) => { const onConfirm = async () => { toggleActivityIndicator(true) const res = await initiateCashout({ variables: { input: { walletId, offerId } } }) - console.log("RESPONSE>>>>>>>>>>>>", res) - if (res.data?.initiateCashout.success) { + if (res.data?.initiateCashout.journalId) { navigation.navigate("CashoutSuccess") } else { setErrorMsg(res.data?.initiateCashout.errors[0].message) @@ -87,14 +86,12 @@ const CashoutConfirmation: React.FC = ({ navigation, route }) => { {!!errorMsg && ( diff --git a/package.json b/package.json index fe415058f..bd0f0613e 100644 --- a/package.json +++ b/package.json @@ -272,6 +272,7 @@ "@wdio/local-runner": "^8.10.5", "@wdio/mocha-framework": "^8.12.1", "@wdio/spec-reporter": "^8.10.5", + "@whatwg-node/promise-helpers": "^1.3.2", "babel-jest": "^29.6.3", "babel-loader": "^9.1.2", "babel-plugin-module-resolver": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index 633af15d3..7dc86d3bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8552,6 +8552,13 @@ fast-querystring "^1.1.1" tslib "^2.3.1" +"@whatwg-node/promise-helpers@^1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@whatwg-node/promise-helpers/-/promise-helpers-1.3.2.tgz#3b54987ad6517ef6db5920c66a6f0dada606587d" + integrity sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA== + dependencies: + tslib "^2.6.3" + "@whatwg-node/server@^0.7.3": version "0.7.7" resolved "https://registry.yarnpkg.com/@whatwg-node/server/-/server-0.7.7.tgz#daaae73999cf8ea4d4f7e617276dcb8e84a6e49e" @@ -24835,7 +24842,7 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3 resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== -tslib@^2.8.1: +tslib@^2.6.3, tslib@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== From 73277bedc67b193091ef8a5228c0237ff4a6d6ff Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Wed, 29 Apr 2026 12:19:42 +0500 Subject: [PATCH 21/95] Withdraw to section is implemented and added on the cashout confirmation screen to show user account details --- .../cashout-flow/CashoutWithdrawTo.tsx | 99 +++++++++++++++++ app/components/cashout-flow/index.ts | 3 +- app/i18n/en/index.ts | 8 +- app/i18n/i18n-types.ts | 103 +++++++++++++----- app/i18n/raw-i18n/source/en.json | 8 +- .../cashout-screen/CashoutConfirmation.tsx | 3 +- 6 files changed, 190 insertions(+), 34 deletions(-) create mode 100644 app/components/cashout-flow/CashoutWithdrawTo.tsx diff --git a/app/components/cashout-flow/CashoutWithdrawTo.tsx b/app/components/cashout-flow/CashoutWithdrawTo.tsx new file mode 100644 index 000000000..fa7bf19b2 --- /dev/null +++ b/app/components/cashout-flow/CashoutWithdrawTo.tsx @@ -0,0 +1,99 @@ +import React, { useState } from "react" +import { TouchableOpacity, View } from "react-native" +import { Icon, makeStyles, Text, useTheme } from "@rneui/themed" + +import { useI18nContext } from "@app/i18n/i18n-react" +import { useLatestAccountUpgradeRequestQuery } from "@app/graphql/generated" + +const CashoutWithdrawTo: React.FC = () => { + const styles = useStyles() + const { colors } = useTheme().theme + const { LL } = useI18nContext() + const [expanded, setExpanded] = useState(false) + + const { data } = useLatestAccountUpgradeRequestQuery({ + fetchPolicy: "cache-first", + }) + + const bankAccount = data?.latestAccountUpgradeRequest.upgradeRequest?.bankAccount + console.log(">>>>>>>>>>>?????????", data) + + if (!bankAccount) return null + + const maskedAccount = `**********${String(bankAccount.accountNumber).slice(-4)}` + + return ( + + + {LL.Cashout.withdrawTo()} + + setExpanded(!expanded)} + activeOpacity={0.7} + > + + {maskedAccount} + + + {expanded && ( + + + + + + + + )} + + + ) +} + +const DetailRow: React.FC<{ label: string; value: string }> = ({ label, value }) => { + const styles = useStyles() + return ( + + + {label} + + {value} + + ) +} + +export default CashoutWithdrawTo + +const useStyles = makeStyles(({ colors }) => ({ + card: { + borderRadius: 10, + marginTop: 5, + marginBottom: 15, + padding: 15, + backgroundColor: colors.grey5, + }, + header: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + }, + details: { + marginTop: 12, + }, + detailRow: { + flexDirection: "row", + justifyContent: "space-between", + paddingVertical: 4, + }, + detailLabel: { + color: colors.grey3, + }, +})) diff --git a/app/components/cashout-flow/index.ts b/app/components/cashout-flow/index.ts index 338db47b5..ff0652b4f 100644 --- a/app/components/cashout-flow/index.ts +++ b/app/components/cashout-flow/index.ts @@ -1,5 +1,6 @@ import CashoutFromWallet from "./CashoutFromWallet" import CashoutPercentage from "./CashoutPercentage" import CashoutCard from "./CashoutCard" +import CashoutWithdrawTo from "./CashoutWithdrawTo" -export { CashoutFromWallet, CashoutPercentage, CashoutCard } +export { CashoutFromWallet, CashoutPercentage, CashoutCard, CashoutWithdrawTo } diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index 50f20185a..24efdaf80 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -89,7 +89,13 @@ const en: BaseTranslation = { receiveAmount: "Receive Amount", fee: "Fee", success: "Settlement request initiated successfully.", - disclaimer: `Please Note: Bank transfers are usually confirmed on the same-day, but may take longer if submitted during the following times:\n\n- Weekdays after 2:00pm\n- Fridays & weekends\n\nTransactions completed after 2:00 pm on weekdays are not confirmed by the bank until the following business day. Please contact us if you do not see your funds within 2-3 business days.\n` + disclaimer: `Please Note: Bank transfers are usually confirmed on the same-day, but may take longer if submitted during the following times:\n\n- Weekdays after 2:00pm\n- Fridays & weekends\n\nTransactions completed after 2:00 pm on weekdays are not confirmed by the bank until the following business day. Please contact us if you do not see your funds within 2-3 business days.\n`, + withdrawTo: "Withdraw to", + bankName: "Bank Name", + bankBranch: "Bank Branch", + accountNumber: "Account Number", + accountType: "Account Type", + currency: "Currency", }, ConversionDetailsScreen: { title: "Swap", diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index d71fc4c0f..b88bdcfda 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -311,6 +311,30 @@ type RootTranslation = { */ disclaimer: string + /** + * W​i​t​h​d​r​a​w​ ​t​o + */ + withdrawTo: string + /** + * B​a​n​k​ ​N​a​m​e + */ + bankName: string + /** + * B​a​n​k​ ​B​r​a​n​c​h + */ + bankBranch: string + /** + * A​c​c​o​u​n​t​ ​N​u​m​b​e​r + */ + accountNumber: string + /** + * A​c​c​o​u​n​t​ ​T​y​p​e + */ + accountType: string + /** + * C​u​r​r​e​n​c​y + */ + currency: string } ConversionDetailsScreen: { /** @@ -1804,7 +1828,7 @@ type RootTranslation = { */ bankTransfer: string /** - * T​r​a​n​s​f​e​r​ ​f​u​n​d​s​ ​f​r​o​m​ ​y​o​u​r​ ​b​a​n​k​ ​a​c​c​o​u​n​t + * T​r​a​n​s​f​e​r​ ​f​u​n​d​s​ ​f​r​o​m​ ​y​o​u​r​ ​b​a​n​k */ bankTransferDesc: string /** @@ -1812,7 +1836,7 @@ type RootTranslation = { */ debitCreditCard: string /** - * P​a​y​ ​w​i​t​h​ ​y​o​u​r​ ​c​a​r​d​ ​v​i​a​ ​F​y​g​a​r​o + * P​a​y​ ​w​i​t​h​ ​y​o​u​r​ ​d​e​b​i​t​ ​o​r​ ​c​r​e​d​i​t​ ​c​a​r​d */ debitCreditCardDesc: string } @@ -1822,7 +1846,7 @@ type RootTranslation = { */ title: string /** - * Bank Transfer + * B​a​n​k​ ​T​r​a​n​s​f​e​r */ bankTransfer: string /** @@ -1894,72 +1918,72 @@ type RootTranslation = { } BankTransfer: { /** - * Bank Transfer + * B​a​n​k​ ​T​r​a​n​s​f​e​r */ title: string /** - * Your order has been created. To complete the order, please transfer ${amount} USD to the bank details provided below. + * Y​o​u​r​ ​o​r​d​e​r​ ​h​a​s​ ​b​e​e​n​ ​c​r​e​a​t​e​d​.​ ​T​o​ ​c​o​m​p​l​e​t​e​ ​t​h​e​ ​o​r​d​e​r​,​ ​p​l​e​a​s​e​ ​t​r​a​n​s​f​e​r​ ​$​{​a​m​o​u​n​t​}​ ​U​S​D​ ​t​o​ ​t​h​e​ ​b​a​n​k​ ​d​e​t​a​i​l​s​ ​p​r​o​v​i​d​e​d​ ​b​e​l​o​w​. * @param {number} amount */ - desc1: string + desc1: RequiredParams<'amount'> /** - * Use {code} as the reference description. This unique code will help us associate the payment with your Flash account and process the Bitcoin transfer. + * U​s​e​ ​{​c​o​d​e​}​ ​a​s​ ​t​h​e​ ​r​e​f​e​r​e​n​c​e​ ​d​e​s​c​r​i​p​t​i​o​n​.​ ​T​h​i​s​ ​u​n​i​q​u​e​ ​c​o​d​e​ ​w​i​l​l​ ​h​e​l​p​ ​u​s​ ​a​s​s​o​c​i​a​t​e​ ​t​h​e​ ​p​a​y​m​e​n​t​ ​w​i​t​h​ ​y​o​u​r​ ​F​l​a​s​h​ ​a​c​c​o​u​n​t​ ​a​n​d​ ​p​r​o​c​e​s​s​ ​t​h​e​ ​B​i​t​c​o​i​n​ ​t​r​a​n​s​f​e​r​. * @param {string} code */ - desc2: string + desc2: RequiredParams<'code'> /** - * After we have received your payment, you will be credited with ${amount} USD in your Cash wallet, with a ${fee} USD fee deducted. You can then choose when you convert those USD to Bitcoin on your own using the Convert functionality in the mobile app. + * A​f​t​e​r​ ​w​e​ ​h​a​v​e​ ​r​e​c​e​i​v​e​d​ ​y​o​u​r​ ​p​a​y​m​e​n​t​,​ ​y​o​u​ ​w​i​l​l​ ​b​e​ ​c​r​e​d​i​t​e​d​ ​w​i​t​h​ ​$​{​a​m​o​u​n​t​}​ ​U​S​D​ ​i​n​ ​y​o​u​r​ ​C​a​s​h​ ​w​a​l​l​e​t​,​ ​w​i​t​h​ ​a​ ​$​{​f​e​e​}​ ​U​S​D​ ​f​e​e​ ​d​e​d​u​c​t​e​d​.​ ​Y​o​u​ ​c​a​n​ ​t​h​e​n​ ​c​h​o​o​s​e​ ​w​h​e​n​ ​y​o​u​ ​c​o​n​v​e​r​t​ ​t​h​o​s​e​ ​U​S​D​ ​t​o​ ​B​i​t​c​o​i​n​ ​o​n​ ​y​o​u​r​ ​o​w​n​ ​u​s​i​n​g​ ​t​h​e​ ​C​o​n​v​e​r​t​ ​f​u​n​c​t​i​o​n​a​l​i​t​y​ ​i​n​ ​t​h​e​ ​m​o​b​i​l​e​ ​a​p​p​. * @param {number} amount * @param {number} fee */ - desc3: string + desc3: RequiredParams<'amount' | 'fee'> /** - * Account Type + * A​c​c​o​u​n​t​ ​T​y​p​e */ accountType: string /** - * Destination Bank + * D​e​s​t​i​n​a​t​i​o​n​ ​B​a​n​k */ destinationBank: string /** - * Account Number + * A​c​c​o​u​n​t​ ​N​u​m​b​e​r */ accountNumber: string /** - * Type of Client + * T​y​p​e​ ​o​f​ ​C​l​i​e​n​t */ typeOfClient: string /** - * Receiver's Name + * R​e​c​e​i​v​e​r​'​s​ ​N​a​m​e */ receiverName: string /** - * Email + * E​m​a​i​l */ email: string /** - * Amount + * A​m​o​u​n​t */ amount: string /** - * Unique Code + * U​n​i​q​u​e​ ​C​o​d​e */ uniqueCode: string /** - * Fees + * F​e​e​s */ fees: string /** - * After payment completion on your end you can send us an email to {email} with a screenshot of your payment confirmation. - * @param {number} email + * A​f​t​e​r​ ​p​a​y​m​e​n​t​ ​c​o​m​p​l​e​t​i​o​n​ ​o​n​ ​y​o​u​r​ ​e​n​d​ ​y​o​u​ ​c​a​n​ ​s​e​n​d​ ​u​s​ ​a​n​ ​e​m​a​i​l​ ​t​o​ ​{​e​m​a​i​l​}​ ​w​i​t​h​ ​a​ ​s​c​r​e​e​n​s​h​o​t​ ​o​f​ ​y​o​u​r​ ​p​a​y​m​e​n​t​ ​c​o​n​f​i​r​m​a​t​i​o​n​. + * @param {string} email */ - desc4: string + desc4: RequiredParams<'email'> /** - * Your payment will be processed even if we don't receive this email, but having this confirmation can help accelerate the order. + * Y​o​u​r​ ​p​a​y​m​e​n​t​ ​w​i​l​l​ ​b​e​ ​p​r​o​c​e​s​s​e​d​ ​e​v​e​n​ ​i​f​ ​w​e​ ​d​o​n​'​t​ ​r​e​c​e​i​v​e​ ​t​h​i​s​ ​e​m​a​i​l​,​ ​b​u​t​ ​h​a​v​i​n​g​ ​t​h​i​s​ ​c​o​n​f​i​r​m​a​t​i​o​n​ ​c​a​n​ ​h​e​l​p​ ​a​c​c​e​l​e​r​a​t​e​ ​t​h​e​ ​o​r​d​e​r​. */ desc5: string /** - * Back to Home + * B​a​c​k​ ​t​o​ ​H​o​m​e */ backHome: string } @@ -3823,7 +3847,7 @@ type RootTranslation = { */ settle: string /** - * S​e​t​t​l​e​ ​p​e​n​d​i​n​g​ ​t​r​a​n​s​a​c​t​i​o​n​s + * C​a​s​h​o​u​t​ ​f​u​n​d​s​ ​f​r​o​m​ ​y​o​u​r​ ​w​a​l​l​e​t */ settleDesc: string } @@ -5757,6 +5781,30 @@ export type TranslationFunctions = { */ disclaimer: () => LocalizedString + /** + * Withdraw to + */ + withdrawTo: () => LocalizedString + /** + * Bank Name + */ + bankName: () => LocalizedString + /** + * Bank Branch + */ + bankBranch: () => LocalizedString + /** + * Account Number + */ + accountNumber: () => LocalizedString + /** + * Account Type + */ + accountType: () => LocalizedString + /** + * Currency + */ + currency: () => LocalizedString } ConversionDetailsScreen: { /** @@ -7341,18 +7389,14 @@ export type TranslationFunctions = { title: () => LocalizedString /** * Your order has been created. To complete the order, please transfer ${amount} USD to the bank details provided below. - * @param {number} amount */ desc1: (arg: { amount: number }) => LocalizedString /** * Use {code} as the reference description. This unique code will help us associate the payment with your Flash account and process the Bitcoin transfer. - * @param {string} code */ desc2: (arg: { code: string }) => LocalizedString /** * After we have received your payment, you will be credited with ${amount} USD in your Cash wallet, with a ${fee} USD fee deducted. You can then choose when you convert those USD to Bitcoin on your own using the Convert functionality in the mobile app. - * @param {number} amount - * @param {number} fee */ desc3: (arg: { amount: number, fee: number }) => LocalizedString /** @@ -7393,7 +7437,6 @@ export type TranslationFunctions = { fees: () => LocalizedString /** * After payment completion on your end you can send us an email to {email} with a screenshot of your payment confirmation. - * @param {string} email */ desc4: (arg: { email: string }) => LocalizedString /** diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index d65284da2..5f8be93f9 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -77,7 +77,13 @@ "receiveAmount": "Receive Amount", "fee": "Fee", "success": "Settlement request initiated successfully.", - "disclaimer": "Please Note: Bank transfers are usually confirmed on the same-day, but may take longer if submitted during the following times:\n\n- Weekdays after 2:00pm\n- Fridays & weekends\n\nTransactions completed after 2:00 pm on weekdays are not confirmed by the bank until the following business day. Please contact us if you do not see your funds within 2-3 business days.\n" + "disclaimer": "Please Note: Bank transfers are usually confirmed on the same-day, but may take longer if submitted during the following times:\n\n- Weekdays after 2:00pm\n- Fridays & weekends\n\nTransactions completed after 2:00 pm on weekdays are not confirmed by the bank until the following business day. Please contact us if you do not see your funds within 2-3 business days.\n", + "withdrawTo": "Withdraw to", + "bankName": "Bank Name", + "bankBranch": "Bank Branch", + "accountNumber": "Account Number", + "accountType": "Account Type", + "currency": "Currency" }, "ConversionDetailsScreen": { "title": "Swap", diff --git a/app/screens/cashout-screen/CashoutConfirmation.tsx b/app/screens/cashout-screen/CashoutConfirmation.tsx index d129505f5..bf3f47bd7 100644 --- a/app/screens/cashout-screen/CashoutConfirmation.tsx +++ b/app/screens/cashout-screen/CashoutConfirmation.tsx @@ -7,7 +7,7 @@ import moment from "moment" // components import { Screen } from "@app/components/screen" import { PrimaryBtn } from "@app/components/buttons" -import { CashoutCard, CashoutFromWallet } from "@app/components/cashout-flow" +import { CashoutCard, CashoutFromWallet, CashoutWithdrawTo } from "@app/components/cashout-flow" // hooks import { useI18nContext } from "@app/i18n/i18n-react" @@ -94,6 +94,7 @@ const CashoutConfirmation: React.FC = ({ navigation, route }) => { detail={`${formattedReceiveUsdAmount} (J$${(receiveJmd / 100).toFixed(2)})`} /> + {!!errorMsg && ( {errorMsg} From 5f28f1d345c193e07a43900adde0d4200d28d749 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Wed, 29 Apr 2026 18:47:39 +0500 Subject: [PATCH 22/95] use TransferOptionModal to show topup option on the TopupCashout screen when press Top up button --- app/i18n/en/index.ts | 10 +- app/i18n/i18n-types.ts | 64 ++++++++++ app/i18n/raw-i18n/source/en.json | 10 +- app/navigation/root-navigator.tsx | 2 - app/navigation/stack-param-lists.ts | 1 - app/screens/buy-bitcoin-flow/BuyBitcoin.tsx | 86 -------------- .../buy-bitcoin-flow/BuySellBitcoin.tsx | 86 ++++++++++++-- .../buy-bitcoin-flow/TransferOptionModal.tsx | 109 ++++++++++++++++++ app/screens/buy-bitcoin-flow/index.ts | 2 - 9 files changed, 267 insertions(+), 103 deletions(-) delete mode 100644 app/screens/buy-bitcoin-flow/BuyBitcoin.tsx create mode 100644 app/screens/buy-bitcoin-flow/TransferOptionModal.tsx diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index 24efdaf80..edab70cde 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -1175,7 +1175,15 @@ const en: BaseTranslation = { topUp: "Top Up", topUpDesc: "Add funds to your wallet", settle: "Settle", - settleDesc: "Cashout funds from your wallet" + settleDesc: "Cashout funds from your wallet", + selectSettleMethod: "Settle to", + selectTopUpMethod: "Top up via", + jmdBankAccount: "JMD Bank Account", + jmdBankAccountDesc: "Withdraw to your Jamaican bank account", + internationalBankAccount: "International Bank Account", + internationalBankAccountDesc: "Withdraw to your international bank account", + internationalBankTransfer: "International Bank Transfer", + internationalBankTransferDesc: "Transfer from your international bank account", }, UpgradeAccountModal: { title: "Upgrade your account", diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index b88bdcfda..2efb5f073 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -3850,6 +3850,38 @@ type RootTranslation = { * C​a​s​h​o​u​t​ ​f​u​n​d​s​ ​f​r​o​m​ ​y​o​u​r​ ​w​a​l​l​e​t */ settleDesc: string + /** + * S​e​t​t​l​e​ ​t​o + */ + selectSettleMethod: string + /** + * T​o​p​ ​u​p​ ​v​i​a + */ + selectTopUpMethod: string + /** + * J​M​D​ ​B​a​n​k​ ​A​c​c​o​u​n​t + */ + jmdBankAccount: string + /** + * W​i​t​h​d​r​a​w​ ​t​o​ ​y​o​u​r​ ​J​a​m​a​i​c​a​n​ ​b​a​n​k​ ​a​c​c​o​u​n​t + */ + jmdBankAccountDesc: string + /** + * I​n​t​e​r​n​a​t​i​o​n​a​l​ ​B​a​n​k​ ​A​c​c​o​u​n​t + */ + internationalBankAccount: string + /** + * W​i​t​h​d​r​a​w​ ​t​o​ ​y​o​u​r​ ​i​n​t​e​r​n​a​t​i​o​n​a​l​ ​b​a​n​k​ ​a​c​c​o​u​n​t + */ + internationalBankAccountDesc: string + /** + * I​n​t​e​r​n​a​t​i​o​n​a​l​ ​B​a​n​k​ ​T​r​a​n​s​f​e​r + */ + internationalBankTransfer: string + /** + * T​r​a​n​s​f​e​r​ ​f​r​o​m​ ​y​o​u​r​ ​i​n​t​e​r​n​a​t​i​o​n​a​l​ ​b​a​n​k​ ​a​c​c​o​u​n​t + */ + internationalBankTransferDesc: string } UpgradeAccountModal: { /** @@ -9260,6 +9292,38 @@ export type TranslationFunctions = { * Cashout funds from your wallet */ settleDesc: () => LocalizedString + /** + * Settle to + */ + selectSettleMethod: () => LocalizedString + /** + * Top up via + */ + selectTopUpMethod: () => LocalizedString + /** + * JMD Bank Account + */ + jmdBankAccount: () => LocalizedString + /** + * Withdraw to your Jamaican bank account + */ + jmdBankAccountDesc: () => LocalizedString + /** + * International Bank Account + */ + internationalBankAccount: () => LocalizedString + /** + * Withdraw to your international bank account + */ + internationalBankAccountDesc: () => LocalizedString + /** + * International Bank Transfer + */ + internationalBankTransfer: () => LocalizedString + /** + * Transfer from your international bank account + */ + internationalBankTransferDesc: () => LocalizedString } UpgradeAccountModal: { /** diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index 5f8be93f9..fdeb6d69b 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -1101,7 +1101,15 @@ "topUp": "Top Up", "topUpDesc": "Add funds to your wallet", "settle": "Settle", - "settleDesc": "Cashout funds from your wallet" + "settleDesc": "Cashout funds from your wallet", + "selectSettleMethod": "Settle to", + "selectTopUpMethod": "Top up via", + "jmdBankAccount": "JMD Bank Account", + "jmdBankAccountDesc": "Withdraw to your Jamaican bank account", + "internationalBankAccount": "International Bank Account", + "internationalBankAccountDesc": "Withdraw to your international bank account", + "internationalBankTransfer": "International Bank Transfer", + "internationalBankTransferDesc": "Transfer from your international bank account" }, "UpgradeAccountModal": { "title": "Upgrade your account", diff --git a/app/navigation/root-navigator.tsx b/app/navigation/root-navigator.tsx index 4e4df0266..7b2620a7d 100644 --- a/app/navigation/root-navigator.tsx +++ b/app/navigation/root-navigator.tsx @@ -131,7 +131,6 @@ import { import { FeaturedProfileView } from "@app/screens/featured-profile-view" import { BankTransfer, - BuyBitcoin, BuyBitcoinDetails, BuyBitcoinSuccess, BuySellBitcoin, @@ -670,7 +669,6 @@ export const RootStack = () => { }} > - diff --git a/app/navigation/stack-param-lists.ts b/app/navigation/stack-param-lists.ts index 95cbfe380..69fcff3b5 100644 --- a/app/navigation/stack-param-lists.ts +++ b/app/navigation/stack-param-lists.ts @@ -164,7 +164,6 @@ export type RootStackParamList = { // Featured profile WebView entry FeaturedProfileView: { entryPoint: "search" | "long_press" | "profile" } BuySellBitcoin: undefined - BuyBitcoin: undefined BuyBitcoinDetails: { paymentType: "card" | "bankTransfer" } BankTransfer: { amount: number diff --git a/app/screens/buy-bitcoin-flow/BuyBitcoin.tsx b/app/screens/buy-bitcoin-flow/BuyBitcoin.tsx deleted file mode 100644 index fc0f7b2f5..000000000 --- a/app/screens/buy-bitcoin-flow/BuyBitcoin.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React from "react" -import { TouchableOpacity, View } from "react-native" -import { RootStackParamList } from "@app/navigation/stack-param-lists" -import { Icon, Text, makeStyles, useTheme } from "@rneui/themed" -import { StackScreenProps } from "@react-navigation/stack" -import { useI18nContext } from "@app/i18n/i18n-react" - -// components -import { Screen } from "@app/components/screen" - -type Props = StackScreenProps - -const BuyBitcoin: React.FC = ({ navigation }) => { - const styles = useStyles() - const { LL } = useI18nContext() - const { colors } = useTheme().theme - - return ( - - - - {LL.TopUpScreen.title()} - - - navigation.navigate("BuyBitcoinDetails", { paymentType: "card" }) - } - > - - - {LL.TopUpScreen.debitCreditCard()} - - {LL.TopUpScreen.debitCreditCardDesc()} - - - - - - navigation.navigate("BuyBitcoinDetails", { paymentType: "bankTransfer" }) - } - > - - - - {LL.TopUpScreen.bankTransfer()} - - {LL.TopUpScreen.bankTransferDesc()} - - - - - - - ) -} - -const useStyles = makeStyles(({ colors }) => ({ - container: { - flex: 1, - paddingHorizontal: 20, - }, - title: { - textAlign: "center", - marginBottom: 30, - }, - btn: { - flexDirection: "row", - alignItems: "center", - borderRadius: 20, - borderWidth: 1, - borderColor: "#dedede", - marginBottom: 20, - minHeight: 100, - paddingHorizontal: 20, - }, - btnTextWrapper: { - flex: 1, - rowGap: 5, - marginHorizontal: 15, - }, -})) - -export default BuyBitcoin diff --git a/app/screens/buy-bitcoin-flow/BuySellBitcoin.tsx b/app/screens/buy-bitcoin-flow/BuySellBitcoin.tsx index f2a4c3169..36bcb52a0 100644 --- a/app/screens/buy-bitcoin-flow/BuySellBitcoin.tsx +++ b/app/screens/buy-bitcoin-flow/BuySellBitcoin.tsx @@ -1,4 +1,4 @@ -import React from "react" +import React, { useMemo, useState } from "react" import { TouchableOpacity, View } from "react-native" import { RootStackParamList } from "@app/navigation/stack-param-lists" import { Icon, Text, makeStyles, useTheme } from "@rneui/themed" @@ -7,6 +7,7 @@ import { useI18nContext } from "@app/i18n/i18n-react" // components import { Screen } from "@app/components/screen" +import TransferOptionModal, { TransferOption } from "./TransferOptionModal" // assets import ArrowDown from "@app/assets/icons/arrow-down-to-bracket.svg" @@ -19,13 +20,65 @@ const BuySellBitcoin: React.FC = ({ navigation }) => { const { LL } = useI18nContext() const { colors } = useTheme().theme - const handleTopUp = () => { - navigation.navigate("BuyBitcoin") - } + const [topUpModalVisible, setTopUpModalVisible] = useState(false) + const [settleModalVisible, setSettleModalVisible] = useState(false) - const handleSettle = () => { - navigation.navigate("CashoutDetails") - } + const topUpOptions: TransferOption[] = useMemo( + () => [ + { + icon: "card", + title: LL.TopUpScreen.debitCreditCard(), + description: LL.TopUpScreen.debitCreditCardDesc(), + onPress: () => { + setTopUpModalVisible(false) + navigation.navigate("BuyBitcoinDetails", { paymentType: "card" }) + }, + }, + { + icon: "business", + title: LL.TopUpScreen.bankTransfer(), + description: LL.TopUpScreen.bankTransferDesc(), + onPress: () => { + setTopUpModalVisible(false) + navigation.navigate("BuyBitcoinDetails", { paymentType: "bankTransfer" }) + }, + }, + { + icon: "globe", + title: LL.TransferScreen.internationalBankTransfer(), + description: LL.TransferScreen.internationalBankTransferDesc(), + onPress: () => { + setTopUpModalVisible(false) + // TODO: navigate to international bank transfer top up flow + }, + }, + ], + [LL, navigation], + ) + + const settleOptions: TransferOption[] = useMemo( + () => [ + { + icon: "business", + title: LL.TransferScreen.jmdBankAccount(), + description: LL.TransferScreen.jmdBankAccountDesc(), + onPress: () => { + setSettleModalVisible(false) + navigation.navigate("CashoutDetails") + }, + }, + { + icon: "globe", + title: LL.TransferScreen.internationalBankAccount(), + description: LL.TransferScreen.internationalBankAccountDesc(), + onPress: () => { + setSettleModalVisible(false) + // TODO: navigate to international bank account cashout flow + }, + }, + ], + [LL, navigation], + ) return ( @@ -33,7 +86,7 @@ const BuySellBitcoin: React.FC = ({ navigation }) => { {LL.TransferScreen.title()} - + setTopUpModalVisible(true)}> {LL.TransferScreen.topUp()} @@ -43,7 +96,7 @@ const BuySellBitcoin: React.FC = ({ navigation }) => { - + setSettleModalVisible(true)}> {LL.TransferScreen.settle()} @@ -54,11 +107,24 @@ const BuySellBitcoin: React.FC = ({ navigation }) => { + + setTopUpModalVisible(false)} + /> + setSettleModalVisible(false)} + /> ) } -const useStyles = makeStyles(({ colors }) => ({ +const useStyles = makeStyles(() => ({ container: { flex: 1, paddingHorizontal: 20, diff --git a/app/screens/buy-bitcoin-flow/TransferOptionModal.tsx b/app/screens/buy-bitcoin-flow/TransferOptionModal.tsx new file mode 100644 index 000000000..6f97f672a --- /dev/null +++ b/app/screens/buy-bitcoin-flow/TransferOptionModal.tsx @@ -0,0 +1,109 @@ +import React from "react" +import { Modal, TouchableOpacity, View } from "react-native" +import { Icon, Text, makeStyles, useTheme } from "@rneui/themed" +import { useSafeAreaInsets } from "react-native-safe-area-context" + +export type TransferOption = { + icon: string + title: string + description: string + onPress: () => void +} + +type Props = { + visible: boolean + title: string + options: TransferOption[] + onClose: () => void +} + +const TransferOptionModal: React.FC = ({ visible, title, options, onClose }) => { + const styles = useStyles() + const { colors, mode } = useTheme().theme + const bottom = useSafeAreaInsets().bottom + + return ( + + + + + {title} + + + + + {options.map((option, index) => ( + + + + {option.title} + + {option.description} + + + + + ))} + + + + ) +} + +export default TransferOptionModal + +const useStyles = makeStyles(({ colors }) => ({ + backdrop: { + flex: 1, + justifyContent: "flex-end", + }, + modalContainer: { + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + paddingTop: 20, + paddingHorizontal: 20, + }, + modalHeader: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + marginBottom: 15, + }, + closeBtn: { + padding: 5, + }, + optionBtn: { + flexDirection: "row", + alignItems: "center", + borderRadius: 15, + borderWidth: 1, + borderColor: colors.border01 || "#dedede", + marginBottom: 10, + paddingHorizontal: 15, + paddingVertical: 15, + }, + optionTextWrapper: { + flex: 1, + rowGap: 3, + marginHorizontal: 15, + }, +})) diff --git a/app/screens/buy-bitcoin-flow/index.ts b/app/screens/buy-bitcoin-flow/index.ts index 8c16ca65b..f6442bb44 100644 --- a/app/screens/buy-bitcoin-flow/index.ts +++ b/app/screens/buy-bitcoin-flow/index.ts @@ -1,5 +1,4 @@ import BuySellBitcoin from "./BuySellBitcoin" -import BuyBitcoin from "./BuyBitcoin" import BuyBitcoinDetails from "./BuyBitcoinDetails" import BankTransfer from "./BankTransfer" import CardPayment from "./CardPayment" @@ -7,7 +6,6 @@ import BuyBitcoinSuccess from "./BuyBitcoinSuccess" export { BuySellBitcoin, - BuyBitcoin, BuyBitcoinDetails, BankTransfer, CardPayment, From 66275f9a15950708f83f1dc9d3fea57499e0fc7f Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Thu, 30 Apr 2026 22:24:57 +0500 Subject: [PATCH 23/95] refactor and clean up topup/cashout flow updting structure --- app/components/home-screen/Buttons.tsx | 2 +- .../CashoutCard.tsx | 0 .../CashoutFromWallet.tsx | 0 .../CashoutPercentage.tsx | 0 .../CashoutWithdrawTo.tsx | 0 .../TransferOptionModal.tsx | 0 .../index.ts | 2 ++ app/i18n/en/index.ts | 2 +- app/i18n/i18n-types.ts | 4 +-- app/i18n/raw-i18n/source/en.json | 2 +- app/navigation/root-navigator.tsx | 30 +++++++++++-------- app/navigation/stack-param-lists.ts | 6 ++-- app/screens/buy-bitcoin-flow/index.ts | 13 -------- app/screens/cashout-screen/index.ts | 5 ---- .../BankTransfer.tsx | 0 .../CardPayment.tsx | 0 .../CashoutConfirmation.tsx | 2 +- .../CashoutDetails.tsx | 2 +- .../CashoutSuccess.tsx | 0 .../TopupCashout.tsx} | 30 +++++++++---------- .../TopupDetails.tsx} | 26 ++++++++-------- .../TopupSuccess.tsx} | 6 ++-- app/screens/topup-cashout-flow/index.ts | 21 +++++++++++++ .../payment-success-screen.tsx | 0 24 files changed, 81 insertions(+), 72 deletions(-) rename app/components/{cashout-flow => topup-cashout-flow}/CashoutCard.tsx (100%) rename app/components/{cashout-flow => topup-cashout-flow}/CashoutFromWallet.tsx (100%) rename app/components/{cashout-flow => topup-cashout-flow}/CashoutPercentage.tsx (100%) rename app/components/{cashout-flow => topup-cashout-flow}/CashoutWithdrawTo.tsx (100%) rename app/{screens/buy-bitcoin-flow => components/topup-cashout-flow}/TransferOptionModal.tsx (100%) rename app/components/{cashout-flow => topup-cashout-flow}/index.ts (67%) delete mode 100644 app/screens/buy-bitcoin-flow/index.ts delete mode 100644 app/screens/cashout-screen/index.ts rename app/screens/{buy-bitcoin-flow => topup-cashout-flow}/BankTransfer.tsx (100%) rename app/screens/{buy-bitcoin-flow => topup-cashout-flow}/CardPayment.tsx (100%) rename app/screens/{cashout-screen => topup-cashout-flow}/CashoutConfirmation.tsx (99%) rename app/screens/{cashout-screen => topup-cashout-flow}/CashoutDetails.tsx (99%) rename app/screens/{cashout-screen => topup-cashout-flow}/CashoutSuccess.tsx (100%) rename app/screens/{buy-bitcoin-flow/BuySellBitcoin.tsx => topup-cashout-flow/TopupCashout.tsx} (83%) rename app/screens/{buy-bitcoin-flow/BuyBitcoinDetails.tsx => topup-cashout-flow/TopupDetails.tsx} (89%) rename app/screens/{buy-bitcoin-flow/BuyBitcoinSuccess.tsx => topup-cashout-flow/TopupSuccess.tsx} (92%) create mode 100644 app/screens/topup-cashout-flow/index.ts rename app/screens/{buy-bitcoin-flow => topup-cashout-flow}/payment-success-screen.tsx (100%) diff --git a/app/components/home-screen/Buttons.tsx b/app/components/home-screen/Buttons.tsx index 3fc8a696f..a8814dbcd 100644 --- a/app/components/home-screen/Buttons.tsx +++ b/app/components/home-screen/Buttons.tsx @@ -71,7 +71,7 @@ const Buttons: React.FC = ({ setModalVisible, setDefaultAccountModalVisib if (currentLevel === AccountLevel.Two || currentLevel === AccountLevel.Three) { buttons.push({ title: LL.HomeScreen.transfer(), - target: "BuySellBitcoin", + target: "TopupCashout", icon: "upDown", }) } diff --git a/app/components/cashout-flow/CashoutCard.tsx b/app/components/topup-cashout-flow/CashoutCard.tsx similarity index 100% rename from app/components/cashout-flow/CashoutCard.tsx rename to app/components/topup-cashout-flow/CashoutCard.tsx diff --git a/app/components/cashout-flow/CashoutFromWallet.tsx b/app/components/topup-cashout-flow/CashoutFromWallet.tsx similarity index 100% rename from app/components/cashout-flow/CashoutFromWallet.tsx rename to app/components/topup-cashout-flow/CashoutFromWallet.tsx diff --git a/app/components/cashout-flow/CashoutPercentage.tsx b/app/components/topup-cashout-flow/CashoutPercentage.tsx similarity index 100% rename from app/components/cashout-flow/CashoutPercentage.tsx rename to app/components/topup-cashout-flow/CashoutPercentage.tsx diff --git a/app/components/cashout-flow/CashoutWithdrawTo.tsx b/app/components/topup-cashout-flow/CashoutWithdrawTo.tsx similarity index 100% rename from app/components/cashout-flow/CashoutWithdrawTo.tsx rename to app/components/topup-cashout-flow/CashoutWithdrawTo.tsx diff --git a/app/screens/buy-bitcoin-flow/TransferOptionModal.tsx b/app/components/topup-cashout-flow/TransferOptionModal.tsx similarity index 100% rename from app/screens/buy-bitcoin-flow/TransferOptionModal.tsx rename to app/components/topup-cashout-flow/TransferOptionModal.tsx diff --git a/app/components/cashout-flow/index.ts b/app/components/topup-cashout-flow/index.ts similarity index 67% rename from app/components/cashout-flow/index.ts rename to app/components/topup-cashout-flow/index.ts index ff0652b4f..9024cf4a4 100644 --- a/app/components/cashout-flow/index.ts +++ b/app/components/topup-cashout-flow/index.ts @@ -4,3 +4,5 @@ import CashoutCard from "./CashoutCard" import CashoutWithdrawTo from "./CashoutWithdrawTo" export { CashoutFromWallet, CashoutPercentage, CashoutCard, CashoutWithdrawTo } +export { default as TransferOptionModal } from "./TransferOptionModal" +export type { TransferOption } from "./TransferOptionModal" diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index edab70cde..66cf0252c 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -582,7 +582,7 @@ const en: BaseTranslation = { debitCreditCard: "Debit/Credit Card", debitCreditCardDesc: "Pay with your debit or credit card" }, - BuyBitcoinDetails: { + TopupDetails: { title: "Card Payment", bankTransfer: "Bank Transfer", email: "Email", diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index 2efb5f073..a7911b32b 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -1840,7 +1840,7 @@ type RootTranslation = { */ debitCreditCardDesc: string } - BuyBitcoinDetails: { + TopupDetails: { /** * C​a​r​d​ ​P​a​y​m​e​n​t */ @@ -7338,7 +7338,7 @@ export type TranslationFunctions = { */ debitCreditCardDesc: () => LocalizedString } - BuyBitcoinDetails: { + TopupDetails: { /** * Card Payment */ diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index fdeb6d69b..128855cc9 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -551,7 +551,7 @@ "debitCreditCard": "Debit/Credit Card", "debitCreditCardDesc": "Pay with your debit or credit card" }, - "BuyBitcoinDetails": { + "TopupDetails": { "title": "Card Payment", "bankTransfer": "Bank Transfer", "email": "Email", diff --git a/app/navigation/root-navigator.tsx b/app/navigation/root-navigator.tsx index 7b2620a7d..3f020c302 100644 --- a/app/navigation/root-navigator.tsx +++ b/app/navigation/root-navigator.tsx @@ -108,11 +108,6 @@ import HomeInactive from "@app/assets/icons/home-inactive.svg" import MapActive from "@app/assets/icons/map-active.svg" import MapInactive from "@app/assets/icons/map-inactive.svg" import ScanQR from "@app/assets/icons/scan-qr.svg" -import { - CashoutDetails, - CashoutConfirmation, - CashoutSuccess, -} from "@app/screens/cashout-screen" import { NostrSettingsScreen } from "@app/screens/settings-screen/nostr-settings/nostr-settings-screen" import ContactDetailsScreen from "@app/screens/chat/contactDetailsScreen" import { SupportGroupChatScreen } from "@app/screens/chat/GroupChat/SupportGroupChat" @@ -131,11 +126,15 @@ import { import { FeaturedProfileView } from "@app/screens/featured-profile-view" import { BankTransfer, - BuyBitcoinDetails, - BuyBitcoinSuccess, - BuySellBitcoin, + TopupDetails, + TopupSuccess, + TopupCashout, CardPayment, -} from "@app/screens/buy-bitcoin-flow" + CashoutDetails, + CashoutConfirmation, + CashoutSuccess, + PaymentSuccessScreen, +} from "@app/screens/topup-cashout-flow" const useStyles = makeStyles(({ colors }) => ({ bottomNavigatorStyle: { @@ -668,13 +667,18 @@ export const RootStack = () => { headerShadowVisible: false, }} > - - + + + diff --git a/app/navigation/stack-param-lists.ts b/app/navigation/stack-param-lists.ts index 69fcff3b5..486b53be9 100644 --- a/app/navigation/stack-param-lists.ts +++ b/app/navigation/stack-param-lists.ts @@ -163,8 +163,8 @@ export type RootStackParamList = { AccountUpgradeSuccess: undefined // Featured profile WebView entry FeaturedProfileView: { entryPoint: "search" | "long_press" | "profile" } - BuySellBitcoin: undefined - BuyBitcoinDetails: { paymentType: "card" | "bankTransfer" } + TopupCashout: undefined + TopupDetails: { paymentType: "card" | "bankTransfer" } BankTransfer: { amount: number wallet: string @@ -173,7 +173,7 @@ export type RootStackParamList = { amount: number wallet: string } - BuyBitcoinSuccess: undefined + TopupSuccess: undefined paymentSuccess: { amount: number wallet: string diff --git a/app/screens/buy-bitcoin-flow/index.ts b/app/screens/buy-bitcoin-flow/index.ts deleted file mode 100644 index f6442bb44..000000000 --- a/app/screens/buy-bitcoin-flow/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import BuySellBitcoin from "./BuySellBitcoin" -import BuyBitcoinDetails from "./BuyBitcoinDetails" -import BankTransfer from "./BankTransfer" -import CardPayment from "./CardPayment" -import BuyBitcoinSuccess from "./BuyBitcoinSuccess" - -export { - BuySellBitcoin, - BuyBitcoinDetails, - BankTransfer, - CardPayment, - BuyBitcoinSuccess, -} diff --git a/app/screens/cashout-screen/index.ts b/app/screens/cashout-screen/index.ts deleted file mode 100644 index 7d1f20b06..000000000 --- a/app/screens/cashout-screen/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import CashoutDetails from "./CashoutDetails" -import CashoutConfirmation from "./CashoutConfirmation" -import CashoutSuccess from "./CashoutSuccess" - -export { CashoutDetails, CashoutConfirmation, CashoutSuccess } diff --git a/app/screens/buy-bitcoin-flow/BankTransfer.tsx b/app/screens/topup-cashout-flow/BankTransfer.tsx similarity index 100% rename from app/screens/buy-bitcoin-flow/BankTransfer.tsx rename to app/screens/topup-cashout-flow/BankTransfer.tsx diff --git a/app/screens/buy-bitcoin-flow/CardPayment.tsx b/app/screens/topup-cashout-flow/CardPayment.tsx similarity index 100% rename from app/screens/buy-bitcoin-flow/CardPayment.tsx rename to app/screens/topup-cashout-flow/CardPayment.tsx diff --git a/app/screens/cashout-screen/CashoutConfirmation.tsx b/app/screens/topup-cashout-flow/CashoutConfirmation.tsx similarity index 99% rename from app/screens/cashout-screen/CashoutConfirmation.tsx rename to app/screens/topup-cashout-flow/CashoutConfirmation.tsx index bf3f47bd7..7ca651b46 100644 --- a/app/screens/cashout-screen/CashoutConfirmation.tsx +++ b/app/screens/topup-cashout-flow/CashoutConfirmation.tsx @@ -7,7 +7,7 @@ import moment from "moment" // components import { Screen } from "@app/components/screen" import { PrimaryBtn } from "@app/components/buttons" -import { CashoutCard, CashoutFromWallet, CashoutWithdrawTo } from "@app/components/cashout-flow" +import { CashoutCard, CashoutFromWallet, CashoutWithdrawTo } from "@app/components/topup-cashout-flow" // hooks import { useI18nContext } from "@app/i18n/i18n-react" diff --git a/app/screens/cashout-screen/CashoutDetails.tsx b/app/screens/topup-cashout-flow/CashoutDetails.tsx similarity index 99% rename from app/screens/cashout-screen/CashoutDetails.tsx rename to app/screens/topup-cashout-flow/CashoutDetails.tsx index 79ef9a0b1..dffa4442c 100644 --- a/app/screens/cashout-screen/CashoutDetails.tsx +++ b/app/screens/topup-cashout-flow/CashoutDetails.tsx @@ -7,7 +7,7 @@ import { RootStackParamList } from "@app/navigation/stack-param-lists" // components import { Screen } from "@app/components/screen" import { AmountInput } from "@app/components/amount-input" -import { CashoutFromWallet, CashoutPercentage } from "@app/components/cashout-flow" +import { CashoutFromWallet, CashoutPercentage } from "@app/components/topup-cashout-flow" // hooks import { useCashoutScreenQuery, useRequestCashoutMutation } from "@app/graphql/generated" diff --git a/app/screens/cashout-screen/CashoutSuccess.tsx b/app/screens/topup-cashout-flow/CashoutSuccess.tsx similarity index 100% rename from app/screens/cashout-screen/CashoutSuccess.tsx rename to app/screens/topup-cashout-flow/CashoutSuccess.tsx diff --git a/app/screens/buy-bitcoin-flow/BuySellBitcoin.tsx b/app/screens/topup-cashout-flow/TopupCashout.tsx similarity index 83% rename from app/screens/buy-bitcoin-flow/BuySellBitcoin.tsx rename to app/screens/topup-cashout-flow/TopupCashout.tsx index 36bcb52a0..6e61f5571 100644 --- a/app/screens/buy-bitcoin-flow/BuySellBitcoin.tsx +++ b/app/screens/topup-cashout-flow/TopupCashout.tsx @@ -7,31 +7,31 @@ import { useI18nContext } from "@app/i18n/i18n-react" // components import { Screen } from "@app/components/screen" -import TransferOptionModal, { TransferOption } from "./TransferOptionModal" +import TransferOptionModal, { TransferOption } from "@app/components/topup-cashout-flow/TransferOptionModal" // assets import ArrowDown from "@app/assets/icons/arrow-down-to-bracket.svg" import ArrowUp from "@app/assets/icons/arrow-up-from-bracket.svg" -type Props = StackScreenProps +type Props = StackScreenProps -const BuySellBitcoin: React.FC = ({ navigation }) => { +const TopupCashout: React.FC = ({ navigation }) => { const styles = useStyles() const { LL } = useI18nContext() const { colors } = useTheme().theme - const [topUpModalVisible, setTopUpModalVisible] = useState(false) + const [topupModalVisible, setTopupModalVisible] = useState(false) const [settleModalVisible, setSettleModalVisible] = useState(false) - const topUpOptions: TransferOption[] = useMemo( + const topupOptions: TransferOption[] = useMemo( () => [ { icon: "card", title: LL.TopUpScreen.debitCreditCard(), description: LL.TopUpScreen.debitCreditCardDesc(), onPress: () => { - setTopUpModalVisible(false) - navigation.navigate("BuyBitcoinDetails", { paymentType: "card" }) + setTopupModalVisible(false) + navigation.navigate("TopupDetails", { paymentType: "card" }) }, }, { @@ -39,8 +39,8 @@ const BuySellBitcoin: React.FC = ({ navigation }) => { title: LL.TopUpScreen.bankTransfer(), description: LL.TopUpScreen.bankTransferDesc(), onPress: () => { - setTopUpModalVisible(false) - navigation.navigate("BuyBitcoinDetails", { paymentType: "bankTransfer" }) + setTopupModalVisible(false) + navigation.navigate("TopupDetails", { paymentType: "bankTransfer" }) }, }, { @@ -48,7 +48,7 @@ const BuySellBitcoin: React.FC = ({ navigation }) => { title: LL.TransferScreen.internationalBankTransfer(), description: LL.TransferScreen.internationalBankTransferDesc(), onPress: () => { - setTopUpModalVisible(false) + setTopupModalVisible(false) // TODO: navigate to international bank transfer top up flow }, }, @@ -86,7 +86,7 @@ const BuySellBitcoin: React.FC = ({ navigation }) => { {LL.TransferScreen.title()} - setTopUpModalVisible(true)}> + setTopupModalVisible(true)}> {LL.TransferScreen.topUp()} @@ -109,10 +109,10 @@ const BuySellBitcoin: React.FC = ({ navigation }) => { setTopUpModalVisible(false)} + options={topupOptions} + onClose={() => setTopupModalVisible(false)} /> ({ }, })) -export default BuySellBitcoin +export default TopupCashout diff --git a/app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx b/app/screens/topup-cashout-flow/TopupDetails.tsx similarity index 89% rename from app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx rename to app/screens/topup-cashout-flow/TopupDetails.tsx index be83593e7..15172010b 100644 --- a/app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx +++ b/app/screens/topup-cashout-flow/TopupDetails.tsx @@ -1,5 +1,5 @@ /** - * BuyBitcoinDetails Component + * TopupDetails Component * * This screen collects payment details before initiating the topup flow. * Users select: @@ -32,9 +32,9 @@ import { useSafeAreaInsets } from "react-native-safe-area-context" import Cash from "@app/assets/icons/cash.svg" import Bitcoin from "@app/assets/icons/bitcoin.svg" -type Props = StackScreenProps +type Props = StackScreenProps -const BuyBitcoinDetails: React.FC = ({ navigation, route }) => { +const TopupDetails: React.FC = ({ navigation, route }) => { const { colors } = useTheme().theme const { LL } = useI18nContext() const { bottom } = useSafeAreaInsets() @@ -76,7 +76,7 @@ const BuyBitcoinDetails: React.FC = ({ navigation, route }) => { */ const handleContinue = async () => { if (!validateAmount(amount)) { - Alert.alert("Invalid Amount", LL.BuyBitcoinDetails.minimumAmount()) + Alert.alert("Invalid Amount", LL.TopupDetails.minimumAmount()) return } @@ -115,7 +115,7 @@ const BuyBitcoinDetails: React.FC = ({ navigation, route }) => { const walletButtons = [ { id: "USD", - text: LL.BuyBitcoinDetails.usdWallet(), + text: LL.TopupDetails.usdWallet(), icon: { selected: , normal: , @@ -123,7 +123,7 @@ const BuyBitcoinDetails: React.FC = ({ navigation, route }) => { }, { id: "BTC", - text: LL.BuyBitcoinDetails.btcWallet(), + text: LL.TopupDetails.btcWallet(), icon: { selected: , normal: , @@ -136,13 +136,13 @@ const BuyBitcoinDetails: React.FC = ({ navigation, route }) => { {route.params.paymentType === "card" - ? LL.BuyBitcoinDetails.title() - : LL.BuyBitcoinDetails.bankTransfer()} + ? LL.TopupDetails.title() + : LL.TopupDetails.bankTransfer()} - {LL.BuyBitcoinDetails.wallet()} + {LL.TopupDetails.wallet()} = ({ navigation, route }) => { - {LL.BuyBitcoinDetails.amount()} + {LL.TopupDetails.amount()} = ({ navigation, route }) => { (props: { bottom: number }) => ({ }, })) -export default BuyBitcoinDetails +export default TopupDetails diff --git a/app/screens/buy-bitcoin-flow/BuyBitcoinSuccess.tsx b/app/screens/topup-cashout-flow/TopupSuccess.tsx similarity index 92% rename from app/screens/buy-bitcoin-flow/BuyBitcoinSuccess.tsx rename to app/screens/topup-cashout-flow/TopupSuccess.tsx index 614daba8a..46da2ffe2 100644 --- a/app/screens/buy-bitcoin-flow/BuyBitcoinSuccess.tsx +++ b/app/screens/topup-cashout-flow/TopupSuccess.tsx @@ -17,9 +17,9 @@ import { GaloyIcon } from "@app/components/atomic/galoy-icon" import { useI18nContext } from "@app/i18n/i18n-react" import { useSafeAreaInsets } from "react-native-safe-area-context" -type Props = StackScreenProps +type Props = StackScreenProps -const BuyBitcoinSuccess: React.FC = () => { +const TopupSuccess: React.FC = () => { const styles = useStyles() const { colors } = useTheme().theme const { LL } = useI18nContext() @@ -62,7 +62,7 @@ const BuyBitcoinSuccess: React.FC = () => { ) } -export default BuyBitcoinSuccess +export default TopupSuccess const useStyles = makeStyles(() => ({ container: { diff --git a/app/screens/topup-cashout-flow/index.ts b/app/screens/topup-cashout-flow/index.ts new file mode 100644 index 000000000..47ec637b0 --- /dev/null +++ b/app/screens/topup-cashout-flow/index.ts @@ -0,0 +1,21 @@ +import TopupCashout from "./TopupCashout" +import TopupDetails from "./TopupDetails" +import BankTransfer from "./BankTransfer" +import CardPayment from "./CardPayment" +import TopupSuccess from "./TopupSuccess" +import CashoutDetails from "./CashoutDetails" +import CashoutConfirmation from "./CashoutConfirmation" +import CashoutSuccess from "./CashoutSuccess" +import PaymentSuccessScreen from "./payment-success-screen" + +export { + TopupCashout, + TopupDetails, + BankTransfer, + CardPayment, + TopupSuccess, + CashoutDetails, + CashoutConfirmation, + CashoutSuccess, + PaymentSuccessScreen, +} diff --git a/app/screens/buy-bitcoin-flow/payment-success-screen.tsx b/app/screens/topup-cashout-flow/payment-success-screen.tsx similarity index 100% rename from app/screens/buy-bitcoin-flow/payment-success-screen.tsx rename to app/screens/topup-cashout-flow/payment-success-screen.tsx From 0d918df88633c6184af33d0c54f8f9c2ada13dd2 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Wed, 6 May 2026 11:10:04 +0500 Subject: [PATCH 24/95] Implement BridgeKycModal to get user details (full name, email and kyc type) and use on the TopupCashout screen --- .typesafe-i18n.json | 2 +- .../topup-cashout-flow/BridgeKycModal.tsx | 190 ++++++++++++++++++ app/components/topup-cashout-flow/index.ts | 1 + app/i18n/en/index.ts | 17 ++ app/i18n/i18n-types.ts | 124 ++++++++++++ app/i18n/raw-i18n/source/en.json | 17 ++ .../topup-cashout-flow/TopupCashout.tsx | 18 +- ios/LNFlash.xcodeproj/project.pbxproj | 56 +++--- 8 files changed, 389 insertions(+), 36 deletions(-) create mode 100644 app/components/topup-cashout-flow/BridgeKycModal.tsx diff --git a/.typesafe-i18n.json b/.typesafe-i18n.json index d40474af1..753a37c8d 100644 --- a/.typesafe-i18n.json +++ b/.typesafe-i18n.json @@ -1,5 +1,5 @@ { "adapter": "react", - "$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json", + "$schema": "https://unpkg.com/typesafe-i18n@5.27.1/schema/typesafe-i18n.json", "outputPath": "./app/i18n" } \ No newline at end of file diff --git a/app/components/topup-cashout-flow/BridgeKycModal.tsx b/app/components/topup-cashout-flow/BridgeKycModal.tsx new file mode 100644 index 000000000..ee3cf2bed --- /dev/null +++ b/app/components/topup-cashout-flow/BridgeKycModal.tsx @@ -0,0 +1,190 @@ +import React, { useState } from "react" +import { KeyboardAvoidingView, Modal, Platform, ScrollView, TouchableOpacity, View } from "react-native" +import { Icon, Text, makeStyles, useTheme } from "@rneui/themed" +import { useSafeAreaInsets } from "react-native-safe-area-context" +import { useI18nContext } from "@app/i18n/i18n-react" + +import { InputField, DropDownField } from "@app/components/account-upgrade-flow" +import { PrimaryBtn } from "@app/components/buttons" + +type BridgeKycModalProps = { + visible: boolean + onClose: () => void + onSubmit: (data: { fullName: string; email: string; kycType: string }) => void +} + +const BridgeKycModal: React.FC = ({ visible, onClose, onSubmit }) => { + const styles = useStyles() + const { colors, mode } = useTheme().theme + const bottom = useSafeAreaInsets().bottom + const { LL } = useI18nContext() + + const [fullName, setFullName] = useState("") + const [email, setEmail] = useState("") + const [kycType, setKycType] = useState("") + + const [fullNameErr, setFullNameErr] = useState() + const [emailErr, setEmailErr] = useState() + const [kycTypeErr, setKycTypeErr] = useState() + + const kycTypeOptions = [ + { label: LL.BridgeKyc.individual(), value: "individual" }, + { label: LL.BridgeKyc.business(), value: "business" }, + ] + + const validate = (): boolean => { + let isValid = true + + if (!fullName.trim()) { + setFullNameErr(LL.BridgeKyc.fullNameRequired()) + isValid = false + } + + if (!email.trim()) { + setEmailErr(LL.BridgeKyc.emailRequired()) + isValid = false + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) { + setEmailErr(LL.BridgeKyc.invalidEmail()) + isValid = false + } + + if (!kycType) { + setKycTypeErr(LL.BridgeKyc.kycTypeRequired()) + isValid = false + } + + return isValid + } + + const handleSubmit = () => { + if (!validate()) return + onSubmit({ fullName: fullName.trim(), email: email.trim(), kycType }) + } + + const handleClose = () => { + setFullName("") + setEmail("") + setKycType("") + setFullNameErr(undefined) + setEmailErr(undefined) + setKycTypeErr(undefined) + onClose() + } + + return ( + + + + true} + > + + {LL.BridgeKyc.title()} + + + + + + + + {LL.BridgeKyc.description()} + + + { + setFullNameErr(undefined) + setFullName(val) + }} + /> + + { + setEmailErr(undefined) + setEmail(val) + }} + /> + + { + setKycTypeErr(undefined) + setKycType(val) + }} + /> + + + + + + + + ) +} + +export default BridgeKycModal + +const useStyles = makeStyles(({ colors }) => ({ + backdrop: { + flex: 1, + justifyContent: "flex-end", + }, + keyboardAvoid: { + justifyContent: "flex-end", + }, + modalContainer: { + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + paddingTop: 20, + paddingHorizontal: 20, + }, + modalHeader: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + marginBottom: 15, + }, + closeBtn: { + padding: 5, + }, + description: { + marginBottom: 20, + }, + submitBtn: { + marginTop: 10, + }, +})) diff --git a/app/components/topup-cashout-flow/index.ts b/app/components/topup-cashout-flow/index.ts index 9024cf4a4..8529de435 100644 --- a/app/components/topup-cashout-flow/index.ts +++ b/app/components/topup-cashout-flow/index.ts @@ -6,3 +6,4 @@ import CashoutWithdrawTo from "./CashoutWithdrawTo" export { CashoutFromWallet, CashoutPercentage, CashoutCard, CashoutWithdrawTo } export { default as TransferOptionModal } from "./TransferOptionModal" export type { TransferOption } from "./TransferOptionModal" +export { default as BridgeKycModal } from "./BridgeKycModal" diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index 66cf0252c..1cf92ed47 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -598,6 +598,23 @@ const en: BaseTranslation = { invalidAmount: "Please enter a valid amount", minimumAmount: "Minimum amount is $1.00" }, + BridgeKyc: { + title: "International Transfer", + description: "Please provide your details to set up international transfers", + fullName: "Full Name", + fullNamePlaceholder: "Enter your full name", + email: "Email", + emailPlaceholder: "Enter your email address", + kycType: "Account Type", + kycTypePlaceholder: "Select account type", + individual: "Individual", + business: "Business", + submit: "Submit", + fullNameRequired: "Full name is required", + emailRequired: "Email is required", + invalidEmail: "Please enter a valid email address", + kycTypeRequired: "Please select an account type", + }, FygaroWebViewScreen: { title: "Fygaro Payment", loading: "Loading payment page...", diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index a7911b32b..df0a0b6eb 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -1898,6 +1898,68 @@ type RootTranslation = { */ minimumAmount: string } + BridgeKyc: { + /** + * I​n​t​e​r​n​a​t​i​o​n​a​l​ ​T​r​a​n​s​f​e​r + */ + title: string + /** + * P​l​e​a​s​e​ ​p​r​o​v​i​d​e​ ​y​o​u​r​ ​d​e​t​a​i​l​s​ ​t​o​ ​s​e​t​ ​u​p​ ​i​n​t​e​r​n​a​t​i​o​n​a​l​ ​t​r​a​n​s​f​e​r​s + */ + description: string + /** + * F​u​l​l​ ​N​a​m​e + */ + fullName: string + /** + * E​n​t​e​r​ ​y​o​u​r​ ​f​u​l​l​ ​n​a​m​e + */ + fullNamePlaceholder: string + /** + * E​m​a​i​l + */ + email: string + /** + * E​n​t​e​r​ ​y​o​u​r​ ​e​m​a​i​l​ ​a​d​d​r​e​s​s + */ + emailPlaceholder: string + /** + * A​c​c​o​u​n​t​ ​T​y​p​e + */ + kycType: string + /** + * S​e​l​e​c​t​ ​a​c​c​o​u​n​t​ ​t​y​p​e + */ + kycTypePlaceholder: string + /** + * I​n​d​i​v​i​d​u​a​l + */ + individual: string + /** + * B​u​s​i​n​e​s​s + */ + business: string + /** + * S​u​b​m​i​t + */ + submit: string + /** + * F​u​l​l​ ​n​a​m​e​ ​i​s​ ​r​e​q​u​i​r​e​d + */ + fullNameRequired: string + /** + * E​m​a​i​l​ ​i​s​ ​r​e​q​u​i​r​e​d + */ + emailRequired: string + /** + * P​l​e​a​s​e​ ​e​n​t​e​r​ ​a​ ​v​a​l​i​d​ ​e​m​a​i​l​ ​a​d​d​r​e​s​s + */ + invalidEmail: string + /** + * P​l​e​a​s​e​ ​s​e​l​e​c​t​ ​a​n​ ​a​c​c​o​u​n​t​ ​t​y​p​e + */ + kycTypeRequired: string + } FygaroWebViewScreen: { /** * F​y​g​a​r​o​ ​P​a​y​m​e​n​t @@ -7396,6 +7458,68 @@ export type TranslationFunctions = { */ minimumAmount: () => LocalizedString } + BridgeKyc: { + /** + * International Transfer + */ + title: () => LocalizedString + /** + * Please provide your details to set up international transfers + */ + description: () => LocalizedString + /** + * Full Name + */ + fullName: () => LocalizedString + /** + * Enter your full name + */ + fullNamePlaceholder: () => LocalizedString + /** + * Email + */ + email: () => LocalizedString + /** + * Enter your email address + */ + emailPlaceholder: () => LocalizedString + /** + * Account Type + */ + kycType: () => LocalizedString + /** + * Select account type + */ + kycTypePlaceholder: () => LocalizedString + /** + * Individual + */ + individual: () => LocalizedString + /** + * Business + */ + business: () => LocalizedString + /** + * Submit + */ + submit: () => LocalizedString + /** + * Full name is required + */ + fullNameRequired: () => LocalizedString + /** + * Email is required + */ + emailRequired: () => LocalizedString + /** + * Please enter a valid email address + */ + invalidEmail: () => LocalizedString + /** + * Please select an account type + */ + kycTypeRequired: () => LocalizedString + } FygaroWebViewScreen: { /** * Fygaro Payment diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index 128855cc9..8e05a5540 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -567,6 +567,23 @@ "invalidAmount": "Please enter a valid amount", "minimumAmount": "Minimum amount is $1.00" }, + "BridgeKyc": { + "title": "International Transfer", + "description": "Please provide your details to set up international transfers", + "fullName": "Full Name", + "fullNamePlaceholder": "Enter your full name", + "email": "Email", + "emailPlaceholder": "Enter your email address", + "kycType": "Account Type", + "kycTypePlaceholder": "Select account type", + "individual": "Individual", + "business": "Business", + "submit": "Submit", + "fullNameRequired": "Full name is required", + "emailRequired": "Email is required", + "invalidEmail": "Please enter a valid email address", + "kycTypeRequired": "Please select an account type" + }, "FygaroWebViewScreen": { "title": "Fygaro Payment", "loading": "Loading payment page...", diff --git a/app/screens/topup-cashout-flow/TopupCashout.tsx b/app/screens/topup-cashout-flow/TopupCashout.tsx index 6e61f5571..3ba19571e 100644 --- a/app/screens/topup-cashout-flow/TopupCashout.tsx +++ b/app/screens/topup-cashout-flow/TopupCashout.tsx @@ -7,7 +7,8 @@ import { useI18nContext } from "@app/i18n/i18n-react" // components import { Screen } from "@app/components/screen" -import TransferOptionModal, { TransferOption } from "@app/components/topup-cashout-flow/TransferOptionModal" +import { TransferOptionModal, BridgeKycModal } from "@app/components/topup-cashout-flow" +import type { TransferOption } from "@app/components/topup-cashout-flow" // assets import ArrowDown from "@app/assets/icons/arrow-down-to-bracket.svg" @@ -22,6 +23,7 @@ const TopupCashout: React.FC = ({ navigation }) => { const [topupModalVisible, setTopupModalVisible] = useState(false) const [settleModalVisible, setSettleModalVisible] = useState(false) + const [bridgeKycModalVisible, setBridgeKycModalVisible] = useState(false) const topupOptions: TransferOption[] = useMemo( () => [ @@ -49,7 +51,7 @@ const TopupCashout: React.FC = ({ navigation }) => { description: LL.TransferScreen.internationalBankTransferDesc(), onPress: () => { setTopupModalVisible(false) - // TODO: navigate to international bank transfer top up flow + setBridgeKycModalVisible(true) }, }, ], @@ -73,7 +75,7 @@ const TopupCashout: React.FC = ({ navigation }) => { description: LL.TransferScreen.internationalBankAccountDesc(), onPress: () => { setSettleModalVisible(false) - // TODO: navigate to international bank account cashout flow + setBridgeKycModalVisible(true) }, }, ], @@ -120,6 +122,16 @@ const TopupCashout: React.FC = ({ navigation }) => { options={settleOptions} onClose={() => setSettleModalVisible(false)} /> + + setBridgeKycModalVisible(false)} + onSubmit={(data) => { + setBridgeKycModalVisible(false) + // TODO: send KYC data to Bridge API when query is ready + console.log("Bridge KYC submitted:", data) + }} + /> ) } diff --git a/ios/LNFlash.xcodeproj/project.pbxproj b/ios/LNFlash.xcodeproj/project.pbxproj index 0de92366f..9be536376 100644 --- a/ios/LNFlash.xcodeproj/project.pbxproj +++ b/ios/LNFlash.xcodeproj/project.pbxproj @@ -12,9 +12,9 @@ 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; - 3152A8AF830F40A7AB689E42 /* (null) in Resources */ = {isa = PBXBuildFile; }; - 3E3C92C2FC91412D8DFDE94D /* (null) in Resources */ = {isa = PBXBuildFile; }; - 560415592C5B4119B9F91553 /* (null) in Resources */ = {isa = PBXBuildFile; }; + 3152A8AF830F40A7AB689E42 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + 3E3C92C2FC91412D8DFDE94D /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + 560415592C5B4119B9F91553 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; 5CDD582462391BD4A7901BD8 /* libPods-LNFlash.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B660162FC198C5D612BB44A /* libPods-LNFlash.a */; }; 69B54F686F5835A64060EE6D /* libPods-LNFlash-Alt.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 177D9FE829C0CBCE225C8178 /* libPods-LNFlash-Alt.a */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; @@ -27,18 +27,18 @@ 8647B9CB2DFD831600E2F160 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 8647B9CC2DFD831600E2F160 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 86FB60FB2BC40BBC0088C78C /* PrivacyInfo.xcprivacy */; }; 8647B9CD2DFD831600E2F160 /* coins.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0147E0DC2BAE64B90071CDF2 /* coins.mp3 */; }; - 8647B9CE2DFD831600E2F160 /* (null) in Resources */ = {isa = PBXBuildFile; }; - 8647B9CF2DFD831600E2F160 /* (null) in Resources */ = {isa = PBXBuildFile; }; - 8647B9D02DFD831600E2F160 /* (null) in Resources */ = {isa = PBXBuildFile; }; - 8647B9D12DFD831600E2F160 /* (null) in Resources */ = {isa = PBXBuildFile; }; - 8647B9D22DFD831600E2F160 /* (null) in Resources */ = {isa = PBXBuildFile; }; - 8647B9D32DFD831600E2F160 /* (null) in Resources */ = {isa = PBXBuildFile; }; + 8647B9CE2DFD831600E2F160 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + 8647B9CF2DFD831600E2F160 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + 8647B9D02DFD831600E2F160 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + 8647B9D12DFD831600E2F160 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + 8647B9D22DFD831600E2F160 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + 8647B9D32DFD831600E2F160 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; 8647B9ED2DFEAFE500E2F160 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 8647B9EC2DFEAFE500E2F160 /* GoogleService-Info.plist */; }; 8647B9EF2DFEAFF900E2F160 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 8647B9EE2DFEAFF900E2F160 /* GoogleService-Info.plist */; }; 86FB60FC2BC40BBC0088C78C /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 86FB60FB2BC40BBC0088C78C /* PrivacyInfo.xcprivacy */; }; - 94D9F3C84EB547D68D41C50F /* (null) in Resources */ = {isa = PBXBuildFile; }; - BD157A9851974C298EB06CB7 /* (null) in Resources */ = {isa = PBXBuildFile; }; - C426C81D58C8450C878B6086 /* (null) in Resources */ = {isa = PBXBuildFile; }; + 94D9F3C84EB547D68D41C50F /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + BD157A9851974C298EB06CB7 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + C426C81D58C8450C878B6086 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; C61EE0AD23C530E30054100C /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C61EE0AC23C530E30054100C /* AuthenticationServices.framework */; }; F1D71F3628CE5C9A00636277 /* AppDelegate.h in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FAF1A68108700A75B9A /* AppDelegate.h */; }; /* End PBXBuildFile section */ @@ -285,12 +285,12 @@ 86FB60FC2BC40BBC0088C78C /* PrivacyInfo.xcprivacy in Resources */, 0147E0DD2BAE64B90071CDF2 /* coins.mp3 in Resources */, 8647B9ED2DFEAFE500E2F160 /* GoogleService-Info.plist in Resources */, - 560415592C5B4119B9F91553 /* (null) in Resources */, - 3E3C92C2FC91412D8DFDE94D /* (null) in Resources */, - BD157A9851974C298EB06CB7 /* (null) in Resources */, - 3152A8AF830F40A7AB689E42 /* (null) in Resources */, - 94D9F3C84EB547D68D41C50F /* (null) in Resources */, - C426C81D58C8450C878B6086 /* (null) in Resources */, + 560415592C5B4119B9F91553 /* BuildFile in Resources */, + 3E3C92C2FC91412D8DFDE94D /* BuildFile in Resources */, + BD157A9851974C298EB06CB7 /* BuildFile in Resources */, + 3152A8AF830F40A7AB689E42 /* BuildFile in Resources */, + 94D9F3C84EB547D68D41C50F /* BuildFile in Resources */, + C426C81D58C8450C878B6086 /* BuildFile in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -304,12 +304,12 @@ 8647B9CC2DFD831600E2F160 /* PrivacyInfo.xcprivacy in Resources */, 8647B9CD2DFD831600E2F160 /* coins.mp3 in Resources */, 8647B9EF2DFEAFF900E2F160 /* GoogleService-Info.plist in Resources */, - 8647B9CE2DFD831600E2F160 /* (null) in Resources */, - 8647B9CF2DFD831600E2F160 /* (null) in Resources */, - 8647B9D02DFD831600E2F160 /* (null) in Resources */, - 8647B9D12DFD831600E2F160 /* (null) in Resources */, - 8647B9D22DFD831600E2F160 /* (null) in Resources */, - 8647B9D32DFD831600E2F160 /* (null) in Resources */, + 8647B9CE2DFD831600E2F160 /* BuildFile in Resources */, + 8647B9CF2DFD831600E2F160 /* BuildFile in Resources */, + 8647B9D02DFD831600E2F160 /* BuildFile in Resources */, + 8647B9D12DFD831600E2F160 /* BuildFile in Resources */, + 8647B9D22DFD831600E2F160 /* BuildFile in Resources */, + 8647B9D32DFD831600E2F160 /* BuildFile in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -339,8 +339,6 @@ "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", ); name = "[CP-User] [RNFB] Core Configuration"; - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"note: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"note: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"note: -> RNFB build script started\"\necho \"note: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"note: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"note: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n if ! _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\"); then\n echo \"error: Failed to parse firebase.json, check for syntax errors.\"\n exit 1\n fi\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"error: python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_analytics_storage\n _ANALYTICS_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_analytics_storage\")\n if [[ $_ANALYTICS_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_storage\n _ANALYTICS_AD_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_storage\")\n if [[ $_ANALYTICS_AD_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_user_data\n _ANALYTICS_AD_USER_DATA=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_user_data\")\n if [[ $_ANALYTICS_AD_USER_DATA ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_USER_DATA\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.analytics_registration_with_ad_network_enabled\n _ANALYTICS_REGISTRATION_WITH_AD_NETWORK=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_registration_with_ad_network_enabled\")\n if [[ $_ANALYTICS_REGISTRATION_WITH_AD_NETWORK ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_REGISTRATION_WITH_AD_NETWORK_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_REGISTRATION_WITH_AD_NETWORK\")\")\n fi\n\n # config.google_analytics_automatic_screen_reporting_enabled\n _ANALYTICS_AUTO_SCREEN_REPORTING=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_automatic_screen_reporting_enabled\")\n if [[ $_ANALYTICS_AUTO_SCREEN_REPORTING ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAutomaticScreenReportingEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_SCREEN_REPORTING\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"note: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"note: <- RNFB build script finished\"\n"; @@ -355,8 +353,6 @@ "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", ); name = "[CP-User] [RNFB] Crashlytics Configuration"; - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\nif [[ ${PODS_ROOT} ]]; then\n echo \"info: Exec FirebaseCrashlytics Run from Pods\"\n \"${PODS_ROOT}/FirebaseCrashlytics/run\"\nelse\n echo \"info: Exec FirebaseCrashlytics Run from framework\"\n \"${PROJECT_DIR}/FirebaseCrashlytics.framework/run\"\nfi\n"; @@ -494,8 +490,6 @@ "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", ); name = "[CP-User] [RNFB] Core Configuration"; - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"note: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"note: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"note: -> RNFB build script started\"\necho \"note: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"note: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"note: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n if ! _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\"); then\n echo \"error: Failed to parse firebase.json, check for syntax errors.\"\n exit 1\n fi\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"error: python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_analytics_storage\n _ANALYTICS_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_analytics_storage\")\n if [[ $_ANALYTICS_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_storage\n _ANALYTICS_AD_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_storage\")\n if [[ $_ANALYTICS_AD_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_user_data\n _ANALYTICS_AD_USER_DATA=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_user_data\")\n if [[ $_ANALYTICS_AD_USER_DATA ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_USER_DATA\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.analytics_registration_with_ad_network_enabled\n _ANALYTICS_REGISTRATION_WITH_AD_NETWORK=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_registration_with_ad_network_enabled\")\n if [[ $_ANALYTICS_REGISTRATION_WITH_AD_NETWORK ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_REGISTRATION_WITH_AD_NETWORK_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_REGISTRATION_WITH_AD_NETWORK\")\")\n fi\n\n # config.google_analytics_automatic_screen_reporting_enabled\n _ANALYTICS_AUTO_SCREEN_REPORTING=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_automatic_screen_reporting_enabled\")\n if [[ $_ANALYTICS_AUTO_SCREEN_REPORTING ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAutomaticScreenReportingEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_SCREEN_REPORTING\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"note: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"note: <- RNFB build script finished\"\n"; @@ -758,8 +752,6 @@ "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", ); name = "[CP-User] [RNFB] Crashlytics Configuration"; - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\nif [[ ${PODS_ROOT} ]]; then\n echo \"info: Exec FirebaseCrashlytics Run from Pods\"\n \"${PODS_ROOT}/FirebaseCrashlytics/run\"\nelse\n echo \"info: Exec FirebaseCrashlytics Run from framework\"\n \"${PROJECT_DIR}/FirebaseCrashlytics.framework/run\"\nfi\n"; From a2861eefd7f7940aa8d1dacfc0dbe1421b645642 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Fri, 15 May 2026 14:29:33 +0500 Subject: [PATCH 25/95] Implement BridgeKycWebView screen to handle tos and kyc flow and add in the root navigation --- app/navigation/root-navigator.tsx | 9 + app/navigation/stack-param-lists.ts | 4 + .../topup-cashout-flow/BridgeKycWebView.tsx | 275 ++++++++++++++++++ app/screens/topup-cashout-flow/index.ts | 2 + 4 files changed, 290 insertions(+) create mode 100644 app/screens/topup-cashout-flow/BridgeKycWebView.tsx diff --git a/app/navigation/root-navigator.tsx b/app/navigation/root-navigator.tsx index 3f020c302..4077a1a2f 100644 --- a/app/navigation/root-navigator.tsx +++ b/app/navigation/root-navigator.tsx @@ -126,6 +126,7 @@ import { import { FeaturedProfileView } from "@app/screens/featured-profile-view" import { BankTransfer, + BridgeKycWebView, TopupDetails, TopupSuccess, TopupCashout, @@ -668,6 +669,14 @@ export const RootStack = () => { }} > + diff --git a/app/navigation/stack-param-lists.ts b/app/navigation/stack-param-lists.ts index 7862bd3dd..f72df14ef 100644 --- a/app/navigation/stack-param-lists.ts +++ b/app/navigation/stack-param-lists.ts @@ -163,6 +163,10 @@ export type RootStackParamList = { AccountUpgradeSuccess: undefined // Featured profile WebView entry FeaturedProfileView: { entryPoint: "search" | "long_press" | "profile" } + BridgeKycWebView: { + tosLink: string + kycLink: string + } TopupCashout: undefined TopupDetails: { paymentType: "card" | "bankTransfer" } BankTransfer: { diff --git a/app/screens/topup-cashout-flow/BridgeKycWebView.tsx b/app/screens/topup-cashout-flow/BridgeKycWebView.tsx new file mode 100644 index 000000000..03eaff8eb --- /dev/null +++ b/app/screens/topup-cashout-flow/BridgeKycWebView.tsx @@ -0,0 +1,275 @@ +import React, { useState, useRef, useCallback } from "react" +import { View, ActivityIndicator, Platform, TouchableOpacity } from "react-native" +import { StackScreenProps } from "@react-navigation/stack" +import { Text, makeStyles, useTheme } from "@rneui/themed" +import { RootStackParamList } from "@app/navigation/stack-param-lists" +import { WebView, WebViewNavigation } from "react-native-webview" +import Icon from "react-native-vector-icons/Ionicons" +import { Screen } from "@app/components/screen" +import { PrimaryBtn } from "@app/components/buttons" + +type Props = StackScreenProps + +type Step = "tos" | "kyc" + +// Injected JS to detect ToS acceptance. +// The Bridge ToS page is a SPA — after clicking "Accept", the page content +// updates inline without a URL change. We intercept the Accept button click, +// then poll for the page content change that indicates acceptance. +const TOS_INJECTED_JS = `(function() { + var accepted = false; + var initialText = ''; + + function notifyAccepted() { + if (accepted) return; + accepted = true; + window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'tos_accepted' })); + } + + function checkForContentChange() { + if (accepted) return; + var currentText = document.body ? document.body.innerText : ''; + var buttons = document.querySelectorAll('button'); + var hasAcceptButton = false; + buttons.forEach(function(btn) { + if (btn.innerText.trim().toLowerCase() === 'accept') hasAcceptButton = true; + }); + if (!hasAcceptButton && initialText && currentText !== initialText) { + notifyAccepted(); + } + } + + setTimeout(function() { + initialText = document.body ? document.body.innerText : ''; + }, 2000); + + document.addEventListener('click', function(e) { + var target = e.target; + while (target && target !== document.body) { + if (target.tagName === 'BUTTON' || target.tagName === 'A') { + var text = (target.innerText || '').trim().toLowerCase(); + if (text === 'accept' || text === 'i accept' || text === 'agree') { + setTimeout(checkForContentChange, 1500); + setTimeout(checkForContentChange, 3000); + setTimeout(checkForContentChange, 5000); + break; + } + } + target = target.parentElement; + } + }, true); + + if (document.body) { + var clickedAccept = false; + document.addEventListener('click', function() { clickedAccept = true; }, true); + var observer = new MutationObserver(function() { + if (clickedAccept) checkForContentChange(); + }); + observer.observe(document.body, { childList: true, subtree: true }); + } + true; +})();` + +// iOS zoom prevention: force 16px font on inputs, disable text-size-adjust, +// and use MutationObserver for dynamically added inputs. +const KYC_ZOOM_PREVENTION_JS = `(function(){ + document.querySelectorAll('meta[name="viewport"]').forEach(m=>m.remove()); + var meta=document.createElement('meta'); + meta.name='viewport'; + meta.content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'; + document.head.insertBefore(meta,document.head.firstChild); + var style=document.createElement('style'); + style.innerHTML='input,textarea,select{font-size:16px!important}*{-webkit-text-size-adjust:100%!important;touch-action:manipulation!important}'; + document.head.appendChild(style); + var preventZoom=function(e){if(e.target&&(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA')){e.target.style.fontSize='16px';e.target.style.transform='none'}}; + document.addEventListener('focusin',preventZoom,true); + document.querySelectorAll('input,textarea,select').forEach(function(el){el.style.fontSize='16px'}); + var observer=new MutationObserver(function(mutations){mutations.forEach(function(m){m.addedNodes.forEach(function(n){if(n.nodeType===1){var inputs=n.querySelectorAll?n.querySelectorAll('input,textarea,select'):[];inputs.forEach(function(el){el.style.fontSize='16px'})}})})}); + if(document.body){observer.observe(document.body,{childList:true,subtree:true})} + true})();` + +// Viewport meta injection before content loads to prevent initial zoom. +const VIEWPORT_INJECTION_JS = `(function(){const forceViewport=()=>{const content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no';document.querySelectorAll('meta[name="viewport"]').forEach(m=>m.remove());const meta=document.createElement('meta');meta.name='viewport';meta.content=content;if(document.head){document.head.insertBefore(meta,document.head.firstChild)}else{document.documentElement.appendChild(meta)}};forceViewport();document.addEventListener('DOMContentLoaded',forceViewport);window.addEventListener('load',forceViewport);if(document.documentElement){document.documentElement.style.touchAction='pan-x pan-y'}true})();` + +const BridgeKycWebView: React.FC = ({ navigation, route }) => { + const styles = useStyles() + const { colors } = useTheme().theme + const webViewRef = useRef(null) + const [isLoading, setIsLoading] = useState(true) + const [currentStep, setCurrentStep] = useState("tos") + const [error, setError] = useState(false) + const isNavigating = useRef(false) + + const { tosLink, kycLink } = route.params + + const currentUrl = currentStep === "tos" ? tosLink : kycLink + + const safeGoBack = useCallback(() => { + if (isNavigating.current) return + isNavigating.current = true + navigation.goBack() + }, [navigation]) + + const handleNavigationStateChange = useCallback( + (navState: WebViewNavigation) => { + const { url } = navState + + if (currentStep === "kyc") { + if ( + url.includes("complete") || + url.includes("success") || + url.includes("verified") + ) { + safeGoBack() + } + } + }, + [currentStep, safeGoBack], + ) + + const handleMessage = useCallback( + (event: { nativeEvent: { data: string } }) => { + try { + const data = JSON.parse(event.nativeEvent.data) + if (currentStep === "tos" && data.type === "tos_accepted") { + setCurrentStep("kyc") + setIsLoading(true) + } else if (currentStep === "kyc" && data.status === "completed") { + safeGoBack() + } + } catch { + // Not JSON - ignore + } + }, + [currentStep, safeGoBack], + ) + + const handleRetry = () => { + setError(false) + setIsLoading(true) + webViewRef.current?.reload() + } + + const closeButton = ( + + + + + + ) + + if (error) { + return ( + + {closeButton} + + + Failed to load + + + + + ) + } + + return ( + + {closeButton} + + {isLoading && ( + + + + {currentStep === "tos" + ? "Loading Terms of Service..." + : "Loading KYC Verification..."} + + + )} + { + setIsLoading(false) + setError(false) + }} + onError={() => { + setIsLoading(false) + setError(true) + }} + injectedJavaScriptBeforeContentLoaded={VIEWPORT_INJECTION_JS} + injectedJavaScript={ + currentStep === "tos" ? TOS_INJECTED_JS : KYC_ZOOM_PREVENTION_JS + } + style={styles.webView} + javaScriptEnabled + domStorageEnabled + startInLoadingState + scalesPageToFit={false} + bounces={false} + sharedCookiesEnabled + thirdPartyCookiesEnabled + originWhitelist={["https://*", "http://*"]} + userAgent={ + Platform.OS === "ios" + ? "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + : undefined + } + /> + + + ) +} + +const useStyles = makeStyles(({ colors }) => ({ + closeButtonWrapper: { + position: "absolute", + zIndex: 1, + top: 15, + right: 15, + }, + closeButton: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: colors.white, + justifyContent: "center", + alignItems: "center", + shadowColor: colors.black, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.15, + shadowRadius: 4, + elevation: 5, + }, + container: { flex: 1 }, + centerContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: 20, + }, + loadingContainer: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: "center", + alignItems: "center", + backgroundColor: "rgba(255, 255, 255, 0.9)", + zIndex: 1, + }, + loadingText: { marginTop: 16, textAlign: "center" }, + errorText: { textAlign: "center", marginBottom: 24 }, + retryButton: { marginTop: 16 }, + webView: { flex: 1 }, +})) + +export default BridgeKycWebView diff --git a/app/screens/topup-cashout-flow/index.ts b/app/screens/topup-cashout-flow/index.ts index 47ec637b0..8a6dae363 100644 --- a/app/screens/topup-cashout-flow/index.ts +++ b/app/screens/topup-cashout-flow/index.ts @@ -1,3 +1,4 @@ +import BridgeKycWebView from "./BridgeKycWebView" import TopupCashout from "./TopupCashout" import TopupDetails from "./TopupDetails" import BankTransfer from "./BankTransfer" @@ -9,6 +10,7 @@ import CashoutSuccess from "./CashoutSuccess" import PaymentSuccessScreen from "./payment-success-screen" export { + BridgeKycWebView, TopupCashout, TopupDetails, BankTransfer, From 21ed900c4d3d758ec00774643fded8eda91a9842 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Fri, 15 May 2026 18:32:29 +0500 Subject: [PATCH 26/95] CashWalletCutoverModal is implemented to notice users about migration --- .../CashWalletCutoverModal.tsx | 105 ++++++++++++++++++ app/components/topup-cashout-flow/index.ts | 10 +- app/i18n/en/index.ts | 5 + app/i18n/i18n-types.ts | 28 +++++ app/i18n/raw-i18n/source/en.json | 5 + 5 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 app/components/topup-cashout-flow/CashWalletCutoverModal.tsx diff --git a/app/components/topup-cashout-flow/CashWalletCutoverModal.tsx b/app/components/topup-cashout-flow/CashWalletCutoverModal.tsx new file mode 100644 index 000000000..3f1b34d1d --- /dev/null +++ b/app/components/topup-cashout-flow/CashWalletCutoverModal.tsx @@ -0,0 +1,105 @@ +import React, { useState } from "react" +import { useWindowDimensions, View } from "react-native" +import Modal from "react-native-modal" +import { makeStyles, Text, useTheme } from "@rneui/themed" +import { useI18nContext } from "@app/i18n/i18n-react" +import { PrimaryBtn } from "@app/components/buttons" +import DollarIllustration from "@app/assets/illustrations/dollar.svg" + +const CashWalletCutoverModal = () => { + const { LL } = useI18nContext() + const styles = useStyles() + const { width } = useWindowDimensions() + const { colors } = useTheme().theme + + const [visible, setVisible] = useState(false) + + const illustrationSize = Math.min(width * 0.35, 140) + + return ( + setVisible(false)} + onSwipeComplete={() => setVisible(false)} + swipeDirection={["down"]} + style={styles.modal} + useNativeDriverForBackdrop + > + + + + + + + + + + + {LL.CashWalletCutover.title()} + + + {LL.CashWalletCutover.body()} + + + + setVisible(false)} + /> + + + + ) +} + +export default CashWalletCutoverModal + +const useStyles = makeStyles(({ colors }) => ({ + modal: { + justifyContent: "flex-end", + margin: 0, + }, + sheet: { + backgroundColor: colors.white, + borderTopLeftRadius: 24, + borderTopRightRadius: 24, + paddingHorizontal: 24, + paddingBottom: 40, + paddingTop: 12, + alignItems: "center", + }, + handle: { + width: 40, + height: 4, + borderRadius: 2, + backgroundColor: colors.grey4, + marginBottom: 20, + }, + illustrationContainer: { + alignItems: "center", + marginBottom: 20, + }, + illustrationCircle: { + width: 140, + height: 140, + borderRadius: 70, + backgroundColor: colors.grey5, + justifyContent: "center", + alignItems: "center", + }, + title: { + textAlign: "center", + marginBottom: 12, + }, + body: { + textAlign: "center", + paddingHorizontal: 8, + marginBottom: 28, + }, + buttonContainer: { + width: "100%", + }, +})) diff --git a/app/components/topup-cashout-flow/index.ts b/app/components/topup-cashout-flow/index.ts index 8529de435..a6007391c 100644 --- a/app/components/topup-cashout-flow/index.ts +++ b/app/components/topup-cashout-flow/index.ts @@ -2,8 +2,16 @@ import CashoutFromWallet from "./CashoutFromWallet" import CashoutPercentage from "./CashoutPercentage" import CashoutCard from "./CashoutCard" import CashoutWithdrawTo from "./CashoutWithdrawTo" +import CashWalletCutoverModal from "./CashWalletCutoverModal" + +export { + CashoutFromWallet, + CashoutPercentage, + CashoutCard, + CashoutWithdrawTo, + CashWalletCutoverModal, +} -export { CashoutFromWallet, CashoutPercentage, CashoutCard, CashoutWithdrawTo } export { default as TransferOptionModal } from "./TransferOptionModal" export type { TransferOption } from "./TransferOptionModal" export { default as BridgeKycModal } from "./BridgeKycModal" diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index 1cf92ed47..5cb39225f 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -97,6 +97,11 @@ const en: BaseTranslation = { accountType: "Account Type", currency: "Currency", }, + CashWalletCutover: { + title: "Cash Wallet Updated", + body: "Your Cash Wallet has been upgraded from USD to USDT. Your balance has been automatically migrated. No action is needed on your part.", + dismissButton: "Got it", + }, ConversionDetailsScreen: { title: "Swap", percentageToConvert: "% to convert", diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index df0a0b6eb..6273f505c 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -336,6 +336,20 @@ type RootTranslation = { */ currency: string } + CashWalletCutover: { + /** + * C​a​s​h​ ​W​a​l​l​e​t​ ​U​p​d​a​t​e​d + */ + title: string + /** + * Y​o​u​r​ ​C​a​s​h​ ​W​a​l​l​e​t​ ​h​a​s​ ​b​e​e​n​ ​u​p​g​r​a​d​e​d​ ​f​r​o​m​ ​U​S​D​ ​t​o​ ​U​S​D​T​.​ ​Y​o​u​r​ ​b​a​l​a​n​c​e​ ​h​a​s​ ​b​e​e​n​ ​a​u​t​o​m​a​t​i​c​a​l​l​y​ ​m​i​g​r​a​t​e​d​.​ ​N​o​ ​a​c​t​i​o​n​ ​i​s​ ​n​e​e​d​e​d​ ​o​n​ ​y​o​u​r​ ​p​a​r​t​. + */ + body: string + /** + * G​o​t​ ​i​t + */ + dismissButton: string + } ConversionDetailsScreen: { /** * S​w​a​p @@ -5900,6 +5914,20 @@ export type TranslationFunctions = { */ currency: () => LocalizedString } + CashWalletCutover: { + /** + * Cash Wallet Updated + */ + title: () => LocalizedString + /** + * Your Cash Wallet has been upgraded from USD to USDT. Your balance has been automatically migrated. No action is needed on your part. + */ + body: () => LocalizedString + /** + * Got it + */ + dismissButton: () => LocalizedString + } ConversionDetailsScreen: { /** * Swap diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index 8e05a5540..8db33acaf 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -85,6 +85,11 @@ "accountType": "Account Type", "currency": "Currency" }, + "CashWalletCutover": { + "title": "Cash Wallet Updated", + "body": "Your Cash Wallet has been upgraded from USD to USDT. Your balance has been automatically migrated. No action is needed on your part.", + "dismissButton": "Got it" + }, "ConversionDetailsScreen": { "title": "Swap", "percentageToConvert": "% to convert", From 1d149d3218712f0c0a8c2b54da01f4e288d63be0 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Sun, 17 May 2026 11:12:03 +0500 Subject: [PATCH 27/95] integrate bridgeKycStatus query and bridgeInitiateKyc mutation into TopupCashout screen --- .../topup-cashout-flow/TopupCashout.tsx | 72 +++++++++++++++++-- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/app/screens/topup-cashout-flow/TopupCashout.tsx b/app/screens/topup-cashout-flow/TopupCashout.tsx index 3ba19571e..2749f3594 100644 --- a/app/screens/topup-cashout-flow/TopupCashout.tsx +++ b/app/screens/topup-cashout-flow/TopupCashout.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useState } from "react" -import { TouchableOpacity, View } from "react-native" +import { Alert, TouchableOpacity, View } from "react-native" import { RootStackParamList } from "@app/navigation/stack-param-lists" import { Icon, Text, makeStyles, useTheme } from "@rneui/themed" import { StackScreenProps } from "@react-navigation/stack" @@ -14,17 +14,28 @@ import type { TransferOption } from "@app/components/topup-cashout-flow" import ArrowDown from "@app/assets/icons/arrow-down-to-bracket.svg" import ArrowUp from "@app/assets/icons/arrow-up-from-bracket.svg" +// hooks +import { + useBridgeInitiateKycMutation, + useBridgeKycStatusQuery, +} from "@app/graphql/generated" +import { useActivityIndicator } from "@app/hooks" + type Props = StackScreenProps const TopupCashout: React.FC = ({ navigation }) => { const styles = useStyles() const { LL } = useI18nContext() const { colors } = useTheme().theme + const { toggleActivityIndicator } = useActivityIndicator() const [topupModalVisible, setTopupModalVisible] = useState(false) const [settleModalVisible, setSettleModalVisible] = useState(false) const [bridgeKycModalVisible, setBridgeKycModalVisible] = useState(false) + const { data } = useBridgeKycStatusQuery() + const [initiateBridgeKyc] = useBridgeInitiateKycMutation() + const topupOptions: TransferOption[] = useMemo( () => [ { @@ -51,7 +62,7 @@ const TopupCashout: React.FC = ({ navigation }) => { description: LL.TransferScreen.internationalBankTransferDesc(), onPress: () => { setTopupModalVisible(false) - setBridgeKycModalVisible(true) + checkBridgeKyc() }, }, ], @@ -75,13 +86,65 @@ const TopupCashout: React.FC = ({ navigation }) => { description: LL.TransferScreen.internationalBankAccountDesc(), onPress: () => { setSettleModalVisible(false) - setBridgeKycModalVisible(true) + checkBridgeKyc() }, }, ], [LL, navigation], ) + const checkBridgeKyc = () => { + console.log("Bridge KYC Status", data) + if (data?.bridgeKycStatus) { + if (true) { + //pending + //show pending status + } else { + // navigate to next screen + } + } else { + setBridgeKycModalVisible(true) + } + } + + const getBridgeKycLink = async (data: { + fullName: string + email: string + kycType: string + }) => { + toggleActivityIndicator(true) + try { + const res = await initiateBridgeKyc({ + variables: { + input: { + full_name: data.fullName, + email: data.email, + type: data.kycType, + }, + }, + }) + toggleActivityIndicator(false) + console.log("BRIDGE INITIATE KYC RESPONSE: ", res) + + const errors = res.data?.bridgeInitiateKyc?.errors + if (errors && errors.length > 0) { + Alert.alert("Error", errors[0].message) + return + } + + const kycLink = res.data?.bridgeInitiateKyc?.kycLink + if (kycLink?.tosLink && kycLink?.kycLink) { + navigation.navigate("BridgeKycWebView", { + tosLink: kycLink.tosLink, + kycLink: kycLink.kycLink, + }) + } + } catch (err) { + toggleActivityIndicator(false) + Alert.alert("Error", "Something went wrong. Please try again.") + } + } + return ( @@ -128,8 +191,7 @@ const TopupCashout: React.FC = ({ navigation }) => { onClose={() => setBridgeKycModalVisible(false)} onSubmit={(data) => { setBridgeKycModalVisible(false) - // TODO: send KYC data to Bridge API when query is ready - console.log("Bridge KYC submitted:", data) + getBridgeKycLink(data) }} /> From 7f2e84badbcc055cf3b6dcb8f3b74b9956708f4e Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Sun, 17 May 2026 11:13:07 +0500 Subject: [PATCH 28/95] use CashWalletCutoverModal in the home screen --- app/screens/home-screen/home-screen.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/screens/home-screen/home-screen.tsx b/app/screens/home-screen/home-screen.tsx index 110da52fd..0c59c5e11 100644 --- a/app/screens/home-screen/home-screen.tsx +++ b/app/screens/home-screen/home-screen.tsx @@ -6,6 +6,7 @@ import { useTheme } from "@rneui/themed" import WalletOverview from "@app/components/wallet-overview/wallet-overview" import { SetDefaultAccountModal } from "@app/components/set-default-account-modal" import { UnVerifiedSeedModal } from "@app/components/unverified-seed-modal" +import { CashWalletCutoverModal } from "@app/components/topup-cashout-flow" import { Screen } from "@app/components/screen" import { AccountCreateModal, @@ -144,6 +145,7 @@ export const HomeScreen: React.FC = () => { setIsVisible={setIsUnverifiedSeedModalVisible} /> + ) } From d6a446ea76d5836a5d58084cc2e479e319457d53 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Sun, 17 May 2026 13:11:50 +0500 Subject: [PATCH 29/95] update ui to show pending status properly and show alert message to the user about kyc pending --- .../TransferOptionModal.tsx | 10 +++++++- .../topup-cashout-flow/TopupCashout.tsx | 25 +++++++++++-------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/app/components/topup-cashout-flow/TransferOptionModal.tsx b/app/components/topup-cashout-flow/TransferOptionModal.tsx index 6f97f672a..aeb1a2cb9 100644 --- a/app/components/topup-cashout-flow/TransferOptionModal.tsx +++ b/app/components/topup-cashout-flow/TransferOptionModal.tsx @@ -8,6 +8,7 @@ export type TransferOption = { title: string description: string onPress: () => void + pending?: boolean } type Props = { @@ -52,7 +53,14 @@ const TransferOptionModal: React.FC = ({ visible, title, options, onClose {options.map((option, index) => ( - + {option.title} diff --git a/app/screens/topup-cashout-flow/TopupCashout.tsx b/app/screens/topup-cashout-flow/TopupCashout.tsx index 2749f3594..f9ea44e3d 100644 --- a/app/screens/topup-cashout-flow/TopupCashout.tsx +++ b/app/screens/topup-cashout-flow/TopupCashout.tsx @@ -33,9 +33,11 @@ const TopupCashout: React.FC = ({ navigation }) => { const [settleModalVisible, setSettleModalVisible] = useState(false) const [bridgeKycModalVisible, setBridgeKycModalVisible] = useState(false) - const { data } = useBridgeKycStatusQuery() + const { data: kycStatusData } = useBridgeKycStatusQuery() const [initiateBridgeKyc] = useBridgeInitiateKycMutation() + const isBridgeKycPending = kycStatusData?.bridgeKycStatus === "pending" + const topupOptions: TransferOption[] = useMemo( () => [ { @@ -60,13 +62,14 @@ const TopupCashout: React.FC = ({ navigation }) => { icon: "globe", title: LL.TransferScreen.internationalBankTransfer(), description: LL.TransferScreen.internationalBankTransferDesc(), + pending: isBridgeKycPending, onPress: () => { setTopupModalVisible(false) checkBridgeKyc() }, }, ], - [LL, navigation], + [LL, navigation, isBridgeKycPending], ) const settleOptions: TransferOption[] = useMemo( @@ -84,24 +87,24 @@ const TopupCashout: React.FC = ({ navigation }) => { icon: "globe", title: LL.TransferScreen.internationalBankAccount(), description: LL.TransferScreen.internationalBankAccountDesc(), + pending: isBridgeKycPending, onPress: () => { setSettleModalVisible(false) checkBridgeKyc() }, }, ], - [LL, navigation], + [LL, navigation, isBridgeKycPending], ) const checkBridgeKyc = () => { - console.log("Bridge KYC Status", data) - if (data?.bridgeKycStatus) { - if (true) { - //pending - //show pending status - } else { - // navigate to next screen - } + if (isBridgeKycPending) { + Alert.alert("KYC Pending", "Your KYC status is pending. Please wait for approval.") + return + } + + if (kycStatusData?.bridgeKycStatus) { + // KYC already completed - navigate to next screen } else { setBridgeKycModalVisible(true) } From 55b6bea30473181a9d9b97e9a0c33878382b6e00 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Mon, 18 May 2026 16:58:39 +0500 Subject: [PATCH 30/95] fix bridgeKycStatus response showing null and implement refetchKycStatus when user finish kyc process --- .../topup-cashout-flow/TopupCashout.tsx | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/app/screens/topup-cashout-flow/TopupCashout.tsx b/app/screens/topup-cashout-flow/TopupCashout.tsx index f9ea44e3d..7fb05fbdd 100644 --- a/app/screens/topup-cashout-flow/TopupCashout.tsx +++ b/app/screens/topup-cashout-flow/TopupCashout.tsx @@ -1,9 +1,10 @@ -import React, { useMemo, useState } from "react" +import React, { useCallback, useMemo, useState } from "react" import { Alert, TouchableOpacity, View } from "react-native" import { RootStackParamList } from "@app/navigation/stack-param-lists" import { Icon, Text, makeStyles, useTheme } from "@rneui/themed" import { StackScreenProps } from "@react-navigation/stack" import { useI18nContext } from "@app/i18n/i18n-react" +import { useFocusEffect } from "@react-navigation/native" // components import { Screen } from "@app/components/screen" @@ -33,10 +34,16 @@ const TopupCashout: React.FC = ({ navigation }) => { const [settleModalVisible, setSettleModalVisible] = useState(false) const [bridgeKycModalVisible, setBridgeKycModalVisible] = useState(false) - const { data: kycStatusData } = useBridgeKycStatusQuery() const [initiateBridgeKyc] = useBridgeInitiateKycMutation() - - const isBridgeKycPending = kycStatusData?.bridgeKycStatus === "pending" + const { data: kycStatusData, refetch: refetchKycStatus } = useBridgeKycStatusQuery({ + fetchPolicy: "cache-and-network", + }) + + useFocusEffect( + useCallback(() => { + refetchKycStatus() + }, [refetchKycStatus]), + ) const topupOptions: TransferOption[] = useMemo( () => [ @@ -62,14 +69,14 @@ const TopupCashout: React.FC = ({ navigation }) => { icon: "globe", title: LL.TransferScreen.internationalBankTransfer(), description: LL.TransferScreen.internationalBankTransferDesc(), - pending: isBridgeKycPending, + pending: kycStatusData?.bridgeKycStatus === "pending", onPress: () => { setTopupModalVisible(false) checkBridgeKyc() }, }, ], - [LL, navigation, isBridgeKycPending], + [LL, navigation, kycStatusData?.bridgeKycStatus], ) const settleOptions: TransferOption[] = useMemo( @@ -87,23 +94,21 @@ const TopupCashout: React.FC = ({ navigation }) => { icon: "globe", title: LL.TransferScreen.internationalBankAccount(), description: LL.TransferScreen.internationalBankAccountDesc(), - pending: isBridgeKycPending, + pending: kycStatusData?.bridgeKycStatus === "pending", onPress: () => { setSettleModalVisible(false) checkBridgeKyc() }, }, ], - [LL, navigation, isBridgeKycPending], + [LL, navigation, kycStatusData?.bridgeKycStatus], ) const checkBridgeKyc = () => { - if (isBridgeKycPending) { + console.log(">>>>>>???????", kycStatusData?.bridgeKycStatus) + if (kycStatusData?.bridgeKycStatus === "pending") { Alert.alert("KYC Pending", "Your KYC status is pending. Please wait for approval.") - return - } - - if (kycStatusData?.bridgeKycStatus) { + } else if (kycStatusData?.bridgeKycStatus === "approved") { // KYC already completed - navigate to next screen } else { setBridgeKycModalVisible(true) From 7e2e94d44605c70387aa85d3939fe03a9ed85bce Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Mon, 18 May 2026 16:59:37 +0500 Subject: [PATCH 31/95] implement pull to refresh to fetch updated kyc status on the TopupCashout screen --- .../topup-cashout-flow/TopupCashout.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/app/screens/topup-cashout-flow/TopupCashout.tsx b/app/screens/topup-cashout-flow/TopupCashout.tsx index 7fb05fbdd..3899d5a5f 100644 --- a/app/screens/topup-cashout-flow/TopupCashout.tsx +++ b/app/screens/topup-cashout-flow/TopupCashout.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useMemo, useState } from "react" -import { Alert, TouchableOpacity, View } from "react-native" +import { Alert, RefreshControl, ScrollView, TouchableOpacity, View } from "react-native" import { RootStackParamList } from "@app/navigation/stack-param-lists" import { Icon, Text, makeStyles, useTheme } from "@rneui/themed" import { StackScreenProps } from "@react-navigation/stack" @@ -33,6 +33,7 @@ const TopupCashout: React.FC = ({ navigation }) => { const [topupModalVisible, setTopupModalVisible] = useState(false) const [settleModalVisible, setSettleModalVisible] = useState(false) const [bridgeKycModalVisible, setBridgeKycModalVisible] = useState(false) + const [refreshing, setRefreshing] = useState(false) const [initiateBridgeKyc] = useBridgeInitiateKycMutation() const { data: kycStatusData, refetch: refetchKycStatus } = useBridgeKycStatusQuery({ @@ -45,6 +46,12 @@ const TopupCashout: React.FC = ({ navigation }) => { }, [refetchKycStatus]), ) + const onRefresh = useCallback(async () => { + setRefreshing(true) + await refetchKycStatus() + setRefreshing(false) + }, [refetchKycStatus]) + const topupOptions: TransferOption[] = useMemo( () => [ { @@ -155,7 +162,12 @@ const TopupCashout: React.FC = ({ navigation }) => { return ( - + + } + > {LL.TransferScreen.title()} @@ -179,7 +191,7 @@ const TopupCashout: React.FC = ({ navigation }) => { - + = ({ navigation }) => { const useStyles = makeStyles(() => ({ container: { - flex: 1, + flexGrow: 1, paddingHorizontal: 20, }, title: { From 4876e142aea8a0bd365e65ba92c9b135fae9f308 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Wed, 20 May 2026 16:33:42 +0500 Subject: [PATCH 32/95] open external browser when user press term of service or privacy poicy on the tos webview screen --- .../topup-cashout-flow/BridgeKycWebView.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/screens/topup-cashout-flow/BridgeKycWebView.tsx b/app/screens/topup-cashout-flow/BridgeKycWebView.tsx index 03eaff8eb..4f2013a38 100644 --- a/app/screens/topup-cashout-flow/BridgeKycWebView.tsx +++ b/app/screens/topup-cashout-flow/BridgeKycWebView.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef, useCallback } from "react" -import { View, ActivityIndicator, Platform, TouchableOpacity } from "react-native" +import { View, ActivityIndicator, Linking, Platform, TouchableOpacity } from "react-native" import { StackScreenProps } from "@react-navigation/stack" import { Text, makeStyles, useTheme } from "@rneui/themed" import { RootStackParamList } from "@app/navigation/stack-param-lists" @@ -216,6 +216,21 @@ const BridgeKycWebView: React.FC = ({ navigation, route }) => { bounces={false} sharedCookiesEnabled thirdPartyCookiesEnabled + onShouldStartLoadWithRequest={(request) => { + // During ToS step, open terms/privacy links in external browser + if (currentStep === "tos" && request.url !== tosLink) { + const url = request.url.toLowerCase() + if ( + url.includes("terms") || + url.includes("privacy") || + url.includes("policy") + ) { + Linking.openURL(request.url) + return false + } + } + return true + }} originWhitelist={["https://*", "http://*"]} userAgent={ Platform.OS === "ios" From 9153a8605dcfb121d6ffe10905e8bbe4c76537f8 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Thu, 21 May 2026 18:29:54 +0500 Subject: [PATCH 33/95] update BankTransfer screen to show bridge virtual account details as well --- .../topup-cashout-flow/BankTransfer.tsx | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/app/screens/topup-cashout-flow/BankTransfer.tsx b/app/screens/topup-cashout-flow/BankTransfer.tsx index c5e4d96d9..ece91e7da 100644 --- a/app/screens/topup-cashout-flow/BankTransfer.tsx +++ b/app/screens/topup-cashout-flow/BankTransfer.tsx @@ -12,6 +12,9 @@ import { useSafeAreaInsets } from "react-native-safe-area-context" import { Screen } from "@app/components/screen" import { PrimaryBtn } from "@app/components/buttons" +// gql +import { useBridgeVirtualAccountQuery } from "@app/graphql/generated" + type Props = StackScreenProps const BankTransfer: React.FC = ({ navigation, route }) => { @@ -19,9 +22,42 @@ const BankTransfer: React.FC = ({ navigation, route }) => { const { bottom } = useSafeAreaInsets() const { LL } = useI18nContext() - const { email, amount, wallet } = route.params + const { data } = useBridgeVirtualAccountQuery() + + const { amount, wallet, paymentType } = route.params const fee = amount * 0.02 + if (paymentType === "bridge") { + return ( + + + {LL.BankTransfer.virtualBankTransfer()} + + + {LL.BankTransfer.desc1({ amount: amount + fee })} + + + {LL.BankTransfer.bankName()} + + {data?.bridgeVirtualAccount?.bankName} + + + + {LL.BankTransfer.accountNumber()} + + {data?.bridgeVirtualAccount?.accountNumber} + + + + {LL.BankTransfer.routingNumber()} + + {data?.bridgeVirtualAccount?.routingNumber} + + + + ) + } + return ( From 578687904111a70913ba7a48d0c0fece688d62f5 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Thu, 21 May 2026 18:30:49 +0500 Subject: [PATCH 34/95] add texts for bridge integration --- app/i18n/en/index.ts | 5 ++++- app/i18n/i18n-types.ts | 25 +++++++++++++++++++++++++ app/i18n/raw-i18n/source/en.json | 5 ++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index 5cb39225f..feb0f03db 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -642,7 +642,10 @@ const en: BaseTranslation = { fees: "Fees", desc4: "After payment completion on your end you can send us an email to {email: string} with a screenshot of your payment confirmation.", desc5: "Your payment will be processed even if we don't receive this email, but having this confirmation can help accelerate the order.", - backHome: "Back to Home" + backHome: "Back to Home", + virtualBankTransfer: "Virtual Bank Transfer", + bankName: "Bank Name", + routingNumber: "Routing Number" }, PaymentSuccessScreen: { title: "Payment Successful", diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index 6273f505c..a49084794 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -2062,6 +2062,18 @@ type RootTranslation = { * B​a​c​k​ ​t​o​ ​H​o​m​e */ backHome: string + /** + * Virtual Bank Transfer + */ + virtualBankTransfer: string + /** + * Bank Name + */ + bankName: string + /** + * Routing Number + */ + routingNumber: string } PaymentSuccessScreen: { /** @@ -7631,6 +7643,19 @@ export type TranslationFunctions = { * Back to Home */ backHome: () => LocalizedString + /** + * Virtual Bank Transfer + */ + virtualBankTransfer: () => LocalizedString + /** + * Bank Name + */ + bankName: () => LocalizedString + /** + * Routing Number + */ + routingNumber: () => LocalizedString + } PaymentSuccessScreen: { /** diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index 8db33acaf..5687dd6b0 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -611,7 +611,10 @@ "fees": "Fees", "desc4": "After payment completion on your end you can send us an email to {email: string} with a screenshot of your payment confirmation.", "desc5": "Your payment will be processed even if we don't receive this email, but having this confirmation can help accelerate the order.", - "backHome": "Back to Home" + "backHome": "Back to Home", + "virtualBankTransfer": "Virtual Bank Transfer", + "bankName": "Bank Name", + "routingNumber": "Routing Number" }, "PaymentSuccessScreen": { "title": "Payment Successful", From 5cbfe105d3f18ce5df0d8935d63a1b459e18b39c Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Thu, 21 May 2026 18:34:25 +0500 Subject: [PATCH 35/95] update TopupCashout and TopupDetails screens to integrate bridge flow --- app/navigation/stack-param-lists.ts | 3 ++- .../topup-cashout-flow/TopupCashout.tsx | 15 +++++++------- .../topup-cashout-flow/TopupDetails.tsx | 20 +++++++++++++++---- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/app/navigation/stack-param-lists.ts b/app/navigation/stack-param-lists.ts index f72df14ef..ec8b643ca 100644 --- a/app/navigation/stack-param-lists.ts +++ b/app/navigation/stack-param-lists.ts @@ -168,10 +168,11 @@ export type RootStackParamList = { kycLink: string } TopupCashout: undefined - TopupDetails: { paymentType: "card" | "bankTransfer" } + TopupDetails: { paymentType: "card" | "bankTransfer" | "bridge" } BankTransfer: { amount: number wallet: string + paymentType: "bankTransfer" | "bridge" } CardPayment: { amount: number diff --git a/app/screens/topup-cashout-flow/TopupCashout.tsx b/app/screens/topup-cashout-flow/TopupCashout.tsx index 3899d5a5f..1cbf77c73 100644 --- a/app/screens/topup-cashout-flow/TopupCashout.tsx +++ b/app/screens/topup-cashout-flow/TopupCashout.tsx @@ -79,7 +79,7 @@ const TopupCashout: React.FC = ({ navigation }) => { pending: kycStatusData?.bridgeKycStatus === "pending", onPress: () => { setTopupModalVisible(false) - checkBridgeKyc() + checkBridgeKyc("topup") }, }, ], @@ -104,19 +104,20 @@ const TopupCashout: React.FC = ({ navigation }) => { pending: kycStatusData?.bridgeKycStatus === "pending", onPress: () => { setSettleModalVisible(false) - checkBridgeKyc() + checkBridgeKyc("settle") }, }, ], [LL, navigation, kycStatusData?.bridgeKycStatus], ) - const checkBridgeKyc = () => { - console.log(">>>>>>???????", kycStatusData?.bridgeKycStatus) + const checkBridgeKyc = (type: "topup" | "settle") => { if (kycStatusData?.bridgeKycStatus === "pending") { Alert.alert("KYC Pending", "Your KYC status is pending. Please wait for approval.") } else if (kycStatusData?.bridgeKycStatus === "approved") { - // KYC already completed - navigate to next screen + type === "topup" + ? navigation.navigate("TopupDetails", { paymentType: "bridge" }) + : navigation.navigate("CashoutDetails") } else { setBridgeKycModalVisible(true) } @@ -164,9 +165,7 @@ const TopupCashout: React.FC = ({ navigation }) => { - } + refreshControl={} > {LL.TransferScreen.title()} diff --git a/app/screens/topup-cashout-flow/TopupDetails.tsx b/app/screens/topup-cashout-flow/TopupDetails.tsx index 15172010b..827a4c0b9 100644 --- a/app/screens/topup-cashout-flow/TopupDetails.tsx +++ b/app/screens/topup-cashout-flow/TopupDetails.tsx @@ -27,6 +27,7 @@ import { ButtonGroup } from "@app/components/button-group" // hooks import { useI18nContext } from "@app/i18n/i18n-react" import { useSafeAreaInsets } from "react-native-safe-area-context" +import { usePersistentStateContext } from "@app/store/persistent-state" // assets import Cash from "@app/assets/icons/cash.svg" @@ -40,6 +41,8 @@ const TopupDetails: React.FC = ({ navigation, route }) => { const { bottom } = useSafeAreaInsets() const styles = useStyles()({ bottom }) + const { persistentState } = usePersistentStateContext() + /** * Component state: * - selectedWallet: Which wallet to credit (USD or BTC) @@ -83,10 +86,14 @@ const TopupDetails: React.FC = ({ navigation, route }) => { setIsLoading(true) try { - if (route.params.paymentType === "bankTransfer") { + if ( + route.params.paymentType === "bankTransfer" || + route.params.paymentType === "bridge" + ) { navigation.navigate("BankTransfer", { amount: parseFloat(amount), wallet: selectedWallet, + paymentType: route.params.paymentType, }) } else { // Card payment flow via Fygaro WebView @@ -121,15 +128,18 @@ const TopupDetails: React.FC = ({ navigation, route }) => { normal: , }, }, - { + ] + + if (persistentState.isAdvanceMode) { + walletButtons.push({ id: "BTC", text: LL.TopupDetails.btcWallet(), icon: { selected: , normal: , }, - }, - ] + }) + } return ( @@ -137,6 +147,8 @@ const TopupDetails: React.FC = ({ navigation, route }) => { {route.params.paymentType === "card" ? LL.TopupDetails.title() + : route.params.paymentType === "bridge" + ? LL.BankTransfer.virtualBankTransfer() : LL.TopupDetails.bankTransfer()} From 4cadac04c61f21861596516cec10679f18533efd Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Thu, 21 May 2026 18:35:32 +0500 Subject: [PATCH 36/95] update button-group component styling --- app/components/button-group/button-group.tsx | 58 ++++++++++++-------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/app/components/button-group/button-group.tsx b/app/components/button-group/button-group.tsx index ee940524a..ae52fa8c2 100644 --- a/app/components/button-group/button-group.tsx +++ b/app/components/button-group/button-group.tsx @@ -1,13 +1,13 @@ import React from "react" -import { StyleProp, TouchableWithoutFeedback, View, ViewStyle } from "react-native" -import { Text, makeStyles } from "@rneui/themed" +import { StyleProp, TouchableOpacity, View, ViewStyle } from "react-native" +import { Text, makeStyles, useTheme } from "@rneui/themed" import Icon from "react-native-vector-icons/Ionicons" import { testProps } from "@app/utils/testProps" type ButtonForButtonGroupProps = { id: string text: string - icon: + icon?: | string | { selected: React.ReactElement @@ -21,22 +21,37 @@ const ButtonForButtonGroup: React.FC< onPress: () => void } > = ({ text, icon, selected, onPress }) => { + const { colors } = useTheme().theme const styles = useStyles(Boolean(selected)) return ( - - - - {text} - - {typeof icon === "string" ? ( - + + {icon && + (typeof icon === "string" ? ( + ) : selected ? ( icon.selected ) : ( icon.normal - )} - - + ))} + + {text} + + ) } @@ -85,20 +100,19 @@ const useStyles = makeStyles(({ colors }, selected: boolean) => ({ flex: 1, flexDirection: "row", alignItems: "center", - justifyContent: "space-between", - padding: 10, - paddingVertical: 15, - marginHorizontal: 3, - borderRadius: 5, - backgroundColor: selected ? colors.grey4 : colors.grey5, + padding: 15, + borderRadius: 10, + borderWidth: 1, + borderColor: colors.grey4, + backgroundColor: colors.grey5, }, - text: { + iconText: { fontSize: 16, color: selected ? colors.primary : colors.grey1, + marginRight: 8, }, buttonGroup: { flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", + gap: 10, }, })) From ea3f3af730aac40fcd9bf6fac2a816983392a1e1 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Thu, 21 May 2026 18:36:47 +0500 Subject: [PATCH 37/95] replace DropDownField component with ButtonGroup for kycType section --- .../topup-cashout-flow/BridgeKycModal.tsx | 71 ++++++++++++++----- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/app/components/topup-cashout-flow/BridgeKycModal.tsx b/app/components/topup-cashout-flow/BridgeKycModal.tsx index ee3cf2bed..d73068fc1 100644 --- a/app/components/topup-cashout-flow/BridgeKycModal.tsx +++ b/app/components/topup-cashout-flow/BridgeKycModal.tsx @@ -1,11 +1,19 @@ import React, { useState } from "react" -import { KeyboardAvoidingView, Modal, Platform, ScrollView, TouchableOpacity, View } from "react-native" +import { + KeyboardAvoidingView, + Modal, + Platform, + ScrollView, + TouchableOpacity, + View, +} from "react-native" import { Icon, Text, makeStyles, useTheme } from "@rneui/themed" import { useSafeAreaInsets } from "react-native-safe-area-context" import { useI18nContext } from "@app/i18n/i18n-react" -import { InputField, DropDownField } from "@app/components/account-upgrade-flow" +import { InputField } from "@app/components/account-upgrade-flow" import { PrimaryBtn } from "@app/components/buttons" +import { ButtonGroup } from "@app/components/button-group" type BridgeKycModalProps = { visible: boolean @@ -13,7 +21,11 @@ type BridgeKycModalProps = { onSubmit: (data: { fullName: string; email: string; kycType: string }) => void } -const BridgeKycModal: React.FC = ({ visible, onClose, onSubmit }) => { +const BridgeKycModal: React.FC = ({ + visible, + onClose, + onSubmit, +}) => { const styles = useStyles() const { colors, mode } = useTheme().theme const bottom = useSafeAreaInsets().bottom @@ -27,9 +39,9 @@ const BridgeKycModal: React.FC = ({ visible, onClose, onSub const [emailErr, setEmailErr] = useState() const [kycTypeErr, setKycTypeErr] = useState() - const kycTypeOptions = [ - { label: LL.BridgeKyc.individual(), value: "individual" }, - { label: LL.BridgeKyc.business(), value: "business" }, + const kycTypeButtons = [ + { id: "individual", text: LL.BridgeKyc.individual() }, + { id: "business", text: LL.BridgeKyc.business() }, ] const validate = (): boolean => { @@ -72,7 +84,12 @@ const BridgeKycModal: React.FC = ({ visible, onClose, onSub } return ( - + = ({ visible, onClose, onSub > = ({ visible, onClose, onSub }} /> - { - setKycTypeErr(undefined) - setKycType(val) - }} - /> + + + {LL.BridgeKyc.kycType()} + + { + setKycTypeErr(undefined) + setKycType(id) + }} + style={styles.kycTypeRow} + /> + {!!kycTypeErr && ( + + {kycTypeErr} + + )} + = ({ visible, onClose, onSub export default BridgeKycModal -const useStyles = makeStyles(({ colors }) => ({ +const useStyles = makeStyles(() => ({ backdrop: { flex: 1, justifyContent: "flex-end", @@ -187,4 +213,11 @@ const useStyles = makeStyles(({ colors }) => ({ submitBtn: { marginTop: 10, }, + kycTypeWrapper: { + marginBottom: 15, + }, + kycTypeRow: { + marginTop: 5, + marginBottom: 2, + }, })) From 81276f3e2d4e9ce59e9547e786c4eb6ef04a9f3f Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Thu, 21 May 2026 18:38:15 +0500 Subject: [PATCH 38/95] bridge related mutations and queries are added --- app/graphql/front-end-mutations.ts | 15 ++++++++++++++- app/graphql/front-end-queries.ts | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/graphql/front-end-mutations.ts b/app/graphql/front-end-mutations.ts index ca7054a85..f9ef679de 100644 --- a/app/graphql/front-end-mutations.ts +++ b/app/graphql/front-end-mutations.ts @@ -142,7 +142,7 @@ gql` code message } - journalId + id } } @@ -210,4 +210,17 @@ gql` uploadUrl } } + + mutation BridgeInitiateKyc($input: BridgeInitiateKycInput!) { + bridgeInitiateKyc(input: $input) { + errors { + code + message + } + kycLink { + kycLink + tosLink + } + } + } ` diff --git a/app/graphql/front-end-queries.ts b/app/graphql/front-end-queries.ts index d0715234c..b670de713 100644 --- a/app/graphql/front-end-queries.ts +++ b/app/graphql/front-end-queries.ts @@ -292,11 +292,14 @@ gql` title } bankAccount { + accountName accountNumber accountType bankBranch bankName currency + id + isDefault } currentLevel fullName @@ -315,4 +318,22 @@ gql` name } } + + query BridgeKycStatus { + bridgeKycStatus + } + + query BridgeVirtualAccount { + bridgeVirtualAccount { + accountNumber + accountNumberLast4 + bankName + id + kycLink + message + pending + routingNumber + tosLink + } + } ` From 8cd952993b4fec3481dee550cd5d80c0a0ec800a Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Thu, 21 May 2026 20:24:58 +0500 Subject: [PATCH 39/95] update codegen.ynl --- codegen.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/codegen.yml b/codegen.yml index 9574c39e8..6f26055d7 100644 --- a/codegen.yml +++ b/codegen.yml @@ -1,6 +1,6 @@ overwrite: true -schema: "https://api.flashapp.me/graphql" -# schema: "http://localhost:4002/graphql" +# schema: "https://api.flashapp.me/graphql" +schema: "http://localhost:4002/graphql" # schema: "https://raw.githubusercontent.com/lnflash/flash/feature/merchant-map-suggest/src/graphql/public/schema.graphql" documents: - "app/**/*.ts" @@ -28,6 +28,7 @@ generates: nonOptionalTypename: true scalars: AccountApiKeyLabel: "string" + AccountNumber: "string" AuthToken: "string" CentAmount: "number" ContactAlias: "string" From d155dc292c1d97ac647d2b73faad39d5f3748d44 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Fri, 22 May 2026 16:40:28 +0500 Subject: [PATCH 40/95] fix tos accept button routing to external browser issue --- app/graphql/generated.gql | 44 ++- app/graphql/generated.ts | 363 ++++++++++++++++-- .../topup-cashout-flow/BridgeKycWebView.tsx | 14 +- 3 files changed, 375 insertions(+), 46 deletions(-) diff --git a/app/graphql/generated.gql b/app/graphql/generated.gql index 18fce6782..b2bc45471 100644 --- a/app/graphql/generated.gql +++ b/app/graphql/generated.gql @@ -81,6 +81,22 @@ fragment TransactionList on TransactionConnection { __typename } +mutation BridgeInitiateKyc($input: BridgeInitiateKycInput!) { + bridgeInitiateKyc(input: $input) { + errors { + code + message + __typename + } + kycLink { + kycLink + tosLink + __typename + } + __typename + } +} + mutation IdDocumentUploadUrlGenerate($input: IdDocumentUploadUrlGenerateInput!) { idDocumentUploadUrlGenerate(input: $input) { errors { @@ -101,7 +117,7 @@ mutation InitiateCashout($input: InitiateCashoutInput!) { message __typename } - journalId + id __typename } } @@ -862,6 +878,25 @@ mutation userUpdateUsername($input: UserUpdateUsernameInput!) { } } +query BridgeKycStatus { + bridgeKycStatus +} + +query BridgeVirtualAccount { + bridgeVirtualAccount { + accountNumber + accountNumberLast4 + bankName + id + kycLink + message + pending + routingNumber + tosLink + __typename + } +} + query ExportCsvSetting($walletIds: [WalletId!]!) { me { id @@ -893,11 +928,14 @@ query LatestAccountUpgradeRequest { __typename } bankAccount { + accountName accountNumber accountType - bankBranch - bankName + bank + branchCode currency + id + isDefault __typename } currentLevel diff --git a/app/graphql/generated.ts b/app/graphql/generated.ts index f7475c95e..c282cec03 100644 --- a/app/graphql/generated.ts +++ b/app/graphql/generated.ts @@ -17,6 +17,8 @@ export type Scalars = { Boolean: { input: boolean; output: boolean; } Int: { input: number; output: number; } Float: { input: number; output: number; } + /** Bank account number. Accepts String or Int and coerces to String. */ + AccountNumber: { input: string; output: string; } /** An Opaque Bearer token */ AuthToken: { input: string; output: string; } /** (Positive) Cent amount (1/100 of a dollar) */ @@ -262,8 +264,9 @@ export type BtcWallet = Wallet & { readonly __typename: 'BTCWallet'; readonly accountId: Scalars['ID']['output']; /** A balance stored in BTC. */ - readonly balance: Scalars['FractionalCentAmount']['output']; + readonly balance?: Maybe; readonly id: Scalars['ID']['output']; + readonly isExternal: Scalars['Boolean']['output']; readonly lnurlp?: Maybe; /** An unconfirmed incoming onchain balance. */ readonly pendingIncomingBalance: Scalars['SignedAmount']['output']; @@ -299,21 +302,98 @@ export type Bank = { export type BankAccount = { readonly __typename: 'BankAccount'; - readonly accountNumber: Scalars['Int']['output']; + readonly accountName: Scalars['String']['output']; + readonly accountNumber: Scalars['String']['output']; readonly accountType: Scalars['String']['output']; - readonly bankBranch: Scalars['String']['output']; - readonly bankName: Scalars['String']['output']; + /** Name of the bank institution */ + readonly bank: Scalars['String']['output']; + readonly branchCode: Scalars['String']['output']; + /** Account currency (e.g. JMD, USD) */ readonly currency: Scalars['String']['output']; + /** ERPNext bank account identifier */ + readonly id: Scalars['ID']['output']; + readonly isDefault: Scalars['Boolean']['output']; }; export type BankAccountInput = { - readonly accountNumber: Scalars['Int']['input']; + readonly accountNumber: Scalars['AccountNumber']['input']; readonly accountType: Scalars['String']['input']; readonly bankBranch: Scalars['String']['input']; readonly bankName: Scalars['String']['input']; readonly currency: Scalars['String']['input']; }; +export type BridgeAddExternalAccountPayload = { + readonly __typename: 'BridgeAddExternalAccountPayload'; + readonly errors: ReadonlyArray; + readonly externalAccount?: Maybe; +}; + +export type BridgeCreateVirtualAccountPayload = { + readonly __typename: 'BridgeCreateVirtualAccountPayload'; + readonly errors: ReadonlyArray; + readonly virtualAccount?: Maybe; +}; + +export type BridgeExternalAccount = { + readonly __typename: 'BridgeExternalAccount'; + readonly accountNumberLast4: Scalars['String']['output']; + readonly bankName: Scalars['String']['output']; + readonly id: Scalars['ID']['output']; + readonly status: Scalars['String']['output']; +}; + +export type BridgeInitiateKycInput = { + readonly email?: InputMaybe; + readonly full_name?: InputMaybe; + readonly type?: InputMaybe; +}; + +export type BridgeInitiateKycPayload = { + readonly __typename: 'BridgeInitiateKycPayload'; + readonly errors: ReadonlyArray; + readonly kycLink?: Maybe; +}; + +export type BridgeInitiateWithdrawalInput = { + readonly amount: Scalars['String']['input']; + readonly externalAccountId: Scalars['ID']['input']; +}; + +export type BridgeInitiateWithdrawalPayload = { + readonly __typename: 'BridgeInitiateWithdrawalPayload'; + readonly errors: ReadonlyArray; + readonly withdrawal?: Maybe; +}; + +export type BridgeKycLink = { + readonly __typename: 'BridgeKycLink'; + readonly kycLink: Scalars['String']['output']; + readonly tosLink: Scalars['String']['output']; +}; + +export type BridgeVirtualAccount = { + readonly __typename: 'BridgeVirtualAccount'; + readonly accountNumber?: Maybe; + readonly accountNumberLast4?: Maybe; + readonly bankName?: Maybe; + readonly id?: Maybe; + readonly kycLink?: Maybe; + readonly message?: Maybe; + readonly pending?: Maybe; + readonly routingNumber?: Maybe; + readonly tosLink?: Maybe; +}; + +export type BridgeWithdrawal = { + readonly __typename: 'BridgeWithdrawal'; + readonly amount: Scalars['String']['output']; + readonly createdAt: Scalars['String']['output']; + readonly currency: Scalars['String']['output']; + readonly id: Scalars['ID']['output']; + readonly status: Scalars['String']['output']; +}; + export type BuildInformation = { readonly __typename: 'BuildInformation'; readonly commitHash?: Maybe; @@ -375,17 +455,17 @@ export type CaptchaRequestAuthCodeInput = { export type CashoutOffer = { readonly __typename: 'CashoutOffer'; /** The rate used when withdrawing to a JMD bank account */ - readonly exchangeRate: Scalars['JMDCents']['output']; + readonly exchangeRate?: Maybe; /** The time at which this offer is no longer accepted by Flash */ readonly expiresAt: Scalars['Timestamp']['output']; - /** The amount that Flash is charging for it's services */ + /** The amount that Flash is charging for its services */ readonly flashFee: Scalars['USDCents']['output']; /** ID of the offer */ readonly offerId: Scalars['ID']['output']; - /** The amount Flash owes to the user denominated in JMD as cents */ - readonly receiveJmd: Scalars['JMDCents']['output']; - /** The amount Flash owes to the user denominated in USD as cents */ - readonly receiveUsd: Scalars['USDCents']['output']; + /** The amount Flash owes to the user denominated in JMD cents (null for USD payouts) */ + readonly receiveJmd?: Maybe; + /** The amount Flash owes to the user denominated in USD cents (null for JMD payouts) */ + readonly receiveUsd?: Maybe; /** The amount the user is sending to flash */ readonly send: Scalars['USDCents']['output']; /** ID for the users USD wallet to send from */ @@ -552,7 +632,7 @@ export type InitiateCashoutInput = { export type InitiatedCashoutResponse = { readonly __typename: 'InitiatedCashoutResponse'; readonly errors: ReadonlyArray; - readonly journalId?: Maybe; + readonly id?: Maybe; }; export type InitiationVia = InitiationViaIntraLedger | InitiationViaLn | InitiationViaOnChain; @@ -835,6 +915,10 @@ export type Mutation = { readonly accountEnableNotificationChannel: AccountUpdateNotificationSettingsPayload; readonly accountUpdateDefaultWalletId: AccountUpdateDefaultWalletIdPayload; readonly accountUpdateDisplayCurrency: AccountUpdateDisplayCurrencyPayload; + readonly bridgeAddExternalAccount: BridgeAddExternalAccountPayload; + readonly bridgeCreateVirtualAccount: BridgeCreateVirtualAccountPayload; + readonly bridgeInitiateKyc: BridgeInitiateKycPayload; + readonly bridgeInitiateWithdrawal: BridgeInitiateWithdrawalPayload; readonly businessAccountUpgradeRequest: AccountUpgradePayload; readonly callbackEndpointAdd: CallbackEndpointAddPayload; readonly callbackEndpointDelete: SuccessPayload; @@ -935,6 +1019,7 @@ export type Mutation = { * The user can review this offer and then execute the withdrawal by calling the initiateCashout mutation. */ readonly requestCashout: RequestCashoutResponse; + readonly updateExternalWallet: UpdateExternalWalletPayload; /** @deprecated will be moved to AccountContact */ readonly userContactUpdateAlias: UserContactUpdateAliasPayload; readonly userEmailDelete: UserEmailDeletePayload; @@ -988,6 +1073,16 @@ export type MutationAccountUpdateDisplayCurrencyArgs = { }; +export type MutationBridgeInitiateKycArgs = { + input: BridgeInitiateKycInput; +}; + + +export type MutationBridgeInitiateWithdrawalArgs = { + input: BridgeInitiateWithdrawalInput; +}; + + export type MutationBusinessAccountUpgradeRequestArgs = { input: BusinessAccountUpgradeRequestInput; }; @@ -1148,6 +1243,11 @@ export type MutationRequestCashoutArgs = { }; +export type MutationUpdateExternalWalletArgs = { + input: UpdateExternalWalletInput; +}; + + export type MutationUserContactUpdateAliasArgs = { input: UserContactUpdateAliasInput; }; @@ -1453,6 +1553,10 @@ export type Query = { readonly __typename: 'Query'; readonly accountDefaultWallet: PublicWallet; readonly beta: Scalars['Boolean']['output']; + readonly bridgeExternalAccounts?: Maybe>>; + readonly bridgeKycStatus?: Maybe; + readonly bridgeVirtualAccount?: Maybe; + readonly bridgeWithdrawals?: Maybe>>; /** @deprecated Deprecated in favor of realtimePrice */ readonly btcPrice?: Maybe; readonly btcPriceList?: Maybe>>; @@ -1608,6 +1712,8 @@ export type RealtimePricePayload = { export type RequestCashoutInput = { /** Amount in USD cents. */ readonly amount: Scalars['USDCents']['input']; + /** ERPNext bank account identifier to receive the cashout. */ + readonly bankAccountId: Scalars['ID']['input']; /** ID for a USD wallet belonging to the current user. */ readonly walletId: Scalars['WalletId']['input']; }; @@ -1806,6 +1912,16 @@ export const TxStatus = { } as const; export type TxStatus = typeof TxStatus[keyof typeof TxStatus]; +export type UpdateExternalWalletInput = { + readonly lnurlp: Scalars['Lnurl']['input']; +}; + +export type UpdateExternalWalletPayload = { + readonly __typename: 'UpdateExternalWalletPayload'; + readonly errors: ReadonlyArray; + readonly walletId?: Maybe; +}; + export type UpgradePayload = { readonly __typename: 'UpgradePayload'; readonly authToken?: Maybe; @@ -1817,8 +1933,9 @@ export type UpgradePayload = { export type UsdWallet = Wallet & { readonly __typename: 'UsdWallet'; readonly accountId: Scalars['ID']['output']; - readonly balance: Scalars['FractionalCentAmount']['output']; + readonly balance?: Maybe; readonly id: Scalars['ID']['output']; + readonly isExternal: Scalars['Boolean']['output']; readonly lnurlp?: Maybe; /** An unconfirmed incoming onchain balance. */ readonly pendingIncomingBalance: Scalars['SignedAmount']['output']; @@ -1846,8 +1963,44 @@ export type UsdWalletTransactionsByAddressArgs = { last?: InputMaybe; }; +/** A wallet belonging to an account which contains a USDT balance and a list of transactions. */ +export type UsdtWallet = Wallet & { + readonly __typename: 'UsdtWallet'; + readonly accountId: Scalars['ID']['output']; + readonly balance: Scalars['FractionalCentAmount']['output']; + readonly id: Scalars['ID']['output']; + readonly isExternal: Scalars['Boolean']['output']; + readonly lnurlp?: Maybe; + /** An unconfirmed incoming onchain balance. */ + readonly pendingIncomingBalance: Scalars['SignedAmount']['output']; + readonly transactions?: Maybe; + readonly transactionsByAddress?: Maybe; + readonly walletCurrency: WalletCurrency; +}; + + +/** A wallet belonging to an account which contains a USDT balance and a list of transactions. */ +export type UsdtWalletTransactionsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + + +/** A wallet belonging to an account which contains a USDT balance and a list of transactions. */ +export type UsdtWalletTransactionsByAddressArgs = { + address: Scalars['OnChainAddress']['input']; + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + export type User = { readonly __typename: 'User'; + /** Bank accounts available for cashout */ + readonly bankAccounts: ReadonlyArray; /** * Get single contact details. * Can include the transactions associated with the contact. @@ -2075,8 +2228,9 @@ export type UserUpdateUsernamePayload = { /** A generic wallet which stores value in one of our supported currencies. */ export type Wallet = { readonly accountId: Scalars['ID']['output']; - readonly balance: Scalars['FractionalCentAmount']['output']; + readonly balance?: Maybe; readonly id: Scalars['ID']['output']; + readonly isExternal: Scalars['Boolean']['output']; readonly lnurlp?: Maybe; readonly pendingIncomingBalance: Scalars['SignedAmount']['output']; /** @@ -2113,7 +2267,8 @@ export type WalletTransactionsByAddressArgs = { export const WalletCurrency = { Btc: 'BTC', - Usd: 'USD' + Usd: 'USD', + Usdt: 'USDT' } as const; export type WalletCurrency = typeof WalletCurrency[keyof typeof WalletCurrency]; @@ -2154,7 +2309,7 @@ export type AnalyticsQueryVariables = Exact<{ [key: string]: never; }>; export type AnalyticsQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly username?: string | null, readonly id: string } | null, readonly globals?: { readonly __typename: 'Globals', readonly network: Network } | null }; -export type MyWalletsFragment = { readonly __typename: 'ConsumerAccount', readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> }; +export type MyWalletsFragment = { readonly __typename: 'ConsumerAccount', readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> }; export type RealtimePriceQueryVariables = Exact<{ [key: string]: never; }>; @@ -2277,14 +2432,14 @@ export type RequestCashoutMutationVariables = Exact<{ }>; -export type RequestCashoutMutation = { readonly __typename: 'Mutation', readonly requestCashout: { readonly __typename: 'RequestCashoutResponse', readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string }>, readonly offer?: { readonly __typename: 'CashoutOffer', readonly exchangeRate: number, readonly expiresAt: number, readonly flashFee: number, readonly offerId: string, readonly receiveJmd: number, readonly receiveUsd: number, readonly send: number, readonly walletId: string } | null } }; +export type RequestCashoutMutation = { readonly __typename: 'Mutation', readonly requestCashout: { readonly __typename: 'RequestCashoutResponse', readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string }>, readonly offer?: { readonly __typename: 'CashoutOffer', readonly exchangeRate?: number | null, readonly expiresAt: number, readonly flashFee: number, readonly offerId: string, readonly receiveJmd?: number | null, readonly receiveUsd?: number | null, readonly send: number, readonly walletId: string } | null } }; export type InitiateCashoutMutationVariables = Exact<{ input: InitiateCashoutInput; }>; -export type InitiateCashoutMutation = { readonly __typename: 'Mutation', readonly initiateCashout: { readonly __typename: 'InitiatedCashoutResponse', readonly journalId?: string | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string }> } }; +export type InitiateCashoutMutation = { readonly __typename: 'Mutation', readonly initiateCashout: { readonly __typename: 'InitiatedCashoutResponse', readonly id?: string | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string }> } }; export type AccountDeleteMutationVariables = Exact<{ [key: string]: never; }>; @@ -2319,6 +2474,13 @@ export type IdDocumentUploadUrlGenerateMutationVariables = Exact<{ export type IdDocumentUploadUrlGenerateMutation = { readonly __typename: 'Mutation', readonly idDocumentUploadUrlGenerate: { readonly __typename: 'IdDocumentUploadUrlPayload', readonly fileKey?: string | null, readonly uploadUrl?: string | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string }> } }; +export type BridgeInitiateKycMutationVariables = Exact<{ + input: BridgeInitiateKycInput; +}>; + + +export type BridgeInitiateKycMutation = { readonly __typename: 'Mutation', readonly bridgeInitiateKyc: { readonly __typename: 'BridgeInitiateKycPayload', readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string }>, readonly kycLink?: { readonly __typename: 'BridgeKycLink', readonly kycLink: string, readonly tosLink: string } | null } }; + export type AuthQueryVariables = Exact<{ [key: string]: never; }>; @@ -2327,7 +2489,7 @@ export type AuthQuery = { readonly __typename: 'Query', readonly me?: { readonly export type HomeAuthedQueryVariables = Exact<{ [key: string]: never; }>; -export type HomeAuthedQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly language: string, readonly username?: string | null, readonly phone?: string | null, readonly npub?: string | null, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly level: AccountLevel, readonly defaultWalletId: string, readonly transactions?: { readonly __typename: 'TransactionConnection', readonly pageInfo: { readonly __typename: 'PageInfo', readonly hasNextPage: boolean, readonly hasPreviousPage: boolean, readonly startCursor?: string | null, readonly endCursor?: string | null }, readonly edges?: ReadonlyArray<{ readonly __typename: 'TransactionEdge', readonly cursor: string, readonly node: { readonly __typename: 'Transaction', readonly id: string, readonly status: TxStatus, readonly direction: TxDirection, readonly memo?: string | null, readonly createdAt: number, readonly settlementAmount: number, readonly settlementFee: number, readonly settlementDisplayFee: string, readonly settlementCurrency: WalletCurrency, readonly settlementDisplayAmount: string, readonly settlementDisplayCurrency: string, readonly settlementPrice: { readonly __typename: 'PriceOfOneSettlementMinorUnitInDisplayMinorUnit', readonly base: number, readonly offset: number, readonly currencyUnit: string, readonly formattedAmount: string }, readonly initiationVia: { readonly __typename: 'InitiationViaIntraLedger', readonly counterPartyWalletId?: string | null, readonly counterPartyUsername?: string | null } | { readonly __typename: 'InitiationViaLn', readonly paymentHash: string } | { readonly __typename: 'InitiationViaOnChain', readonly address: string }, readonly settlementVia: { readonly __typename: 'SettlementViaIntraLedger', readonly counterPartyWalletId?: string | null, readonly counterPartyUsername?: string | null } | { readonly __typename: 'SettlementViaLn', readonly paymentSecret?: string | null } | { readonly __typename: 'SettlementViaOnChain', readonly transactionHash?: string | null } } }> | null } | null, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; +export type HomeAuthedQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly language: string, readonly username?: string | null, readonly phone?: string | null, readonly npub?: string | null, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly level: AccountLevel, readonly defaultWalletId: string, readonly transactions?: { readonly __typename: 'TransactionConnection', readonly pageInfo: { readonly __typename: 'PageInfo', readonly hasNextPage: boolean, readonly hasPreviousPage: boolean, readonly startCursor?: string | null, readonly endCursor?: string | null }, readonly edges?: ReadonlyArray<{ readonly __typename: 'TransactionEdge', readonly cursor: string, readonly node: { readonly __typename: 'Transaction', readonly id: string, readonly status: TxStatus, readonly direction: TxDirection, readonly memo?: string | null, readonly createdAt: number, readonly settlementAmount: number, readonly settlementFee: number, readonly settlementDisplayFee: string, readonly settlementCurrency: WalletCurrency, readonly settlementDisplayAmount: string, readonly settlementDisplayCurrency: string, readonly settlementPrice: { readonly __typename: 'PriceOfOneSettlementMinorUnitInDisplayMinorUnit', readonly base: number, readonly offset: number, readonly currencyUnit: string, readonly formattedAmount: string }, readonly initiationVia: { readonly __typename: 'InitiationViaIntraLedger', readonly counterPartyWalletId?: string | null, readonly counterPartyUsername?: string | null } | { readonly __typename: 'InitiationViaLn', readonly paymentHash: string } | { readonly __typename: 'InitiationViaOnChain', readonly address: string }, readonly settlementVia: { readonly __typename: 'SettlementViaIntraLedger', readonly counterPartyWalletId?: string | null, readonly counterPartyUsername?: string | null } | { readonly __typename: 'SettlementViaLn', readonly paymentSecret?: string | null } | { readonly __typename: 'SettlementViaOnChain', readonly transactionHash?: string | null } } }> | null } | null, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; export type HomeUnauthedQueryVariables = Exact<{ [key: string]: never; }>; @@ -2347,17 +2509,17 @@ export type TransactionListForDefaultAccountQuery = { readonly __typename: 'Quer export type SetDefaultAccountModalQueryVariables = Exact<{ [key: string]: never; }>; -export type SetDefaultAccountModalQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; +export type SetDefaultAccountModalQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; export type ConversionScreenQueryVariables = Exact<{ [key: string]: never; }>; -export type ConversionScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; +export type ConversionScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; export type CashoutScreenQueryVariables = Exact<{ [key: string]: never; }>; -export type CashoutScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; +export type CashoutScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; export type WalletCsvTransactionsQueryVariables = Exact<{ walletIds: ReadonlyArray | Scalars['WalletId']['input']; @@ -2369,12 +2531,12 @@ export type WalletCsvTransactionsQuery = { readonly __typename: 'Query', readonl export type WalletOverviewScreenQueryVariables = Exact<{ [key: string]: never; }>; -export type WalletOverviewScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; +export type WalletOverviewScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; export type SendBitcoinDestinationQueryVariables = Exact<{ [key: string]: never; }>; -export type SendBitcoinDestinationQuery = { readonly __typename: 'Query', readonly globals?: { readonly __typename: 'Globals', readonly network: Network } | null, readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string } | { readonly __typename: 'UsdWallet', readonly id: string }> }, readonly contacts: ReadonlyArray<{ readonly __typename: 'UserContact', readonly id: string, readonly username: string }> } | null }; +export type SendBitcoinDestinationQuery = { readonly __typename: 'Query', readonly globals?: { readonly __typename: 'Globals', readonly network: Network } | null, readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string } | { readonly __typename: 'UsdWallet', readonly id: string } | { readonly __typename: 'UsdtWallet', readonly id: string }> }, readonly contacts: ReadonlyArray<{ readonly __typename: 'UserContact', readonly id: string, readonly username: string }> } | null }; export type AccountDefaultWalletQueryVariables = Exact<{ username: Scalars['Username']['input']; @@ -2386,7 +2548,7 @@ export type AccountDefaultWalletQuery = { readonly __typename: 'Query', readonly export type SendBitcoinDetailsScreenQueryVariables = Exact<{ [key: string]: never; }>; -export type SendBitcoinDetailsScreenQuery = { readonly __typename: 'Query', readonly globals?: { readonly __typename: 'Globals', readonly network: Network } | null, readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly walletCurrency: WalletCurrency, readonly balance: number } | { readonly __typename: 'UsdWallet', readonly id: string, readonly walletCurrency: WalletCurrency, readonly balance: number }> } } | null }; +export type SendBitcoinDetailsScreenQuery = { readonly __typename: 'Query', readonly globals?: { readonly __typename: 'Globals', readonly network: Network } | null, readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly walletCurrency: WalletCurrency, readonly balance?: number | null } | { readonly __typename: 'UsdWallet', readonly id: string, readonly walletCurrency: WalletCurrency, readonly balance?: number | null } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly walletCurrency: WalletCurrency, readonly balance: number }> } } | null }; export type SendBitcoinWithdrawalLimitsQueryVariables = Exact<{ [key: string]: never; }>; @@ -2401,12 +2563,12 @@ export type SendBitcoinInternalLimitsQuery = { readonly __typename: 'Query', rea export type SendBitcoinConfirmationScreenQueryVariables = Exact<{ [key: string]: never; }>; -export type SendBitcoinConfirmationScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; +export type SendBitcoinConfirmationScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; export type ScanningQrCodeScreenQueryVariables = Exact<{ [key: string]: never; }>; -export type ScanningQrCodeScreenQuery = { readonly __typename: 'Query', readonly globals?: { readonly __typename: 'Globals', readonly network: Network } | null, readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string } | { readonly __typename: 'UsdWallet', readonly id: string }> }, readonly contacts: ReadonlyArray<{ readonly __typename: 'UserContact', readonly id: string, readonly username: string }> } | null }; +export type ScanningQrCodeScreenQuery = { readonly __typename: 'Query', readonly globals?: { readonly __typename: 'Globals', readonly network: Network } | null, readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string } | { readonly __typename: 'UsdWallet', readonly id: string } | { readonly __typename: 'UsdtWallet', readonly id: string }> }, readonly contacts: ReadonlyArray<{ readonly __typename: 'UserContact', readonly id: string, readonly username: string }> } | null }; export type RealtimePriceUnauthedQueryVariables = Exact<{ currency: Scalars['DisplayCurrency']['input']; @@ -2425,13 +2587,23 @@ export type NpubByUsernameQuery = { readonly __typename: 'Query', readonly npubB export type LatestAccountUpgradeRequestQueryVariables = Exact<{ [key: string]: never; }>; -export type LatestAccountUpgradeRequestQuery = { readonly __typename: 'Query', readonly latestAccountUpgradeRequest: { readonly __typename: 'AccountUpgradeRequestPayload', readonly errors?: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string } | null> | null, readonly upgradeRequest?: { readonly __typename: 'AccountUpgradeRequest', readonly currentLevel: AccountLevel, readonly fullName: string, readonly terminalsRequested: number, readonly status: string, readonly requestedLevel: AccountLevel, readonly phoneNumber: string, readonly email?: string | null, readonly idDocument: boolean, readonly address: { readonly __typename: 'Address', readonly city: string, readonly country: string, readonly line1: string, readonly line2?: string | null, readonly postalCode?: string | null, readonly state: string, readonly title: string }, readonly bankAccount?: { readonly __typename: 'BankAccount', readonly accountNumber: number, readonly accountType: string, readonly bankBranch: string, readonly bankName: string, readonly currency: string } | null } | null } }; +export type LatestAccountUpgradeRequestQuery = { readonly __typename: 'Query', readonly latestAccountUpgradeRequest: { readonly __typename: 'AccountUpgradeRequestPayload', readonly errors?: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string } | null> | null, readonly upgradeRequest?: { readonly __typename: 'AccountUpgradeRequest', readonly currentLevel: AccountLevel, readonly fullName: string, readonly terminalsRequested: number, readonly status: string, readonly requestedLevel: AccountLevel, readonly phoneNumber: string, readonly email?: string | null, readonly idDocument: boolean, readonly address: { readonly __typename: 'Address', readonly city: string, readonly country: string, readonly line1: string, readonly line2?: string | null, readonly postalCode?: string | null, readonly state: string, readonly title: string }, readonly bankAccount?: { readonly __typename: 'BankAccount', readonly accountName: string, readonly accountNumber: string, readonly accountType: string, readonly bank: string, readonly branchCode: string, readonly currency: string, readonly id: string, readonly isDefault: boolean } | null } | null } }; export type SupportedBanksQueryVariables = Exact<{ [key: string]: never; }>; export type SupportedBanksQuery = { readonly __typename: 'Query', readonly supportedBanks: ReadonlyArray<{ readonly __typename: 'Bank', readonly name: string }> }; +export type BridgeKycStatusQueryVariables = Exact<{ [key: string]: never; }>; + + +export type BridgeKycStatusQuery = { readonly __typename: 'Query', readonly bridgeKycStatus?: string | null }; + +export type BridgeVirtualAccountQueryVariables = Exact<{ [key: string]: never; }>; + + +export type BridgeVirtualAccountQuery = { readonly __typename: 'Query', readonly bridgeVirtualAccount?: { readonly __typename: 'BridgeVirtualAccount', readonly accountNumber?: string | null, readonly accountNumberLast4?: string | null, readonly bankName?: string | null, readonly id?: string | null, readonly kycLink?: string | null, readonly message?: string | null, readonly pending?: boolean | null, readonly routingNumber?: string | null, readonly tosLink?: string | null } | null }; + export type RealtimePriceWsSubscriptionVariables = Exact<{ currency: Scalars['DisplayCurrency']['input']; }>; @@ -2638,7 +2810,7 @@ export type MyLnUpdatesSubscription = { readonly __typename: 'Subscription', rea export type PaymentRequestQueryVariables = Exact<{ [key: string]: never; }>; -export type PaymentRequestQuery = { readonly __typename: 'Query', readonly globals?: { readonly __typename: 'Globals', readonly network: Network, readonly feesInformation: { readonly __typename: 'FeesInformation', readonly deposit: { readonly __typename: 'DepositFeesInformation', readonly minBankFee: string, readonly minBankFeeThreshold: string } } } | null, readonly me?: { readonly __typename: 'User', readonly id: string, readonly username?: string | null, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; +export type PaymentRequestQuery = { readonly __typename: 'Query', readonly globals?: { readonly __typename: 'Globals', readonly network: Network, readonly feesInformation: { readonly __typename: 'FeesInformation', readonly deposit: { readonly __typename: 'DepositFeesInformation', readonly minBankFee: string, readonly minBankFeeThreshold: string } } } | null, readonly me?: { readonly __typename: 'User', readonly id: string, readonly username?: string | null, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; export type LnNoAmountInvoiceCreateMutationVariables = Exact<{ input: LnNoAmountInvoiceCreateInput; @@ -2678,7 +2850,7 @@ export type FeedbackSubmitMutation = { readonly __typename: 'Mutation', readonly export type AccountScreenQueryVariables = Exact<{ [key: string]: never; }>; -export type AccountScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly phone?: string | null, readonly totpEnabled: boolean, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly level: AccountLevel, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; +export type AccountScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly phone?: string | null, readonly totpEnabled: boolean, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly level: AccountLevel, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; export type UserEmailDeleteMutationVariables = Exact<{ [key: string]: never; }>; @@ -2700,7 +2872,7 @@ export type UserTotpDeleteMutation = { readonly __typename: 'Mutation', readonly export type WarningSecureAccountQueryVariables = Exact<{ [key: string]: never; }>; -export type WarningSecureAccountQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly level: AccountLevel, readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; +export type WarningSecureAccountQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly level: AccountLevel, readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; export type AccountUpdateDefaultWalletIdMutationVariables = Exact<{ input: AccountUpdateDefaultWalletIdInput; @@ -2712,7 +2884,7 @@ export type AccountUpdateDefaultWalletIdMutation = { readonly __typename: 'Mutat export type SetDefaultWalletScreenQueryVariables = Exact<{ [key: string]: never; }>; -export type SetDefaultWalletScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; +export type SetDefaultWalletScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; export type AccountUpdateDisplayCurrencyMutationVariables = Exact<{ input: AccountUpdateDisplayCurrencyInput; @@ -2769,7 +2941,7 @@ export type AccountDisableNotificationCategoryMutation = { readonly __typename: export type SettingsScreenQueryVariables = Exact<{ [key: string]: never; }>; -export type SettingsScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly phone?: string | null, readonly username?: string | null, readonly language: string, readonly totpEnabled: boolean, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> }, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null } | null }; +export type SettingsScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly phone?: string | null, readonly username?: string | null, readonly language: string, readonly totpEnabled: boolean, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> }, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null } | null }; export type ExportCsvSettingQueryVariables = Exact<{ walletIds: ReadonlyArray | Scalars['WalletId']['input']; @@ -2819,7 +2991,7 @@ export type DeviceNotificationTokenCreateMutation = { readonly __typename: 'Muta export type WalletsQueryVariables = Exact<{ [key: string]: never; }>; -export type WalletsQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly walletCurrency: WalletCurrency, readonly id: string, readonly lnurlp?: string | null } | { readonly __typename: 'UsdWallet', readonly walletCurrency: WalletCurrency, readonly id: string, readonly lnurlp?: string | null }> } } | null }; +export type WalletsQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly walletCurrency: WalletCurrency, readonly id: string, readonly lnurlp?: string | null } | { readonly __typename: 'UsdWallet', readonly walletCurrency: WalletCurrency, readonly id: string, readonly lnurlp?: string | null } | { readonly __typename: 'UsdtWallet', readonly walletCurrency: WalletCurrency, readonly id: string, readonly lnurlp?: string | null }> } } | null }; export const MyWalletsFragmentDoc = gql` fragment MyWallets on ConsumerAccount { @@ -3785,7 +3957,7 @@ export const InitiateCashoutDocument = gql` code message } - journalId + id } } `; @@ -4002,6 +4174,46 @@ export function useIdDocumentUploadUrlGenerateMutation(baseOptions?: Apollo.Muta export type IdDocumentUploadUrlGenerateMutationHookResult = ReturnType; export type IdDocumentUploadUrlGenerateMutationResult = Apollo.MutationResult; export type IdDocumentUploadUrlGenerateMutationOptions = Apollo.BaseMutationOptions; +export const BridgeInitiateKycDocument = gql` + mutation BridgeInitiateKyc($input: BridgeInitiateKycInput!) { + bridgeInitiateKyc(input: $input) { + errors { + code + message + } + kycLink { + kycLink + tosLink + } + } +} + `; +export type BridgeInitiateKycMutationFn = Apollo.MutationFunction; + +/** + * __useBridgeInitiateKycMutation__ + * + * To run a mutation, you first call `useBridgeInitiateKycMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useBridgeInitiateKycMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [bridgeInitiateKycMutation, { data, loading, error }] = useBridgeInitiateKycMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useBridgeInitiateKycMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(BridgeInitiateKycDocument, options); + } +export type BridgeInitiateKycMutationHookResult = ReturnType; +export type BridgeInitiateKycMutationResult = Apollo.MutationResult; +export type BridgeInitiateKycMutationOptions = Apollo.BaseMutationOptions; export const AuthDocument = gql` query auth { me { @@ -4794,11 +5006,14 @@ export const LatestAccountUpgradeRequestDocument = gql` title } bankAccount { + accountName accountNumber accountType - bankBranch - bankName + bank + branchCode currency + id + isDefault } currentLevel fullName @@ -4873,6 +5088,80 @@ export function useSupportedBanksLazyQuery(baseOptions?: Apollo.LazyQueryHookOpt export type SupportedBanksQueryHookResult = ReturnType; export type SupportedBanksLazyQueryHookResult = ReturnType; export type SupportedBanksQueryResult = Apollo.QueryResult; +export const BridgeKycStatusDocument = gql` + query BridgeKycStatus { + bridgeKycStatus +} + `; + +/** + * __useBridgeKycStatusQuery__ + * + * To run a query within a React component, call `useBridgeKycStatusQuery` and pass it any options that fit your needs. + * When your component renders, `useBridgeKycStatusQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useBridgeKycStatusQuery({ + * variables: { + * }, + * }); + */ +export function useBridgeKycStatusQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(BridgeKycStatusDocument, options); + } +export function useBridgeKycStatusLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(BridgeKycStatusDocument, options); + } +export type BridgeKycStatusQueryHookResult = ReturnType; +export type BridgeKycStatusLazyQueryHookResult = ReturnType; +export type BridgeKycStatusQueryResult = Apollo.QueryResult; +export const BridgeVirtualAccountDocument = gql` + query BridgeVirtualAccount { + bridgeVirtualAccount { + accountNumber + accountNumberLast4 + bankName + id + kycLink + message + pending + routingNumber + tosLink + } +} + `; + +/** + * __useBridgeVirtualAccountQuery__ + * + * To run a query within a React component, call `useBridgeVirtualAccountQuery` and pass it any options that fit your needs. + * When your component renders, `useBridgeVirtualAccountQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useBridgeVirtualAccountQuery({ + * variables: { + * }, + * }); + */ +export function useBridgeVirtualAccountQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(BridgeVirtualAccountDocument, options); + } +export function useBridgeVirtualAccountLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(BridgeVirtualAccountDocument, options); + } +export type BridgeVirtualAccountQueryHookResult = ReturnType; +export type BridgeVirtualAccountLazyQueryHookResult = ReturnType; +export type BridgeVirtualAccountQueryResult = Apollo.QueryResult; export const RealtimePriceWsDocument = gql` subscription realtimePriceWs($currency: DisplayCurrency!) { realtimePrice(input: {currency: $currency}) { diff --git a/app/screens/topup-cashout-flow/BridgeKycWebView.tsx b/app/screens/topup-cashout-flow/BridgeKycWebView.tsx index 4f2013a38..dff28ea63 100644 --- a/app/screens/topup-cashout-flow/BridgeKycWebView.tsx +++ b/app/screens/topup-cashout-flow/BridgeKycWebView.tsx @@ -1,5 +1,11 @@ import React, { useState, useRef, useCallback } from "react" -import { View, ActivityIndicator, Linking, Platform, TouchableOpacity } from "react-native" +import { + View, + ActivityIndicator, + Linking, + Platform, + TouchableOpacity, +} from "react-native" import { StackScreenProps } from "@react-navigation/stack" import { Text, makeStyles, useTheme } from "@rneui/themed" import { RootStackParamList } from "@app/navigation/stack-param-lists" @@ -220,11 +226,7 @@ const BridgeKycWebView: React.FC = ({ navigation, route }) => { // During ToS step, open terms/privacy links in external browser if (currentStep === "tos" && request.url !== tosLink) { const url = request.url.toLowerCase() - if ( - url.includes("terms") || - url.includes("privacy") || - url.includes("policy") - ) { + if (url.includes("www.bridge.xyz/legal")) { Linking.openURL(request.url) return false } From 4753d843ba132f6e0418b92319da3608ad4f0cc7 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Tue, 26 May 2026 12:05:03 +0500 Subject: [PATCH 41/95] add BankAccounts query that fetches user bank account from erpnext --- app/graphql/front-end-queries.ts | 16 ++++++++ app/graphql/generated.gql | 22 ++++++++++- app/graphql/generated.ts | 65 ++++++++++++++++++++++++++++---- 3 files changed, 93 insertions(+), 10 deletions(-) diff --git a/app/graphql/front-end-queries.ts b/app/graphql/front-end-queries.ts index b670de713..2e7f9abdd 100644 --- a/app/graphql/front-end-queries.ts +++ b/app/graphql/front-end-queries.ts @@ -336,4 +336,20 @@ gql` tosLink } } + + query BankAccounts { + me { + id + bankAccounts { + accountName + accountNumber + accountType + bankBranch + bankName + currency + id + isDefault + } + } + } ` diff --git a/app/graphql/generated.gql b/app/graphql/generated.gql index b2bc45471..8987cb0f8 100644 --- a/app/graphql/generated.gql +++ b/app/graphql/generated.gql @@ -878,6 +878,24 @@ mutation userUpdateUsername($input: UserUpdateUsernameInput!) { } } +query BankAccounts { + me { + id + bankAccounts { + accountName + accountNumber + accountType + bankBranch + bankName + currency + id + isDefault + __typename + } + __typename + } +} + query BridgeKycStatus { bridgeKycStatus } @@ -931,8 +949,8 @@ query LatestAccountUpgradeRequest { accountName accountNumber accountType - bank - branchCode + bankBranch + bankName currency id isDefault diff --git a/app/graphql/generated.ts b/app/graphql/generated.ts index c282cec03..174244357 100644 --- a/app/graphql/generated.ts +++ b/app/graphql/generated.ts @@ -302,16 +302,15 @@ export type Bank = { export type BankAccount = { readonly __typename: 'BankAccount'; - readonly accountName: Scalars['String']['output']; + readonly accountName?: Maybe; readonly accountNumber: Scalars['String']['output']; readonly accountType: Scalars['String']['output']; - /** Name of the bank institution */ - readonly bank: Scalars['String']['output']; - readonly branchCode: Scalars['String']['output']; + readonly bankBranch: Scalars['String']['output']; + readonly bankName: Scalars['String']['output']; /** Account currency (e.g. JMD, USD) */ readonly currency: Scalars['String']['output']; /** ERPNext bank account identifier */ - readonly id: Scalars['ID']['output']; + readonly id?: Maybe; readonly isDefault: Scalars['Boolean']['output']; }; @@ -390,6 +389,7 @@ export type BridgeWithdrawal = { readonly amount: Scalars['String']['output']; readonly createdAt: Scalars['String']['output']; readonly currency: Scalars['String']['output']; + readonly failureReason?: Maybe; readonly id: Scalars['ID']['output']; readonly status: Scalars['String']['output']; }; @@ -2587,7 +2587,7 @@ export type NpubByUsernameQuery = { readonly __typename: 'Query', readonly npubB export type LatestAccountUpgradeRequestQueryVariables = Exact<{ [key: string]: never; }>; -export type LatestAccountUpgradeRequestQuery = { readonly __typename: 'Query', readonly latestAccountUpgradeRequest: { readonly __typename: 'AccountUpgradeRequestPayload', readonly errors?: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string } | null> | null, readonly upgradeRequest?: { readonly __typename: 'AccountUpgradeRequest', readonly currentLevel: AccountLevel, readonly fullName: string, readonly terminalsRequested: number, readonly status: string, readonly requestedLevel: AccountLevel, readonly phoneNumber: string, readonly email?: string | null, readonly idDocument: boolean, readonly address: { readonly __typename: 'Address', readonly city: string, readonly country: string, readonly line1: string, readonly line2?: string | null, readonly postalCode?: string | null, readonly state: string, readonly title: string }, readonly bankAccount?: { readonly __typename: 'BankAccount', readonly accountName: string, readonly accountNumber: string, readonly accountType: string, readonly bank: string, readonly branchCode: string, readonly currency: string, readonly id: string, readonly isDefault: boolean } | null } | null } }; +export type LatestAccountUpgradeRequestQuery = { readonly __typename: 'Query', readonly latestAccountUpgradeRequest: { readonly __typename: 'AccountUpgradeRequestPayload', readonly errors?: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string } | null> | null, readonly upgradeRequest?: { readonly __typename: 'AccountUpgradeRequest', readonly currentLevel: AccountLevel, readonly fullName: string, readonly terminalsRequested: number, readonly status: string, readonly requestedLevel: AccountLevel, readonly phoneNumber: string, readonly email?: string | null, readonly idDocument: boolean, readonly address: { readonly __typename: 'Address', readonly city: string, readonly country: string, readonly line1: string, readonly line2?: string | null, readonly postalCode?: string | null, readonly state: string, readonly title: string }, readonly bankAccount?: { readonly __typename: 'BankAccount', readonly accountName?: string | null, readonly accountNumber: string, readonly accountType: string, readonly bankBranch: string, readonly bankName: string, readonly currency: string, readonly id?: string | null, readonly isDefault: boolean } | null } | null } }; export type SupportedBanksQueryVariables = Exact<{ [key: string]: never; }>; @@ -2604,6 +2604,11 @@ export type BridgeVirtualAccountQueryVariables = Exact<{ [key: string]: never; } export type BridgeVirtualAccountQuery = { readonly __typename: 'Query', readonly bridgeVirtualAccount?: { readonly __typename: 'BridgeVirtualAccount', readonly accountNumber?: string | null, readonly accountNumberLast4?: string | null, readonly bankName?: string | null, readonly id?: string | null, readonly kycLink?: string | null, readonly message?: string | null, readonly pending?: boolean | null, readonly routingNumber?: string | null, readonly tosLink?: string | null } | null }; +export type BankAccountsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type BankAccountsQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly bankAccounts: ReadonlyArray<{ readonly __typename: 'BankAccount', readonly accountName?: string | null, readonly accountNumber: string, readonly accountType: string, readonly bankBranch: string, readonly bankName: string, readonly currency: string, readonly id?: string | null, readonly isDefault: boolean }> } | null }; + export type RealtimePriceWsSubscriptionVariables = Exact<{ currency: Scalars['DisplayCurrency']['input']; }>; @@ -5009,8 +5014,8 @@ export const LatestAccountUpgradeRequestDocument = gql` accountName accountNumber accountType - bank - branchCode + bankBranch + bankName currency id isDefault @@ -5162,6 +5167,50 @@ export function useBridgeVirtualAccountLazyQuery(baseOptions?: Apollo.LazyQueryH export type BridgeVirtualAccountQueryHookResult = ReturnType; export type BridgeVirtualAccountLazyQueryHookResult = ReturnType; export type BridgeVirtualAccountQueryResult = Apollo.QueryResult; +export const BankAccountsDocument = gql` + query BankAccounts { + me { + id + bankAccounts { + accountName + accountNumber + accountType + bankBranch + bankName + currency + id + isDefault + } + } +} + `; + +/** + * __useBankAccountsQuery__ + * + * To run a query within a React component, call `useBankAccountsQuery` and pass it any options that fit your needs. + * When your component renders, `useBankAccountsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useBankAccountsQuery({ + * variables: { + * }, + * }); + */ +export function useBankAccountsQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(BankAccountsDocument, options); + } +export function useBankAccountsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(BankAccountsDocument, options); + } +export type BankAccountsQueryHookResult = ReturnType; +export type BankAccountsLazyQueryHookResult = ReturnType; +export type BankAccountsQueryResult = Apollo.QueryResult; export const RealtimePriceWsDocument = gql` subscription realtimePriceWs($currency: DisplayCurrency!) { realtimePrice(input: {currency: $currency}) { From 6a3feace53df1aa9e8bf52953566d8820c22a5b9 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Tue, 26 May 2026 12:06:15 +0500 Subject: [PATCH 42/95] update CashoutDetails screen to implement requestCashout mutation changes --- .../topup-cashout-flow/CashoutDetails.tsx | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/app/screens/topup-cashout-flow/CashoutDetails.tsx b/app/screens/topup-cashout-flow/CashoutDetails.tsx index dffa4442c..57fe1727a 100644 --- a/app/screens/topup-cashout-flow/CashoutDetails.tsx +++ b/app/screens/topup-cashout-flow/CashoutDetails.tsx @@ -10,7 +10,11 @@ import { AmountInput } from "@app/components/amount-input" import { CashoutFromWallet, CashoutPercentage } from "@app/components/topup-cashout-flow" // hooks -import { useCashoutScreenQuery, useRequestCashoutMutation } from "@app/graphql/generated" +import { + useBankAccountsQuery, + useCashoutScreenQuery, + useRequestCashoutMutation, +} from "@app/graphql/generated" import { useI18nContext } from "@app/i18n/i18n-react" import { useActivityIndicator, useDisplayCurrency, usePriceConversion } from "@app/hooks" @@ -43,6 +47,10 @@ const CashoutDetails = ({ navigation }: Props) => { const [requestCashout] = useRequestCashoutMutation() + const { data: bankAccountsData, loading } = useBankAccountsQuery({ + fetchPolicy: "network-only", + }) + const { data } = useCashoutScreenQuery({ fetchPolicy: "cache-first", returnPartialData: true, @@ -56,14 +64,24 @@ const CashoutDetails = ({ navigation }: Props) => { const usdBalance = toUsdMoneyAmount(usdWallet?.balance ?? NaN) const settlementSendAmount = convertMoneyAmount(moneyAmount, "USD") const isValidAmount = - settlementSendAmount.amount > 0 && settlementSendAmount.amount <= usdBalance.amount + settlementSendAmount.amount > 0 && + settlementSendAmount.amount <= usdBalance.amount && + !loading const onNext = async () => { - if (usdWallet) { + const defaultBankAccount = + bankAccountsData?.me?.bankAccounts.find((el) => el.isDefault) || + bankAccountsData?.me?.bankAccounts[0] + + if (usdWallet && defaultBankAccount?.id) { toggleActivityIndicator(true) const res = await requestCashout({ variables: { - input: { walletId: usdWallet.id, amount: settlementSendAmount.amount }, + input: { + bankAccountId: defaultBankAccount.id, + walletId: usdWallet.id, + amount: settlementSendAmount.amount, + }, }, }) console.log("Response: ", res.data?.requestCashout) From dea543b11c50b01a4836491ff95b7a88beb4da8c Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Tue, 26 May 2026 12:08:07 +0500 Subject: [PATCH 43/95] resolve CashoutConfirmation screen issues after cashout mutations updated and update CashoutWithdrawTo component to use BankAccounts query to show user default bank account --- .../topup-cashout-flow/CashoutWithdrawTo.tsx | 10 +++---- .../CashoutConfirmation.tsx | 28 ++++++++++++------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/app/components/topup-cashout-flow/CashoutWithdrawTo.tsx b/app/components/topup-cashout-flow/CashoutWithdrawTo.tsx index fa7bf19b2..e54e64a2a 100644 --- a/app/components/topup-cashout-flow/CashoutWithdrawTo.tsx +++ b/app/components/topup-cashout-flow/CashoutWithdrawTo.tsx @@ -3,7 +3,7 @@ import { TouchableOpacity, View } from "react-native" import { Icon, makeStyles, Text, useTheme } from "@rneui/themed" import { useI18nContext } from "@app/i18n/i18n-react" -import { useLatestAccountUpgradeRequestQuery } from "@app/graphql/generated" +import { useBankAccountsQuery } from "@app/graphql/generated" const CashoutWithdrawTo: React.FC = () => { const styles = useStyles() @@ -11,12 +11,10 @@ const CashoutWithdrawTo: React.FC = () => { const { LL } = useI18nContext() const [expanded, setExpanded] = useState(false) - const { data } = useLatestAccountUpgradeRequestQuery({ - fetchPolicy: "cache-first", - }) + const { data } = useBankAccountsQuery({ fetchPolicy: "cache-only" }) - const bankAccount = data?.latestAccountUpgradeRequest.upgradeRequest?.bankAccount - console.log(">>>>>>>>>>>?????????", data) + const bankAccount = + data?.me?.bankAccounts.find((el) => el.isDefault) || data?.me?.bankAccounts[0] if (!bankAccount) return null diff --git a/app/screens/topup-cashout-flow/CashoutConfirmation.tsx b/app/screens/topup-cashout-flow/CashoutConfirmation.tsx index 7ca651b46..c4e3c5e99 100644 --- a/app/screens/topup-cashout-flow/CashoutConfirmation.tsx +++ b/app/screens/topup-cashout-flow/CashoutConfirmation.tsx @@ -7,7 +7,11 @@ import moment from "moment" // components import { Screen } from "@app/components/screen" import { PrimaryBtn } from "@app/components/buttons" -import { CashoutCard, CashoutFromWallet, CashoutWithdrawTo } from "@app/components/topup-cashout-flow" +import { + CashoutCard, + CashoutFromWallet, + CashoutWithdrawTo, +} from "@app/components/topup-cashout-flow" // hooks import { useI18nContext } from "@app/i18n/i18n-react" @@ -57,7 +61,7 @@ const CashoutConfirmation: React.FC = ({ navigation, route }) => { const onConfirm = async () => { toggleActivityIndicator(true) const res = await initiateCashout({ variables: { input: { walletId, offerId } } }) - if (res.data?.initiateCashout.journalId) { + if (res.data?.initiateCashout.id) { navigation.navigate("CashoutSuccess") } else { setErrorMsg(res.data?.initiateCashout.errors[0].message) @@ -84,15 +88,19 @@ const CashoutConfirmation: React.FC = ({ navigation, route }) => { {LL.Cashout.valid({ time: moment(expiresAt).fromNow(true) })} - + {exchangeRate && ( + + )} - + {receiveJmd && ( + + )} {!!errorMsg && ( From 64280bfc21895a979a9d8cb17ea1f93f9d1d958f Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Tue, 26 May 2026 12:08:26 +0500 Subject: [PATCH 44/95] resolve ios build issue --- ios/.xcode.env | 4 +++- ios/.xcode.env.local | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ios/.xcode.env b/ios/.xcode.env index b632b750d..01de1c0d2 100644 --- a/ios/.xcode.env +++ b/ios/.xcode.env @@ -7,4 +7,6 @@ # Customize the NODE_BINARY variable here. # For example, to use nvm with brew, add the following line # . "$(brew --prefix nvm)/nvm.sh" --no-use -export NODE_BINARY=/usr/local/bin/node +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" --no-use +export NODE_BINARY="$(nvm which current)" diff --git a/ios/.xcode.env.local b/ios/.xcode.env.local index d85cff741..f1e77fe8b 100644 --- a/ios/.xcode.env.local +++ b/ios/.xcode.env.local @@ -1 +1,3 @@ -export NODE_BINARY=/usr/local/bin/node +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" --no-use +export NODE_BINARY="$(nvm which current)" From 36900de98b0c91265e780b9c2ec634e39d7d78c0 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Sun, 31 May 2026 20:33:39 +0500 Subject: [PATCH 45/95] display cashout/topup button to all users except level Zero on the home screen --- app/components/home-screen/Buttons.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/home-screen/Buttons.tsx b/app/components/home-screen/Buttons.tsx index a8814dbcd..7a46d228f 100644 --- a/app/components/home-screen/Buttons.tsx +++ b/app/components/home-screen/Buttons.tsx @@ -68,7 +68,7 @@ const Buttons: React.FC = ({ setModalVisible, setDefaultAccountModalVisib }) } - if (currentLevel === AccountLevel.Two || currentLevel === AccountLevel.Three) { + if (currentLevel !== AccountLevel.Zero) { buttons.push({ title: LL.HomeScreen.transfer(), target: "TopupCashout", From cc87f3ab4ccb47faaf91b3a24bb83e5f70423649 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Sun, 31 May 2026 20:37:21 +0500 Subject: [PATCH 46/95] update topup cashout option on the TopupCashout screen that level Zero/One users press the top up or cashout button navigates to account upgrade level --- .../topup-cashout-flow/TopupCashout.tsx | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/app/screens/topup-cashout-flow/TopupCashout.tsx b/app/screens/topup-cashout-flow/TopupCashout.tsx index 1cbf77c73..d5ae8fd58 100644 --- a/app/screens/topup-cashout-flow/TopupCashout.tsx +++ b/app/screens/topup-cashout-flow/TopupCashout.tsx @@ -21,12 +21,17 @@ import { useBridgeKycStatusQuery, } from "@app/graphql/generated" import { useActivityIndicator } from "@app/hooks" +import { useLevel } from "@app/graphql/level-context" + +// utils +import { AccountLevel } from "@app/graphql/generated" type Props = StackScreenProps const TopupCashout: React.FC = ({ navigation }) => { const styles = useStyles() const { LL } = useI18nContext() + const { currentLevel } = useLevel() const { colors } = useTheme().theme const { toggleActivityIndicator } = useActivityIndicator() @@ -60,7 +65,7 @@ const TopupCashout: React.FC = ({ navigation }) => { description: LL.TopUpScreen.debitCreditCardDesc(), onPress: () => { setTopupModalVisible(false) - navigation.navigate("TopupDetails", { paymentType: "card" }) + checkAccountLevel("card") }, }, { @@ -69,7 +74,7 @@ const TopupCashout: React.FC = ({ navigation }) => { description: LL.TopUpScreen.bankTransferDesc(), onPress: () => { setTopupModalVisible(false) - navigation.navigate("TopupDetails", { paymentType: "bankTransfer" }) + checkAccountLevel("bankTransfer") }, }, { @@ -94,7 +99,7 @@ const TopupCashout: React.FC = ({ navigation }) => { description: LL.TransferScreen.jmdBankAccountDesc(), onPress: () => { setSettleModalVisible(false) - navigation.navigate("CashoutDetails") + checkAccountLevel("cashout") }, }, { @@ -111,6 +116,25 @@ const TopupCashout: React.FC = ({ navigation }) => { [LL, navigation, kycStatusData?.bridgeKycStatus], ) + const checkAccountLevel = (type: "card" | "bankTransfer" | "cashout") => { + if (currentLevel === AccountLevel.Zero || currentLevel === AccountLevel.One) { + Alert.alert( + "Account upgrade required", + "You should upgrade your account to use this feature", + [ + { text: "Continue", onPress: () => navigation.navigate("AccountType") }, + { text: "Cancel", style: "cancel" }, + ], + ) + } else { + if (type === "cashout") { + navigation.navigate("CashoutDetails") + } else { + navigation.navigate("TopupDetails", { paymentType: type }) + } + } + } + const checkBridgeKyc = (type: "topup" | "settle") => { if (kycStatusData?.bridgeKycStatus === "pending") { Alert.alert("KYC Pending", "Your KYC status is pending. Please wait for approval.") @@ -141,13 +165,11 @@ const TopupCashout: React.FC = ({ navigation }) => { }) toggleActivityIndicator(false) console.log("BRIDGE INITIATE KYC RESPONSE: ", res) - const errors = res.data?.bridgeInitiateKyc?.errors if (errors && errors.length > 0) { Alert.alert("Error", errors[0].message) return } - const kycLink = res.data?.bridgeInitiateKyc?.kycLink if (kycLink?.tosLink && kycLink?.kycLink) { navigation.navigate("BridgeKycWebView", { From 955c0e9cbe1e95d5359f9d28e647c1344f8f710c Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Mon, 1 Jun 2026 16:51:07 +0500 Subject: [PATCH 47/95] fixed node versioning --- ios/.xcode.env.local | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/.xcode.env.local b/ios/.xcode.env.local index f1e77fe8b..32469593f 100644 --- a/ios/.xcode.env.local +++ b/ios/.xcode.env.local @@ -1,3 +1,3 @@ export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" --no-use -export NODE_BINARY="$(nvm which current)" +export NODE_BINARY="$(nvm which default)" From 24598e9d7ffb3e72bddae6563f60529eec91d4a5 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Sun, 7 Jun 2026 16:38:19 +0500 Subject: [PATCH 48/95] update Type Definitions & Core Infrastructure to support usdt wallet --- app/graphql/cache.ts | 2 +- app/types/amounts.ts | 22 ++++++++++++++++++++++ app/types/wallets.ts | 1 + 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/graphql/cache.ts b/app/graphql/cache.ts index 4d10e375b..e7a2290fc 100644 --- a/app/graphql/cache.ts +++ b/app/graphql/cache.ts @@ -61,7 +61,7 @@ export const createCache = () => new InMemoryCache({ possibleTypes: { // TODO: add other possible types - Wallet: ["BTCWallet", "UsdWallet"], + Wallet: ["BTCWallet", "UsdWallet", "UsdtWallet"], Account: ["ConsumerAccount"], }, typePolicies: { diff --git a/app/types/amounts.ts b/app/types/amounts.ts index 867b75d8b..92c80b796 100644 --- a/app/types/amounts.ts +++ b/app/types/amounts.ts @@ -23,6 +23,7 @@ export type MoneyAmount = { export type WalletAmount = MoneyAmount export type UsdMoneyAmount = WalletAmount +export type UsdtMoneyAmount = WalletAmount export type BtcMoneyAmount = WalletAmount export const ZeroUsdMoneyAmount: UsdMoneyAmount = { @@ -31,6 +32,12 @@ export const ZeroUsdMoneyAmount: UsdMoneyAmount = { currencyCode: "USD", } +export const ZeroUsdtMoneyAmount: UsdtMoneyAmount = { + amount: 0, + currency: WalletCurrency.Usdt, + currencyCode: "USDT", +} + export const ZeroBtcMoneyAmount: BtcMoneyAmount = { amount: 0, currency: WalletCurrency.Btc, @@ -67,6 +74,21 @@ export const toUsdMoneyAmount = (amount: number | undefined | null): UsdMoneyAmo } } +export const toUsdtMoneyAmount = (amount: number | undefined | null): UsdtMoneyAmount => { + if (amount === undefined || amount === null) { + return { + amount: NaN, + currency: WalletCurrency.Usdt, + currencyCode: "USDT", + } + } + return { + amount, + currency: WalletCurrency.Usdt, + currencyCode: "USDT", + } +} + export const toWalletAmount = ({ amount, currency, diff --git a/app/types/wallets.ts b/app/types/wallets.ts index de7a5d996..19d871332 100644 --- a/app/types/wallets.ts +++ b/app/types/wallets.ts @@ -6,4 +6,5 @@ export type WalletDescriptor = { } export type UsdWalletDescriptor = WalletDescriptor<"USD"> +export type UsdtWalletDescriptor = WalletDescriptor<"USDT"> export type BtcWalletDescriptor = WalletDescriptor<"BTC"> From 104b829e5ab529b8d8bc1691696bb8356491c4fa Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Sun, 7 Jun 2026 16:39:29 +0500 Subject: [PATCH 49/95] update Price Conversion & Display hooks to support usdt wallet --- app/hooks/use-display-currency.ts | 8 ++++++++ app/hooks/use-price-conversion.ts | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/app/hooks/use-display-currency.ts b/app/hooks/use-display-currency.ts index 877fbb660..5b874364e 100644 --- a/app/hooks/use-display-currency.ts +++ b/app/hooks/use-display-currency.ts @@ -136,6 +136,7 @@ export const useDisplayCurrency = () => { case WalletCurrency.Btc: return moneyAmount.amount case WalletCurrency.Usd: + case WalletCurrency.Usdt: return moneyAmount.amount / 100 case DisplayCurrency: return moneyAmount.amount / 10 ** displayCurrencyInfo.fractionDigits @@ -153,6 +154,7 @@ export const useDisplayCurrency = () => { case WalletCurrency.Btc: return toBtcMoneyAmount(Math.round(amount)) case WalletCurrency.Usd: + case WalletCurrency.Usdt: return toUsdMoneyAmount(Math.round(amount * 100)) case DisplayCurrency: return toDisplayMoneyAmount( @@ -176,6 +178,12 @@ export const useDisplayCurrency = () => { showFractionDigits: true, currencyCode: usdDisplayCurrency.id, }, + [WalletCurrency.Usdt]: { + symbol: usdDisplayCurrency.symbol, + minorUnitToMajorUnitOffset: usdDisplayCurrency.fractionDigits, + showFractionDigits: true, + currencyCode: "USDT", + }, [WalletCurrency.Btc]: { symbol: "₿", minorUnitToMajorUnitOffset: 0, diff --git a/app/hooks/use-price-conversion.ts b/app/hooks/use-price-conversion.ts index 19e3cd29a..6d4063e18 100644 --- a/app/hooks/use-price-conversion.ts +++ b/app/hooks/use-price-conversion.ts @@ -62,16 +62,25 @@ export const usePriceConversion = () => { [WalletCurrency.Btc]: { [DisplayCurrency]: displayCurrencyPerSat, [WalletCurrency.Usd]: displayCurrencyPerSat * (1 / displayCurrencyPerCent), + [WalletCurrency.Usdt]: displayCurrencyPerSat * (1 / displayCurrencyPerCent), [WalletCurrency.Btc]: 1, }, [WalletCurrency.Usd]: { [DisplayCurrency]: displayCurrencyPerCent, [WalletCurrency.Btc]: displayCurrencyPerCent * (1 / displayCurrencyPerSat), [WalletCurrency.Usd]: 1, + [WalletCurrency.Usdt]: 1, + }, + [WalletCurrency.Usdt]: { + [DisplayCurrency]: displayCurrencyPerCent, + [WalletCurrency.Btc]: displayCurrencyPerCent * (1 / displayCurrencyPerSat), + [WalletCurrency.Usd]: 1, + [WalletCurrency.Usdt]: 1, }, [DisplayCurrency]: { [WalletCurrency.Btc]: 1 / displayCurrencyPerSat, [WalletCurrency.Usd]: 1 / displayCurrencyPerCent, + [WalletCurrency.Usdt]: 1 / displayCurrencyPerCent, [DisplayCurrency]: 1, }, } From fb752d4887e53f86cb90b48567f385785049b44d Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Sun, 7 Jun 2026 16:40:55 +0500 Subject: [PATCH 50/95] change getUsdtWallet to getCashWallet that returns usdt wallet if exist otherwise fallback to usd wallet --- app/graphql/wallets-utils.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/graphql/wallets-utils.ts b/app/graphql/wallets-utils.ts index 6dff22a5c..fa54eb52b 100644 --- a/app/graphql/wallets-utils.ts +++ b/app/graphql/wallets-utils.ts @@ -18,6 +18,23 @@ export const getUsdWallet = (wallets: readonly WalletBalance[] | undefined) => { return wallets.find((wallet) => wallet.walletCurrency === WalletCurrency.Usd) } +export const getCashWallet = (wallets: readonly WalletBalance[] | undefined) => { + if (wallets === undefined || wallets.length === 0) { + return undefined + } + + const usdtWallet = wallets.find( + (wallet) => wallet.walletCurrency === WalletCurrency.Usdt, + ) + if (usdtWallet) { + return usdtWallet + } + + const usdWallet = wallets.find((wallet) => wallet.walletCurrency === WalletCurrency.Usd) + + return usdWallet +} + export const getDefaultWallet = ( wallets: readonly WalletBalance[] | undefined, defaultWalletId: string | undefined, From f47cd2ba027e3be2689f0cbfd8200a1d02fcf170 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Sun, 7 Jun 2026 16:44:15 +0500 Subject: [PATCH 51/95] update UI Components to support USDT wallet --- app/components/amount-input/amount-input.tsx | 12 +++++++----- .../icon-transactions/icon-transactions.tsx | 1 + app/components/receive-screen/OnChainCharge.tsx | 3 ++- app/components/receive-screen/WalletBottomSheet.tsx | 8 ++++++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/app/components/amount-input/amount-input.tsx b/app/components/amount-input/amount-input.tsx index 407b4f50d..a4221ad8b 100644 --- a/app/components/amount-input/amount-input.tsx +++ b/app/components/amount-input/amount-input.tsx @@ -55,8 +55,9 @@ export const AmountInput: React.FC = ({ let formattedSecondaryAmount = undefined if (isNonZeroMoneyAmount(unitOfAccountAmount)) { - const isBtcDenominatedUsdWalletAmount = - walletCurrency === WalletCurrency.Usd && + const isBtcDenominatedCashWalletAmount = + (walletCurrency === WalletCurrency.Usd || + walletCurrency === WalletCurrency.Usdt) && unitOfAccountAmount.currency === WalletCurrency.Btc const primaryAmount = convertMoneyAmount(unitOfAccountAmount, DisplayCurrency) @@ -73,7 +74,7 @@ export const AmountInput: React.FC = ({ formattedPrimaryAmount = formatMoneyAmount({ moneyAmount: primaryAmount, - isApproximate: isBtcDenominatedUsdWalletAmount && !secondaryAmount, + isApproximate: isBtcDenominatedCashWalletAmount && !secondaryAmount, }) formattedSecondaryAmount = @@ -81,8 +82,9 @@ export const AmountInput: React.FC = ({ formatMoneyAmount({ moneyAmount: secondaryAmount, isApproximate: - isBtcDenominatedUsdWalletAmount && - secondaryAmount.currency === WalletCurrency.Usd, + isBtcDenominatedCashWalletAmount && + (secondaryAmount.currency === WalletCurrency.Usd || + secondaryAmount.currency === WalletCurrency.Usdt), }) } diff --git a/app/components/icon-transactions/icon-transactions.tsx b/app/components/icon-transactions/icon-transactions.tsx index b8031b971..b9a9e134c 100644 --- a/app/components/icon-transactions/icon-transactions.tsx +++ b/app/components/icon-transactions/icon-transactions.tsx @@ -28,6 +28,7 @@ export const IconTransaction: React.FC = ({ if (onChain && !pending) return return case WalletCurrency.Usd: + case WalletCurrency.Usdt: return default: return diff --git a/app/components/receive-screen/OnChainCharge.tsx b/app/components/receive-screen/OnChainCharge.tsx index d2c1559d0..be77e6248 100644 --- a/app/components/receive-screen/OnChainCharge.tsx +++ b/app/components/receive-screen/OnChainCharge.tsx @@ -15,7 +15,8 @@ const OnChainCharge: React.FC = ({ request }) => { const { LL } = useI18nContext() if ( - request.receivingWalletDescriptor.currency === "USD" && + (request.receivingWalletDescriptor.currency === "USD" || + request.receivingWalletDescriptor.currency === "USDT") && request.feesInformation?.deposit.minBankFee && request.feesInformation?.deposit.minBankFeeThreshold && request.type === Invoice.OnChain diff --git a/app/components/receive-screen/WalletBottomSheet.tsx b/app/components/receive-screen/WalletBottomSheet.tsx index b845cf03e..6f65d2dca 100644 --- a/app/components/receive-screen/WalletBottomSheet.tsx +++ b/app/components/receive-screen/WalletBottomSheet.tsx @@ -10,13 +10,15 @@ import { useI18nContext } from "@app/i18n/i18n-react" import Cash from "@app/assets/icons/cash.svg" import Bitcoin from "@app/assets/icons/bitcoin.svg" -const icons = { +const icons: Record = { USD: Cash, + USDT: Cash, BTC: Bitcoin, } const wallets = [ { title: "Cash", key: "USD" }, + { title: "Cash", key: "USDT" }, { title: "Bitcoin", key: "BTC" }, ] @@ -49,7 +51,9 @@ const WalletBottomSheet: React.FC = ({ currency, disabled, onChange }) => > - {currency === "USD" ? "Cash" : "Bitcoin"} + + {currency === "USD" || currency === "USDT" ? "Cash" : "Bitcoin"} + Date: Sun, 7 Jun 2026 16:47:53 +0500 Subject: [PATCH 52/95] update Payment Flows (interlader, lightning and onchain) to support USDT wallet --- .../payment-details/intraledger.ts | 3 ++- .../payment-details/lightning.ts | 3 ++- .../payment-details/onchain.ts | 15 +++++++++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/screens/send-bitcoin-screen/payment-details/intraledger.ts b/app/screens/send-bitcoin-screen/payment-details/intraledger.ts index ecee175ee..ea3f56c3e 100644 --- a/app/screens/send-bitcoin-screen/payment-details/intraledger.ts +++ b/app/screens/send-bitcoin-screen/payment-details/intraledger.ts @@ -81,7 +81,8 @@ export const createIntraledgerPaymentDetails = ( } } else if ( settlementAmount.amount && - sendingWalletDescriptor.currency === WalletCurrency.Usd + (sendingWalletDescriptor.currency === WalletCurrency.Usd || + sendingWalletDescriptor.currency === WalletCurrency.Usdt) ) { const sendPaymentMutation: SendPaymentMutation = async (paymentMutations) => { const { data } = await paymentMutations.intraLedgerUsdPaymentSend({ diff --git a/app/screens/send-bitcoin-screen/payment-details/lightning.ts b/app/screens/send-bitcoin-screen/payment-details/lightning.ts index 0ca8d974e..ac4094a7f 100644 --- a/app/screens/send-bitcoin-screen/payment-details/lightning.ts +++ b/app/screens/send-bitcoin-screen/payment-details/lightning.ts @@ -114,7 +114,8 @@ export const createNoAmountLightningPaymentDetails = ( } } else if ( settlementAmount?.amount && - sendingWalletDescriptor.currency === WalletCurrency.Usd + (sendingWalletDescriptor.currency === WalletCurrency.Usd || + sendingWalletDescriptor.currency === WalletCurrency.Usdt) ) { const getFee: GetFee = async (getFeeFns) => { const { data } = await getFeeFns.lnNoAmountUsdInvoiceFeeProbe({ diff --git a/app/screens/send-bitcoin-screen/payment-details/onchain.ts b/app/screens/send-bitcoin-screen/payment-details/onchain.ts index f6da684f6..d518f0bd5 100644 --- a/app/screens/send-bitcoin-screen/payment-details/onchain.ts +++ b/app/screens/send-bitcoin-screen/payment-details/onchain.ts @@ -88,7 +88,10 @@ export const createNoAmountOnchainPaymentDetails = ( return { amount, } - } else if (sendingWalletDescriptor.currency === WalletCurrency.Usd) { + } else if ( + sendingWalletDescriptor.currency === WalletCurrency.Usd || + sendingWalletDescriptor.currency === WalletCurrency.Usdt + ) { const { data } = await getFeeFns.onChainUsdTxFee({ variables: { walletId: sendingWalletDescriptor.id, @@ -173,12 +176,16 @@ export const createNoAmountOnchainPaymentDetails = ( } } else if ( settlementAmount.amount && - sendingWalletDescriptor.currency === WalletCurrency.Usd + (sendingWalletDescriptor.currency === WalletCurrency.Usd || + sendingWalletDescriptor.currency === WalletCurrency.Usdt) ) { let sendPaymentMutation: SendPaymentMutation let getFee: GetFee - if (settlementAmount.currency === WalletCurrency.Usd) { + if ( + settlementAmount.currency === WalletCurrency.Usd || + settlementAmount.currency === WalletCurrency.Usdt + ) { sendPaymentMutation = async (paymentMutations) => { const { data } = await paymentMutations.onChainUsdPaymentSend({ variables: { @@ -408,7 +415,7 @@ export const createAmountOnchainPaymentDetails = ( getFee, } } else { - // sendingWalletDescriptor.currency === WalletCurrency.Usd + // sendingWalletDescriptor.currency === WalletCurrency.Usd or WalletCurrency.Usdt console.log(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") console.log("Destination Specified Amount", destinationSpecifiedAmount) console.log("PARAMS:", { From a9cfcb449c5583dcf0e9d42c1eaa1db73b61d0e1 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Mon, 8 Jun 2026 09:52:35 +0500 Subject: [PATCH 53/95] update multiple screens to support usdt wallet --- app/components/modal-nfc/modal-nfc.tsx | 7 ++++-- .../ConfirmationDestinationAmountNote.tsx | 4 +++- .../send-flow/ConfirmationWalletFee.tsx | 5 ++++- app/components/send-flow/DetailAmountNote.tsx | 9 ++++++-- app/hooks/useSwap.ts | 6 ++--- .../conversion-details-screen.tsx | 6 ++--- app/screens/home-screen/home-screen.tsx | 3 ++- .../payment/payment-request.ts | 8 +++++-- .../receive-bitcoin-screen/receive-screen.tsx | 9 ++++++-- .../use-receive-bitcoin.ts | 12 ++++++---- .../redeem-bitcoin-detail-screen.tsx | 5 ++++- .../redeem-bitcoin-result-screen.tsx | 5 ++++- .../send-bitcoin-confirmation-screen.tsx | 22 ++++++------------- .../send-bitcoin-success-screen.tsx | 12 +++++----- 14 files changed, 70 insertions(+), 43 deletions(-) diff --git a/app/components/modal-nfc/modal-nfc.tsx b/app/components/modal-nfc/modal-nfc.tsx index 775d6adb7..59448f825 100644 --- a/app/components/modal-nfc/modal-nfc.tsx +++ b/app/components/modal-nfc/modal-nfc.tsx @@ -145,7 +145,7 @@ export const ModalNfc: React.FC<{ // TODO: add a loading icon because this call do a fetch() to an external server // and the response can be arbitrary long toggleActivityIndicator(true) - if (settlementAmount?.currency === "USD") { + if (settlementAmount?.currency === "USD" || settlementAmount?.currency === "USDT") { const destination = await parseDestination({ rawInput: lnurl, myWalletIds: wallets.map((wallet) => wallet.id), @@ -160,7 +160,10 @@ export const ModalNfc: React.FC<{ Alert.alert(LL.SettingsScreen.nfcOnlyReceive()) } else { let amount = settlementAmount.amount - if (settlementAmount.currency === WalletCurrency.Usd) { + if ( + settlementAmount.currency === WalletCurrency.Usd || + settlementAmount.currency === WalletCurrency.Usdt + ) { amount = convertMoneyAmount( toUsdMoneyAmount(settlementAmount.amount), WalletCurrency.Btc, diff --git a/app/components/send-flow/ConfirmationDestinationAmountNote.tsx b/app/components/send-flow/ConfirmationDestinationAmountNote.tsx index 6e065fbd8..430c75a46 100644 --- a/app/components/send-flow/ConfirmationDestinationAmountNote.tsx +++ b/app/components/send-flow/ConfirmationDestinationAmountNote.tsx @@ -59,7 +59,9 @@ const ConfirmationDestinationAmountNote: React.FC = ({ {LL.SendBitcoinScreen.amount()} = ({ const getSendingFee = async () => { setFee({ status: "loading", amount: undefined }) - if (sendingWalletDescriptor.currency === "USD") { + if ( + sendingWalletDescriptor.currency === "USD" || + sendingWalletDescriptor.currency === "USDT" + ) { setFee(getLightningFee) } else { const { fee, err } = await fetchBreezFee( diff --git a/app/components/send-flow/DetailAmountNote.tsx b/app/components/send-flow/DetailAmountNote.tsx index 7ad488d59..4252c99f3 100644 --- a/app/components/send-flow/DetailAmountNote.tsx +++ b/app/components/send-flow/DetailAmountNote.tsx @@ -60,7 +60,10 @@ const DetailAmountNote: React.FC = ({ const checkErrorMessage = () => { if (!convertMoneyAmount) return null - if (paymentDetail?.sendingWalletDescriptor.currency === "USD") { + if ( + paymentDetail?.sendingWalletDescriptor.currency === "USD" || + paymentDetail?.sendingWalletDescriptor.currency === "USDT" + ) { if (paymentDetail?.paymentType === "lnurl") { if ( paymentDetail.canSetAmount && @@ -162,7 +165,9 @@ const DetailAmountNote: React.FC = ({ { returnPartialData: true, }) - const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + const usdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) const btcBalance = toBtcMoneyAmount(btcWallet?.balance ?? NaN) const usdBalance = toUsdMoneyAmount(usdWallet?.balance ?? NaN) @@ -184,7 +184,7 @@ export const useSwap = () => { amount: number, ) => { if (lnInvoice && usdWallet) { - if (fromWalletCurrency === "USD") { + if (fromWalletCurrency === "USD" || fromWalletCurrency === "USDT") { const res = await lnInvoicePaymentSend({ variables: { input: { diff --git a/app/screens/conversion-flow/conversion-details-screen.tsx b/app/screens/conversion-flow/conversion-details-screen.tsx index 537b124e5..edaf4b8ff 100644 --- a/app/screens/conversion-flow/conversion-details-screen.tsx +++ b/app/screens/conversion-flow/conversion-details-screen.tsx @@ -32,7 +32,7 @@ import { WalletOrDisplayCurrency, } from "@app/types/amounts" import { RootStackParamList } from "@app/navigation/stack-param-lists" -import { getUsdWallet } from "@app/graphql/wallets-utils" +import { getCashWallet } from "@app/graphql/wallets-utils" // gql import { @@ -67,7 +67,7 @@ export const ConversionDetailsScreen: React.FC = ({ navigation }) => { returnPartialData: true, }) - const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + const usdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) const btcBalance = toBtcMoneyAmount(btcWallet?.balance ?? NaN) const usdBalance = toUsdMoneyAmount(usdWallet?.balance ?? NaN) @@ -111,7 +111,7 @@ export const ConversionDetailsScreen: React.FC = ({ navigation }) => { const moveToNextScreen = async () => { toggleActivityIndicator(true) const { data, err } = - fromWalletCurrency === "USD" + fromWalletCurrency === "USD" || fromWalletCurrency === "USDT" ? await prepareUsdToBtc(settlementSendAmount) : await prepareBtcToUsd(settlementSendAmount) diff --git a/app/screens/home-screen/home-screen.tsx b/app/screens/home-screen/home-screen.tsx index 0c59c5e11..28571f162 100644 --- a/app/screens/home-screen/home-screen.tsx +++ b/app/screens/home-screen/home-screen.tsx @@ -83,7 +83,8 @@ export const HomeScreen: React.FC = () => { ) if ( !persistentState.defaultWallet || - (persistentState.defaultWallet.walletCurrency === "USD" && + ((persistentState.defaultWallet.walletCurrency === "USD" || + persistentState.defaultWallet.walletCurrency === "USDT") && persistentState.defaultWallet.id !== defaultWallet?.id) ) { updateState((state: any) => { diff --git a/app/screens/receive-bitcoin-screen/payment/payment-request.ts b/app/screens/receive-bitcoin-screen/payment/payment-request.ts index a32204a43..d8ba18cd0 100644 --- a/app/screens/receive-bitcoin-screen/payment/payment-request.ts +++ b/app/screens/receive-bitcoin-screen/payment/payment-request.ts @@ -115,9 +115,13 @@ export const createPaymentRequest = ( info = generateOnChainInfo(res.paymentRequest, [], []) } } else { - // Handle USD payment requests + // Handle USD/USDT payment requests if (pr.type === Invoice.Lightning) { - if (pr.settlementAmount && pr.settlementAmount?.currency === WalletCurrency.Usd) { + if ( + pr.settlementAmount && + (pr.settlementAmount?.currency === WalletCurrency.Usd || + pr.settlementAmount?.currency === WalletCurrency.Usdt) + ) { console.log("Invoice create amount: ", pr.settlementAmount.amount) const { data, errors } = await mutations.lnUsdInvoiceCreate({ variables: { diff --git a/app/screens/receive-bitcoin-screen/receive-screen.tsx b/app/screens/receive-bitcoin-screen/receive-screen.tsx index 951ec34f4..8f94bc3f8 100644 --- a/app/screens/receive-bitcoin-screen/receive-screen.tsx +++ b/app/screens/receive-bitcoin-screen/receive-screen.tsx @@ -51,7 +51,11 @@ const ReceiveScreen = () => { const lnAddressHostname = appConfig.galoyInstance.lnAddressHostname const wallets: any = data?.me?.defaultAccount.wallets - const usdWallet = wallets?.find((el: any) => el.walletCurrency === WalletCurrency.Usd) + const usdWallet = wallets?.find( + (el: any) => + el.walletCurrency === WalletCurrency.Usdt || + el.walletCurrency === WalletCurrency.Usd, + ) useEffect(() => { if ( @@ -72,7 +76,8 @@ const ReceiveScreen = () => { if ( request && request.type === "PayCode" && - request.receivingWalletDescriptor.currency === "USD" && + (request.receivingWalletDescriptor.currency === "USD" || + request.receivingWalletDescriptor.currency === "USDT") && Boolean(usdWallet?.lnurlp) && Boolean(userData.username) ) { diff --git a/app/screens/receive-bitcoin-screen/use-receive-bitcoin.ts b/app/screens/receive-bitcoin-screen/use-receive-bitcoin.ts index 356c4f4fc..9fbc0c380 100644 --- a/app/screens/receive-bitcoin-screen/use-receive-bitcoin.ts +++ b/app/screens/receive-bitcoin-screen/use-receive-bitcoin.ts @@ -24,7 +24,7 @@ import { useAppConfig, useBreez, usePriceConversion } from "@app/hooks" import Clipboard from "@react-native-clipboard/clipboard" import { gql } from "@apollo/client" import { useIsAuthed } from "@app/graphql/is-authed-context" -import { getUsdWallet } from "@app/graphql/wallets-utils" +import { getCashWallet } from "@app/graphql/wallets-utils" import { createPaymentRequest } from "./payment/payment-request" import { MoneyAmount, WalletOrDisplayCurrency } from "@app/types/amounts" import { useLnUpdateHashPaid } from "@app/graphql/ln-update-context" @@ -162,7 +162,7 @@ export const useReceiveBitcoin = (initPRParams = {}) => { const defaultWallet = persistentState.defaultWallet - const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + const usdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) const username = data?.me?.username @@ -445,10 +445,14 @@ export const useReceiveBitcoin = (initPRParams = {}) => { id: btcWallet.id, currency: WalletCurrency.Btc, }) - } else if (walletCurrency === WalletCurrency.Usd && usdWallet) { + } else if ( + (walletCurrency === WalletCurrency.Usd || + walletCurrency === WalletCurrency.Usdt) && + usdWallet + ) { return pr.setReceivingWalletDescriptor({ id: usdWallet.id, - currency: WalletCurrency.Usd, + currency: usdWallet.walletCurrency, }) } } diff --git a/app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-detail-screen.tsx b/app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-detail-screen.tsx index f87d5f5b0..3a74c5683 100644 --- a/app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-detail-screen.tsx +++ b/app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-detail-screen.tsx @@ -71,7 +71,10 @@ const RedeemBitcoinDetailScreen: React.FC = ({ route, navigation }) => { btcMoneyAmount.amount >= minSats.amount useEffect(() => { - if (persistentState.defaultWallet?.walletCurrency === WalletCurrency.Usd) { + if ( + persistentState.defaultWallet?.walletCurrency === WalletCurrency.Usd || + persistentState.defaultWallet?.walletCurrency === WalletCurrency.Usdt + ) { calculateEstimatedFee() navigation.setOptions({ title: LL.RedeemBitcoinScreen.usdTitle() }) } else if (persistentState.defaultWallet?.walletCurrency === WalletCurrency.Btc) { diff --git a/app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-result-screen.tsx b/app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-result-screen.tsx index 58fad61a5..fd7e3973d 100644 --- a/app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-result-screen.tsx +++ b/app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-result-screen.tsx @@ -56,7 +56,10 @@ const RedeemBitcoinResultScreen: React.FC = ({ route, navigation }) => { const [success, setSuccess] = useState(false) useEffect(() => { - if (receivingWalletDescriptor.currency === WalletCurrency.Usd) { + if ( + receivingWalletDescriptor.currency === WalletCurrency.Usd || + receivingWalletDescriptor.currency === WalletCurrency.Usdt + ) { navigation.setOptions({ title: LL.RedeemBitcoinScreen.usdTitle() }) } else if (receivingWalletDescriptor.currency === WalletCurrency.Btc) { navigation.setOptions({ title: LL.RedeemBitcoinScreen.title() }) diff --git a/app/screens/send-bitcoin-screen/send-bitcoin-confirmation-screen.tsx b/app/screens/send-bitcoin-screen/send-bitcoin-confirmation-screen.tsx index 8a612f181..4a8320b92 100644 --- a/app/screens/send-bitcoin-screen/send-bitcoin-confirmation-screen.tsx +++ b/app/screens/send-bitcoin-screen/send-bitcoin-confirmation-screen.tsx @@ -42,7 +42,7 @@ import { RootStackParamList } from "@app/navigation/stack-param-lists" // utils import { logPaymentAttempt, logPaymentResult } from "@app/utils/analytics" -import { getUsdWallet } from "@app/graphql/wallets-utils" +import { getCashWallet } from "@app/graphql/wallets-utils" import { useChatContext } from "../chat/chatContext" import { addToContactList } from "@app/utils/nostr" import { getSigner } from "@app/nostr/signer" @@ -81,7 +81,7 @@ const SendBitcoinConfirmationScreen: React.FC = ({ route, navigation }) = useRequireContactList() const { data } = useSendBitcoinConfirmationScreenQuery({ skip: !useIsAuthed() }) - const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + const usdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) const { loading: sendPaymentLoading, @@ -182,21 +182,11 @@ const SendBitcoinConfirmationScreen: React.FC = ({ route, navigation }) = } const hexPubkey = nip19.decode(destinationNpub).data as string - await addToContactList( - signer, - hexPubkey, - promptForContactList, - contactsEvent, - ) + await addToContactList(signer, hexPubkey, promptForContactList, contactsEvent) } catch (err) { console.warn("Failed to auto-add flash user to contacts", err) } - }, [ - flashUserAddress, - npubByUsernameQuery, - promptForContactList, - contactsEvent, - ]) + }, [flashUserAddress, npubByUsernameQuery, promptForContactList, contactsEvent]) const handleSendPayment = useCallback(async () => { if (sendPayment && sendingWalletDescriptor?.currency) { @@ -219,7 +209,9 @@ const SendBitcoinConfirmationScreen: React.FC = ({ route, navigation }) = navigation.navigate("sendBitcoinSuccess", { walletCurrency: sendingWalletDescriptor.currency, unitOfAccountAmount: - sendingWalletDescriptor.currency === "USD" && invoiceAmount + (sendingWalletDescriptor.currency === "USD" || + sendingWalletDescriptor.currency === "USDT") && + invoiceAmount ? invoiceAmount : paymentDetail.unitOfAccountAmount, onSuccessAddContact: autoAddContact, diff --git a/app/screens/send-bitcoin-screen/send-bitcoin-success-screen.tsx b/app/screens/send-bitcoin-screen/send-bitcoin-success-screen.tsx index 16ad639c8..540e428ee 100644 --- a/app/screens/send-bitcoin-screen/send-bitcoin-success-screen.tsx +++ b/app/screens/send-bitcoin-screen/send-bitcoin-success-screen.tsx @@ -112,8 +112,9 @@ const SendBitcoinSuccessScreen: React.FC = ({ navigation, route }) => { }, []) if (isNonZeroMoneyAmount(unitOfAccountAmount)) { - const isBtcDenominatedUsdWalletAmount = - walletCurrency === WalletCurrency.Usd && + const isBtcDenominatedCashWalletAmount = + (walletCurrency === WalletCurrency.Usd || + walletCurrency === WalletCurrency.Usdt) && unitOfAccountAmount.currency === WalletCurrency.Btc const primaryAmount = convertMoneyAmount(unitOfAccountAmount, DisplayCurrency) @@ -130,7 +131,7 @@ const SendBitcoinSuccessScreen: React.FC = ({ navigation, route }) => { formattedPrimaryAmount = formatMoneyAmount({ moneyAmount: primaryAmount, - isApproximate: isBtcDenominatedUsdWalletAmount && !secondaryAmount, + isApproximate: isBtcDenominatedCashWalletAmount && !secondaryAmount, }) formattedSecondaryAmount = @@ -138,8 +139,9 @@ const SendBitcoinSuccessScreen: React.FC = ({ navigation, route }) => { formatMoneyAmount({ moneyAmount: secondaryAmount, isApproximate: - isBtcDenominatedUsdWalletAmount && - secondaryAmount.currency === WalletCurrency.Usd, + isBtcDenominatedCashWalletAmount && + (secondaryAmount.currency === WalletCurrency.Usd || + secondaryAmount.currency === WalletCurrency.Usdt), }) } From 462106ef5462c84339acd853fb3379e8b2e66f41 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Mon, 8 Jun 2026 09:55:37 +0500 Subject: [PATCH 54/95] replace getUsdWallet with getCashWallet method --- .../amount-input-screen-ui.tsx | 4 ++-- .../set-default-account-modal.tsx | 4 ++-- .../wallet-overview/wallet-overview.tsx | 4 ++-- .../conversion-confirmation-screen.tsx | 4 ++-- .../send-bitcoin-details-screen.tsx | 6 ++--- .../settings-screen/account-screen.tsx | 4 ++-- .../account/settings/delete.tsx | 4 ++-- .../show-warning-secure-account-hook.ts | 4 ++-- .../settings-screen/default-wallet.tsx | 10 ++++++--- .../settings/advanced-export-csv.tsx | 22 ++++++++----------- .../settings/advanced-mode-toggle.tsx | 4 ++-- .../show-warning-secure-account.tsx | 4 ++-- .../CashoutConfirmation.tsx | 4 ++-- .../topup-cashout-flow/CashoutDetails.tsx | 4 ++-- 14 files changed, 41 insertions(+), 41 deletions(-) diff --git a/app/components/amount-input-screen/amount-input-screen-ui.tsx b/app/components/amount-input-screen/amount-input-screen-ui.tsx index a8e77ac98..5f95aacd7 100644 --- a/app/components/amount-input-screen/amount-input-screen-ui.tsx +++ b/app/components/amount-input-screen/amount-input-screen-ui.tsx @@ -19,7 +19,7 @@ import Sync from "@app/assets/icons/sync.svg" // utils import { toBtcMoneyAmount, toUsdMoneyAmount } from "@app/types/amounts" -import { getUsdWallet } from "@app/graphql/wallets-utils" +import { getCashWallet } from "@app/graphql/wallets-utils" export type AmountInputScreenUIProps = { walletCurrency: WalletCurrency @@ -68,7 +68,7 @@ export const AmountInputScreenUI: React.FC = ({ moneyAmount: toBtcMoneyAmount(btcWallet?.balance ?? 0), }) } - const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + const usdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) return moneyAmountToDisplayCurrencyString({ moneyAmount: toUsdMoneyAmount(usdWallet?.balance ?? 0), }) diff --git a/app/components/set-default-account-modal/set-default-account-modal.tsx b/app/components/set-default-account-modal/set-default-account-modal.tsx index bda4f3e36..0170b8d3a 100644 --- a/app/components/set-default-account-modal/set-default-account-modal.tsx +++ b/app/components/set-default-account-modal/set-default-account-modal.tsx @@ -17,7 +17,7 @@ import { import { usePersistentStateContext } from "@app/store/persistent-state" // utils -import { getUsdWallet } from "@app/graphql/wallets-utils" +import { getCashWallet } from "@app/graphql/wallets-utils" import { setHasPromptedSetDefaultAccount } from "@app/graphql/client-only-query" // assets @@ -42,7 +42,7 @@ export const SetDefaultAccountModal = ({ isVisible, toggleModal }: Props) => { const { data } = useSetDefaultAccountModalQuery({ fetchPolicy: "cache-only", }) - const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + const usdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) const onPressHandler = async (currency: string) => { let defaultWallet = usdWallet diff --git a/app/components/wallet-overview/wallet-overview.tsx b/app/components/wallet-overview/wallet-overview.tsx index 97dbcb318..370ed322d 100644 --- a/app/components/wallet-overview/wallet-overview.tsx +++ b/app/components/wallet-overview/wallet-overview.tsx @@ -17,7 +17,7 @@ import { useBreez, useFlashcard } from "@app/hooks" // utils import { toBtcMoneyAmount, toUsdMoneyAmount } from "@app/types/amounts" -import { getUsdWallet } from "@app/graphql/wallets-utils" +import { getCashWallet } from "@app/graphql/wallets-utils" type Props = { setIsUnverifiedSeedModalVisible: (value: boolean) => void @@ -91,7 +91,7 @@ const WalletOverview: React.FC = ({ setIsUnverifiedSeedModalVisible }) => ) if (data) { - const extUsdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + const extUsdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) const extUsdWalletBalance = toUsdMoneyAmount(extUsdWallet?.balance ?? NaN) setCashBalance( formatMoneyAmount({ diff --git a/app/screens/conversion-flow/conversion-confirmation-screen.tsx b/app/screens/conversion-flow/conversion-confirmation-screen.tsx index feba73542..df15b6b93 100644 --- a/app/screens/conversion-flow/conversion-confirmation-screen.tsx +++ b/app/screens/conversion-flow/conversion-confirmation-screen.tsx @@ -17,7 +17,7 @@ import { useActivityIndicator, useBreez, useSwap } from "@app/hooks" // utils import { toastShow } from "@app/utils/toast" -import { getUsdWallet } from "@app/graphql/wallets-utils" +import { getCashWallet } from "@app/graphql/wallets-utils" // types import { RootStackParamList } from "@app/navigation/stack-param-lists" @@ -42,7 +42,7 @@ export const ConversionConfirmationScreen: React.FC = ({ navigation, rout returnPartialData: true, }) - const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + const usdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) const convertHandler = async () => { if (lnInvoice) { diff --git a/app/screens/send-bitcoin-screen/send-bitcoin-details-screen.tsx b/app/screens/send-bitcoin-screen/send-bitcoin-details-screen.tsx index 174865e19..1034ccb4f 100644 --- a/app/screens/send-bitcoin-screen/send-bitcoin-details-screen.tsx +++ b/app/screens/send-bitcoin-screen/send-bitcoin-details-screen.tsx @@ -23,7 +23,7 @@ import { WalletCurrency, } from "@app/graphql/generated" import { decodeInvoiceString, Network as NetworkLibGaloy } from "@galoymoney/client" -import { getInternalWallets, getUsdWallet } from "@app/graphql/wallets-utils" +import { getCashWallet } from "@app/graphql/wallets-utils" // hooks import { useIsAuthed } from "@app/graphql/is-authed-context" @@ -94,7 +94,7 @@ const SendBitcoinDetailsScreen: React.FC = ({ navigation, route }) => { const defaultWallet = persistentState.defaultWallet - const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + const usdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) const usdBalanceMoneyAmount = toUsdMoneyAmount(usdWallet?.balance) const btcBalanceMoneyAmount = toBtcMoneyAmount(btcWallet.balance || btcWallet?.balance) @@ -382,7 +382,7 @@ const SendBitcoinDetailsScreen: React.FC = ({ navigation, route }) => { > diff --git a/app/screens/settings-screen/account-screen.tsx b/app/screens/settings-screen/account-screen.tsx index 917604702..baa727b27 100644 --- a/app/screens/settings-screen/account-screen.tsx +++ b/app/screens/settings-screen/account-screen.tsx @@ -24,7 +24,7 @@ import { GaloyPrimaryButton } from "@app/components/atomic/galoy-primary-button" import Modal from "react-native-modal" import { CONTACT_EMAIL_ADDRESS } from "@app/config" import { GaloySecondaryButton } from "@app/components/atomic/galoy-secondary-button" -import { getUsdWallet } from "@app/graphql/wallets-utils" +import { getCashWallet } from "@app/graphql/wallets-utils" import { useNavigation } from "@react-navigation/native" import { useAppConfig, useBreez } from "@app/hooks" import useNostrProfile from "@app/hooks/use-nostr-profile" @@ -153,7 +153,7 @@ export const AccountScreen = () => { const [setEmailMutation] = useUserEmailRegistrationInitiateMutation() - const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + const usdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) const usdWalletBalance = toUsdMoneyAmount(usdWallet?.balance) const btcWalletBalance = toBtcMoneyAmount(btcWallet?.balance) diff --git a/app/screens/settings-screen/account/settings/delete.tsx b/app/screens/settings-screen/account/settings/delete.tsx index 005600f8d..c88d89a26 100644 --- a/app/screens/settings-screen/account/settings/delete.tsx +++ b/app/screens/settings-screen/account/settings/delete.tsx @@ -23,7 +23,7 @@ import { useAccountDeleteMutation, useSettingsScreenQuery } from "@app/graphql/g // utils import { CONTACT_EMAIL_ADDRESS } from "@app/config" -import { getUsdWallet } from "@app/graphql/wallets-utils" +import { getCashWallet } from "@app/graphql/wallets-utils" import { toBtcMoneyAmount, toUsdMoneyAmount } from "@app/types/amounts" export const Delete = () => { @@ -43,7 +43,7 @@ export const Delete = () => { const [deleteAccount] = useAccountDeleteMutation({ fetchPolicy: "no-cache" }) const { data, loading } = useSettingsScreenQuery() - const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + const usdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) const usdWalletBalance = toUsdMoneyAmount(usdWallet?.balance) const btcWalletBalance = toBtcMoneyAmount(btcWallet?.balance) diff --git a/app/screens/settings-screen/account/show-warning-secure-account-hook.ts b/app/screens/settings-screen/account/show-warning-secure-account-hook.ts index 8c738ff4e..f5631d5cc 100644 --- a/app/screens/settings-screen/account/show-warning-secure-account-hook.ts +++ b/app/screens/settings-screen/account/show-warning-secure-account-hook.ts @@ -1,7 +1,7 @@ import { gql } from "@apollo/client" import { useWarningSecureAccountQuery } from "@app/graphql/generated" import { useIsAuthed } from "@app/graphql/is-authed-context" -import { getUsdWallet } from "@app/graphql/wallets-utils" +import { getCashWallet } from "@app/graphql/wallets-utils" import { useBreez, usePriceConversion } from "@app/hooks" import { ZeroUsdMoneyAmount, @@ -43,7 +43,7 @@ export const useShowWarningSecureAccount = () => { if (data?.me?.defaultAccount?.level !== "ZERO") return false - const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + const usdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) const usdMoneyAmount = toUsdMoneyAmount(usdWallet?.balance) const btcMoneyAmount = toBtcMoneyAmount(btcWallet?.balance) diff --git a/app/screens/settings-screen/default-wallet.tsx b/app/screens/settings-screen/default-wallet.tsx index 907f35185..4cc40b0c1 100644 --- a/app/screens/settings-screen/default-wallet.tsx +++ b/app/screens/settings-screen/default-wallet.tsx @@ -12,7 +12,7 @@ import { Screen } from "../../components/screen" import { testProps } from "../../utils/testProps" import { GaloyInfo } from "@app/components/atomic/galoy-info" import { MenuSelect, MenuSelectItem } from "@app/components/menu-select" -import { getUsdWallet } from "@app/graphql/wallets-utils" +import { getCashWallet } from "@app/graphql/wallets-utils" import { useBreez } from "@app/hooks" import { usePersistentStateContext } from "@app/store/persistent-state" @@ -66,7 +66,7 @@ export const DefaultWalletScreen: React.FC = () => { }) const [updateDefaultWalletId] = useAccountUpdateDefaultWalletIdMutation() - const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + const usdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) const btcWalletId = btcWallet?.id const usdWalletId = usdWallet?.id @@ -86,7 +86,11 @@ export const DefaultWalletScreen: React.FC = () => { return {"Loading BTC account..."} } - return {"Tap to retry BTC account setup"} + return ( + + {"Tap to retry BTC account setup"} + + ) } const handleSetDefaultWallet = async (id: string) => { diff --git a/app/screens/settings-screen/settings/advanced-export-csv.tsx b/app/screens/settings-screen/settings/advanced-export-csv.tsx index ce9f9d7ee..b3bedb04a 100644 --- a/app/screens/settings-screen/settings/advanced-export-csv.tsx +++ b/app/screens/settings-screen/settings/advanced-export-csv.tsx @@ -7,7 +7,7 @@ import { useSettingsScreenQuery, useWalletCsvTransactionsLazyQuery, } from "@app/graphql/generated" -import { getUsdWallet } from "@app/graphql/wallets-utils" +import { getCashWallet } from "@app/graphql/wallets-utils" import { useI18nContext } from "@app/i18n/i18n-react" import { getCrashlytics } from "@react-native-firebase/crashlytics" @@ -31,7 +31,7 @@ export const ExportCsvSetting: React.FC = () => { const { btcWallet } = useBreez() const { data, loading } = useSettingsScreenQuery() - const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + const usdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) const btcWalletId = btcWallet?.id const usdWalletId = usdWallet?.id @@ -90,17 +90,13 @@ export const ExportCsvSetting: React.FC = () => { const filePath = `${RNFS.DownloadDirectoryPath}/flash-transactions.csv` await RNFS.writeFile(filePath, csvContent, "utf8") - Alert.alert( - "CSV Exported", - `Saved to Downloads/flash-transactions.csv`, - [ - { - text: "Share", - onPress: () => shareCsvFile(filePath), - }, - { text: "OK", style: "cancel" }, - ], - ) + Alert.alert("CSV Exported", `Saved to Downloads/flash-transactions.csv`, [ + { + text: "Share", + onPress: () => shareCsvFile(filePath), + }, + { text: "OK", style: "cancel" }, + ]) } } catch (err: unknown) { if (err instanceof Error) { diff --git a/app/screens/settings-screen/settings/advanced-mode-toggle.tsx b/app/screens/settings-screen/settings/advanced-mode-toggle.tsx index 21563e32e..970a34d91 100644 --- a/app/screens/settings-screen/settings/advanced-mode-toggle.tsx +++ b/app/screens/settings-screen/settings/advanced-mode-toggle.tsx @@ -8,7 +8,7 @@ import { toBtcMoneyAmount } from "@app/types/amounts" import { useDisplayCurrency } from "@app/hooks/use-display-currency" import { Alert, Platform } from "react-native" import { useSettingsScreenQuery } from "@app/graphql/generated" -import { getUsdWallet } from "@app/graphql/wallets-utils" +import { getCashWallet } from "@app/graphql/wallets-utils" import { useLevel } from "@app/graphql/level-context" import { usePersistentStateContext } from "@app/store/persistent-state" import { useBreez } from "@app/hooks" @@ -37,7 +37,7 @@ export const AdvancedModeToggle: React.FC = () => { skip: !isAtLeastLevelZero, }) - const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + const usdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) useEffect(() => { checkRecoveryPhrase() diff --git a/app/screens/settings-screen/show-warning-secure-account.tsx b/app/screens/settings-screen/show-warning-secure-account.tsx index de8952dfd..2f397bfca 100644 --- a/app/screens/settings-screen/show-warning-secure-account.tsx +++ b/app/screens/settings-screen/show-warning-secure-account.tsx @@ -1,7 +1,7 @@ import { gql } from "@apollo/client" import { useWarningSecureAccountQuery } from "@app/graphql/generated" import { useIsAuthed } from "@app/graphql/is-authed-context" -import { getUsdWallet } from "@app/graphql/wallets-utils" +import { getCashWallet } from "@app/graphql/wallets-utils" import { useBreez, usePriceConversion } from "@app/hooks" import { ZeroUsdMoneyAmount, @@ -43,7 +43,7 @@ export const useShowWarningSecureAccount = () => { if (data?.me?.defaultAccount.level !== "ZERO") return false - const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + const usdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) const usdMoneyAmount = toUsdMoneyAmount(usdWallet?.balance) const btcMoneyAmount = toBtcMoneyAmount(btcWallet?.balance) diff --git a/app/screens/topup-cashout-flow/CashoutConfirmation.tsx b/app/screens/topup-cashout-flow/CashoutConfirmation.tsx index c4e3c5e99..a7bbd8bb6 100644 --- a/app/screens/topup-cashout-flow/CashoutConfirmation.tsx +++ b/app/screens/topup-cashout-flow/CashoutConfirmation.tsx @@ -20,7 +20,7 @@ import { useActivityIndicator, useDisplayCurrency } from "@app/hooks" import { useCashoutScreenQuery, useInitiateCashoutMutation } from "@app/graphql/generated" //utils -import { getUsdWallet } from "@app/graphql/wallets-utils" +import { getCashWallet } from "@app/graphql/wallets-utils" import { toUsdMoneyAmount } from "@app/types/amounts" import { StackScreenProps } from "@react-navigation/stack" import { RootStackParamList } from "@app/navigation/stack-param-lists" @@ -44,7 +44,7 @@ const CashoutConfirmation: React.FC = ({ navigation, route }) => { returnPartialData: true, }) - const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + const usdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) const usdBalance = toUsdMoneyAmount(usdWallet?.balance ?? NaN) const { diff --git a/app/screens/topup-cashout-flow/CashoutDetails.tsx b/app/screens/topup-cashout-flow/CashoutDetails.tsx index 57fe1727a..b87d91e11 100644 --- a/app/screens/topup-cashout-flow/CashoutDetails.tsx +++ b/app/screens/topup-cashout-flow/CashoutDetails.tsx @@ -25,7 +25,7 @@ import { toWalletAmount, WalletOrDisplayCurrency, } from "@app/types/amounts" -import { getUsdWallet } from "@app/graphql/wallets-utils" +import { getCashWallet } from "@app/graphql/wallets-utils" import { View } from "react-native" import { PrimaryBtn } from "@app/components/buttons" import { useSafeAreaInsets } from "react-native-safe-area-context" @@ -60,7 +60,7 @@ const CashoutDetails = ({ navigation }: Props) => { return } - const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + const usdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) const usdBalance = toUsdMoneyAmount(usdWallet?.balance ?? NaN) const settlementSendAmount = convertMoneyAmount(moneyAmount, "USD") const isValidAmount = From f22c57e7681e204c968d500c247ac582c293caae Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Mon, 8 Jun 2026 09:59:47 +0500 Subject: [PATCH 55/95] set the header X-Flash-Client-Capabilities: cash-wallet-usdt-v1 to support usdt wallet schemas --- app/graphql/client.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/graphql/client.tsx b/app/graphql/client.tsx index 6c8930a0a..4d70605fe 100644 --- a/app/graphql/client.tsx +++ b/app/graphql/client.tsx @@ -198,6 +198,13 @@ const GaloyClient: React.FC = ({ children }) => { }, }) + const capabilitiesLink = setContext((_, { headers }) => ({ + headers: { + ...headers, + "X-Flash-Client-Capabilities": "cash-wallet-usdt-v1", + }, + })) + let authLink: ApolloLink if (token) { authLink = setContext((request, { headers }) => ({ @@ -245,6 +252,7 @@ const GaloyClient: React.FC = ({ children }) => { errorLink, retryLink, appCheckLink, + capabilitiesLink, authLink, retry401ErrorLink, persistedQueryLink, From 72238d8d223906022305a6bf39aed5792ea06143 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Tue, 9 Jun 2026 12:08:15 +0500 Subject: [PATCH 56/95] enable card top up for level one users --- .../topup-cashout-flow/TopupCashout.tsx | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/app/screens/topup-cashout-flow/TopupCashout.tsx b/app/screens/topup-cashout-flow/TopupCashout.tsx index d5ae8fd58..26da15199 100644 --- a/app/screens/topup-cashout-flow/TopupCashout.tsx +++ b/app/screens/topup-cashout-flow/TopupCashout.tsx @@ -117,20 +117,24 @@ const TopupCashout: React.FC = ({ navigation }) => { ) const checkAccountLevel = (type: "card" | "bankTransfer" | "cashout") => { - if (currentLevel === AccountLevel.Zero || currentLevel === AccountLevel.One) { - Alert.alert( - "Account upgrade required", - "You should upgrade your account to use this feature", - [ - { text: "Continue", onPress: () => navigation.navigate("AccountType") }, - { text: "Cancel", style: "cancel" }, - ], - ) + if (type === "card") { + navigation.navigate("TopupDetails", { paymentType: type }) } else { - if (type === "cashout") { - navigation.navigate("CashoutDetails") + if (currentLevel === AccountLevel.One) { + Alert.alert( + "Account upgrade required", + "You should upgrade your account to use this feature", + [ + { text: "Continue", onPress: () => navigation.navigate("AccountType") }, + { text: "Cancel", style: "cancel" }, + ], + ) } else { - navigation.navigate("TopupDetails", { paymentType: type }) + if (type === "cashout") { + navigation.navigate("CashoutDetails") + } else { + navigation.navigate("TopupDetails", { paymentType: type }) + } } } } From a921cbca923c99b13ecf045ea6eab80d9bf65a00 Mon Sep 17 00:00:00 2001 From: Patoo Date: Tue, 9 Jun 2026 10:44:23 -0400 Subject: [PATCH 57/95] Add International upgrade option (#633) * Add International upgrade option * fix: open bridge kyc modal from account type * clean up and open bridge kyc modal on the account upgrade type screen * clean up * Hide the international/bridge upgrade button when KYC is approved and Show orange border on the international card when KYC is pending * hide the international kyc button if user level zero --------- Co-authored-by: Vandana Co-authored-by: nodirbek75 --- app/i18n/en/index.ts | 2 + app/i18n/i18n-types.ts | 23 +++- app/i18n/raw-i18n/source/en.json | 11 +- app/navigation/stack-param-lists.ts | 1 - .../account-upgrade-flow/AccountType.tsx | 104 +++++++++++++++++- .../topup-cashout-flow/TopupCashout.tsx | 1 - 6 files changed, 128 insertions(+), 14 deletions(-) diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index feb0f03db..e6f22524b 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -1659,6 +1659,8 @@ const en: BaseTranslation = { proDesc: "Accept payments and get discovered on the map. Requires a business name and location.", merchant: "Merchant", merchantDesc: "Give rewards, appear on the map, and settle to your bank. ID and bank info required.", + international: "International", + internationalDesc: "Set up international transfers with Bridge KYC.", personalInfo: "Personal Information", fullName: "Full name", phoneNumber: "Phone Number", diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index a49084794..147911943 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -2063,15 +2063,15 @@ type RootTranslation = { */ backHome: string /** - * Virtual Bank Transfer + * V​i​r​t​u​a​l​ ​B​a​n​k​ ​T​r​a​n​s​f​e​r */ virtualBankTransfer: string /** - * Bank Name + * B​a​n​k​ ​N​a​m​e */ bankName: string /** - * Routing Number + * R​o​u​t​i​n​g​ ​N​u​m​b​e​r */ routingNumber: string } @@ -5513,6 +5513,14 @@ type RootTranslation = { * G​i​v​e​ ​r​e​w​a​r​d​s​,​ ​a​p​p​e​a​r​ ​o​n​ ​t​h​e​ ​m​a​p​,​ ​a​n​d​ ​s​e​t​t​l​e​ ​t​o​ ​y​o​u​r​ ​b​a​n​k​.​ ​I​D​ ​a​n​d​ ​b​a​n​k​ ​i​n​f​o​ ​r​e​q​u​i​r​e​d​. */ merchantDesc: string + /** + * I​n​t​e​r​n​a​t​i​o​n​a​l + */ + international: string + /** + * S​e​t​ ​u​p​ ​i​n​t​e​r​n​a​t​i​o​n​a​l​ ​t​r​a​n​s​f​e​r​s​ ​w​i​t​h​ ​B​r​i​d​g​e​ ​K​Y​C​. + */ + internationalDesc: string /** * P​e​r​s​o​n​a​l​ ​I​n​f​o​r​m​a​t​i​o​n */ @@ -7655,7 +7663,6 @@ export type TranslationFunctions = { * Routing Number */ routingNumber: () => LocalizedString - } PaymentSuccessScreen: { /** @@ -11016,6 +11023,14 @@ export type TranslationFunctions = { * Give rewards, appear on the map, and settle to your bank. ID and bank info required. */ merchantDesc: () => LocalizedString + /** + * International + */ + international: () => LocalizedString + /** + * Set up international transfers with Bridge KYC. + */ + internationalDesc: () => LocalizedString /** * Personal Information */ diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index 5687dd6b0..17128f6b0 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -612,10 +612,10 @@ "desc4": "After payment completion on your end you can send us an email to {email: string} with a screenshot of your payment confirmation.", "desc5": "Your payment will be processed even if we don't receive this email, but having this confirmation can help accelerate the order.", "backHome": "Back to Home", - "virtualBankTransfer": "Virtual Bank Transfer", - "bankName": "Bank Name", - "routingNumber": "Routing Number" - }, + "virtualBankTransfer": "Virtual Bank Transfer", + "bankName": "Bank Name", + "routingNumber": "Routing Number" + }, "PaymentSuccessScreen": { "title": "Payment Successful", "successMessage": "Your payment has been processed successfully", @@ -797,6 +797,7 @@ "amount": "Amount", "MinOnChainLimit": "Minimum amount for this transaction is US$2.00", "MinOnChainSatLimit": "Minimum amount for this transaction is 5,500 sats", + "MinFlashcardLimit": "Minimum amount when reloading from flashcard is 100 sats", "amountExceed": "Amount exceeds your balance of {balance: string}", "amountExceedsLimit": "Amount exceeds your remaining daily limit of {limit: string}", "upgradeAccountToIncreaseLimit": "Upgrade your account to increase your limit", @@ -1559,6 +1560,8 @@ "proDesc": "Accept payments and get discovered on the map. Requires a business name and location.", "merchant": "Merchant", "merchantDesc": "Give rewards, appear on the map, and settle to your bank. ID and bank info required.", + "international": "International", + "internationalDesc": "Set up international transfers with Bridge KYC.", "personalInfo": "Personal Information", "fullName": "Full name", "phoneNumber": "Phone Number", diff --git a/app/navigation/stack-param-lists.ts b/app/navigation/stack-param-lists.ts index ec8b643ca..424d5cef5 100644 --- a/app/navigation/stack-param-lists.ts +++ b/app/navigation/stack-param-lists.ts @@ -4,7 +4,6 @@ import { CashoutOffer, PhoneCodeChannelType, TransactionFragment, - Wallet, WalletCurrency, } from "@app/graphql/generated" import type { Payment, DepositInfo } from "@breeztech/breez-sdk-spark-react-native" diff --git a/app/screens/account-upgrade-flow/AccountType.tsx b/app/screens/account-upgrade-flow/AccountType.tsx index 2946d94fc..182ef6713 100644 --- a/app/screens/account-upgrade-flow/AccountType.tsx +++ b/app/screens/account-upgrade-flow/AccountType.tsx @@ -1,17 +1,23 @@ -import React, { useEffect } from "react" -import { TouchableOpacity, View } from "react-native" +import React, { useCallback, useState } from "react" +import { Alert, TouchableOpacity, View } from "react-native" import { StackScreenProps } from "@react-navigation/stack" import { Icon, makeStyles, Text, useTheme } from "@rneui/themed" import { RootStackParamList } from "@app/navigation/stack-param-lists" -import { AccountLevel } from "@app/graphql/generated" +import { + AccountLevel, + useBridgeInitiateKycMutation, + useBridgeKycStatusQuery, +} from "@app/graphql/generated" +import { useFocusEffect } from "@react-navigation/native" // components import { Screen } from "@app/components/screen" +import { BridgeKycModal } from "@app/components/topup-cashout-flow" import { ProgressSteps } from "@app/components/account-upgrade-flow" // hooks import { useLevel } from "@app/graphql/level-context" -import { useAccountUpgrade } from "@app/hooks" +import { useActivityIndicator } from "@app/hooks" import { useI18nContext } from "@app/i18n/i18n-react" // store @@ -26,6 +32,22 @@ const AccountType: React.FC = ({ navigation }) => { const { colors } = useTheme().theme const { LL } = useI18nContext() const { currentLevel } = useLevel() + const { toggleActivityIndicator } = useActivityIndicator() + + const [bridgeKycModalVisible, setBridgeKycModalVisible] = useState(false) + + const [initiateBridgeKyc] = useBridgeInitiateKycMutation() + const { data: kycStatusData, refetch: refetchKycStatus } = useBridgeKycStatusQuery({ + fetchPolicy: "cache-and-network", + }) + + useFocusEffect( + useCallback(() => { + refetchKycStatus() + }, [refetchKycStatus]), + ) + + const bridgeKycStatus = kycStatusData?.bridgeKycStatus const onPress = (accountType: string) => { const numOfSteps = @@ -35,6 +57,50 @@ const AccountType: React.FC = ({ navigation }) => { navigation.navigate("PersonalInformation") } + const checkBridgeKyc = () => { + if (bridgeKycStatus === "pending") { + Alert.alert("KYC Pending", "Your KYC status is pending. Please wait for approval.") + } else { + setBridgeKycModalVisible(true) + } + } + + const getBridgeKycLink = async (data: { + fullName: string + email: string + kycType: string + }) => { + toggleActivityIndicator(true) + try { + const res = await initiateBridgeKyc({ + variables: { + input: { + full_name: data.fullName, + email: data.email, + type: data.kycType, + }, + }, + }) + toggleActivityIndicator(false) + console.log("BRIDGE INITIATE KYC RESPONSE: ", res) + const errors = res.data?.bridgeInitiateKyc?.errors + if (errors && errors.length > 0) { + Alert.alert("Error", errors[0].message) + return + } + const kycLink = res.data?.bridgeInitiateKyc?.kycLink + if (kycLink?.tosLink && kycLink?.kycLink) { + navigation.navigate("BridgeKycWebView", { + tosLink: kycLink.tosLink, + kycLink: kycLink.kycLink, + }) + } + } catch (err) { + toggleActivityIndicator(false) + Alert.alert("Error", "Something went wrong. Please try again.") + } + } + const numOfSteps = currentLevel === AccountLevel.Zero ? 3 : 4 return ( @@ -80,6 +146,36 @@ const AccountType: React.FC = ({ navigation }) => { + {currentLevel !== AccountLevel.Zero && bridgeKycStatus !== "approved" && ( + + + + + {LL.AccountUpgrade.international()} + + + {LL.AccountUpgrade.internationalDesc()} + + + + + )} + setBridgeKycModalVisible(false)} + onSubmit={(data) => { + setBridgeKycModalVisible(false) + getBridgeKycLink(data) + }} + /> ) } diff --git a/app/screens/topup-cashout-flow/TopupCashout.tsx b/app/screens/topup-cashout-flow/TopupCashout.tsx index 26da15199..a072ae8da 100644 --- a/app/screens/topup-cashout-flow/TopupCashout.tsx +++ b/app/screens/topup-cashout-flow/TopupCashout.tsx @@ -230,7 +230,6 @@ const TopupCashout: React.FC = ({ navigation }) => { options={settleOptions} onClose={() => setSettleModalVisible(false)} /> - setBridgeKycModalVisible(false)} From 8bf9163b9b928bb668a307ddb6d17c57b1d4af5a Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Wed, 10 Jun 2026 12:19:43 +0500 Subject: [PATCH 58/95] hide and never display CashWalletCutover modal once user see it --- .../CashWalletCutoverModal.tsx | 19 ++++++++++++++----- .../persistent-state/state-migrations.ts | 1 + 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/components/topup-cashout-flow/CashWalletCutoverModal.tsx b/app/components/topup-cashout-flow/CashWalletCutoverModal.tsx index 3f1b34d1d..cba388905 100644 --- a/app/components/topup-cashout-flow/CashWalletCutoverModal.tsx +++ b/app/components/topup-cashout-flow/CashWalletCutoverModal.tsx @@ -1,18 +1,27 @@ -import React, { useState } from "react" +import React from "react" import { useWindowDimensions, View } from "react-native" import Modal from "react-native-modal" import { makeStyles, Text, useTheme } from "@rneui/themed" import { useI18nContext } from "@app/i18n/i18n-react" import { PrimaryBtn } from "@app/components/buttons" import DollarIllustration from "@app/assets/illustrations/dollar.svg" +import { usePersistentStateContext } from "@app/store/persistent-state" const CashWalletCutoverModal = () => { const { LL } = useI18nContext() const styles = useStyles() const { width } = useWindowDimensions() const { colors } = useTheme().theme + const { persistentState, updateState } = usePersistentStateContext() - const [visible, setVisible] = useState(false) + const visible = !persistentState.hasSeenCashWalletCutoverModal + + const dismiss = () => { + updateState((state) => { + if (!state) return state + return { ...state, hasSeenCashWalletCutoverModal: true } + }) + } const illustrationSize = Math.min(width * 0.35, 140) @@ -22,8 +31,8 @@ const CashWalletCutoverModal = () => { backdropOpacity={0.5} backdropColor={colors.black} backdropTransitionOutTiming={0} - onBackdropPress={() => setVisible(false)} - onSwipeComplete={() => setVisible(false)} + onBackdropPress={dismiss} + onSwipeComplete={dismiss} swipeDirection={["down"]} style={styles.modal} useNativeDriverForBackdrop @@ -47,7 +56,7 @@ const CashWalletCutoverModal = () => { setVisible(false)} + onPress={dismiss} /> diff --git a/app/store/persistent-state/state-migrations.ts b/app/store/persistent-state/state-migrations.ts index 579e3ba4a..babf4ba32 100644 --- a/app/store/persistent-state/state-migrations.ts +++ b/app/store/persistent-state/state-migrations.ts @@ -80,6 +80,7 @@ type PersistentState_7 = { flashcardHtml?: string hasPostedToNostr?: boolean // true if user has made at least one Nostr post sparkMigrationCompleted?: boolean + hasSeenCashWalletCutoverModal?: boolean // Featured profile view tracking featuredProfile?: { hasViewedProfile: boolean From 3c598d0eea33a4fb83a1945f093bd48760da7c2c Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Mon, 15 Jun 2026 13:17:12 +0500 Subject: [PATCH 59/95] Update CashWalletCutoverModal to show modal only for existing users and not show newly registered users after cutover --- .../CashWalletCutoverModal.tsx | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/app/components/topup-cashout-flow/CashWalletCutoverModal.tsx b/app/components/topup-cashout-flow/CashWalletCutoverModal.tsx index cba388905..aba306d3f 100644 --- a/app/components/topup-cashout-flow/CashWalletCutoverModal.tsx +++ b/app/components/topup-cashout-flow/CashWalletCutoverModal.tsx @@ -6,6 +6,11 @@ import { useI18nContext } from "@app/i18n/i18n-react" import { PrimaryBtn } from "@app/components/buttons" import DollarIllustration from "@app/assets/illustrations/dollar.svg" import { usePersistentStateContext } from "@app/store/persistent-state" +import { + CashWalletCutoverState, + useCashWalletCutoverQuery, + useHomeAuthedQuery, +} from "@app/graphql/generated" const CashWalletCutoverModal = () => { const { LL } = useI18nContext() @@ -13,8 +18,21 @@ const CashWalletCutoverModal = () => { const { width } = useWindowDimensions() const { colors } = useTheme().theme const { persistentState, updateState } = usePersistentStateContext() + const { data: cutoverData } = useCashWalletCutoverQuery() + const { data: meData } = useHomeAuthedQuery() - const visible = !persistentState.hasSeenCashWalletCutoverModal + const cutoverState = cutoverData?.cashWalletCutover?.state + const cutoverCompletedAt = cutoverData?.cashWalletCutover?.completedAt + const accountCreatedAt = meData?.me?.createdAt + + // Show modal only for accounts created before the cutover completed + const isOldAccount = + cutoverState === CashWalletCutoverState.Complete && + cutoverCompletedAt && + accountCreatedAt && + accountCreatedAt < cutoverCompletedAt + + const visible = !persistentState.hasSeenCashWalletCutoverModal && Boolean(isOldAccount) const dismiss = () => { updateState((state) => { @@ -54,10 +72,7 @@ const CashWalletCutoverModal = () => { - + From 2eb9d0bc1a74f20f77ff4c7866df1343761545a2 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Mon, 15 Jun 2026 13:18:34 +0500 Subject: [PATCH 60/95] bridge withdrawal mutation and queries are added --- app/graphql/front-end-mutations.ts | 51 ++++ app/graphql/front-end-queries.ts | 25 ++ app/graphql/generated.gql | 87 +++++++ app/graphql/generated.ts | 364 +++++++++++++++++++++++++++-- 4 files changed, 510 insertions(+), 17 deletions(-) diff --git a/app/graphql/front-end-mutations.ts b/app/graphql/front-end-mutations.ts index 6f0cc454d..b8b9cf8b6 100644 --- a/app/graphql/front-end-mutations.ts +++ b/app/graphql/front-end-mutations.ts @@ -233,4 +233,55 @@ gql` } } } + + mutation BridgeAddExternalAccount { + bridgeAddExternalAccount { + externalAccount { + expiresAt + linkUrl + } + errors { + code + message + } + } + } + + mutation BridgeRequestWithdrawal($input: BridgeRequestWithdrawalInput!) { + bridgeRequestWithdrawal(input: $input) { + withdrawal { + amount + createdAt + bridgeTransferId + currency + externalAccountId + failureReason + id + status + } + errors { + code + message + } + } + } + + mutation BridgeInitiateWithdrawal($input: BridgeInitiateWithdrawalInput!) { + bridgeInitiateWithdrawal(input: $input) { + withdrawal { + amount + bridgeTransferId + createdAt + currency + externalAccountId + failureReason + id + status + } + errors { + code + message + } + } + } ` diff --git a/app/graphql/front-end-queries.ts b/app/graphql/front-end-queries.ts index 081cb8462..145150e32 100644 --- a/app/graphql/front-end-queries.ts +++ b/app/graphql/front-end-queries.ts @@ -19,6 +19,7 @@ gql` language username phone + createdAt email { address verified @@ -362,4 +363,28 @@ gql` } } } + + query BridgeExternalAccounts { + bridgeExternalAccounts { + accountNumberLast4 + bankName + id + status + } + } + + query CashWalletCutover { + cashWalletCutover { + completedAt + cutoverVersion + pauseReason + pausedAt + runId + scheduledAt + startedAt + state + updatedAt + updatedBy + } + } ` diff --git a/app/graphql/generated.gql b/app/graphql/generated.gql index a3731d342..14d5d445a 100644 --- a/app/graphql/generated.gql +++ b/app/graphql/generated.gql @@ -82,6 +82,22 @@ fragment TransactionList on TransactionConnection { __typename } +mutation BridgeAddExternalAccount { + bridgeAddExternalAccount { + externalAccount { + expiresAt + linkUrl + __typename + } + errors { + code + message + __typename + } + __typename + } +} + mutation BridgeInitiateKyc($input: BridgeInitiateKycInput!) { bridgeInitiateKyc(input: $input) { errors { @@ -98,6 +114,50 @@ mutation BridgeInitiateKyc($input: BridgeInitiateKycInput!) { } } +mutation BridgeInitiateWithdrawal($input: BridgeInitiateWithdrawalInput!) { + bridgeInitiateWithdrawal(input: $input) { + withdrawal { + amount + bridgeTransferId + createdAt + currency + externalAccountId + failureReason + id + status + __typename + } + errors { + code + message + __typename + } + __typename + } +} + +mutation BridgeRequestWithdrawal($input: BridgeRequestWithdrawalInput!) { + bridgeRequestWithdrawal(input: $input) { + withdrawal { + amount + createdAt + bridgeTransferId + currency + externalAccountId + failureReason + id + status + __typename + } + errors { + code + message + __typename + } + __typename + } +} + mutation IdDocumentUploadUrlGenerate($input: IdDocumentUploadUrlGenerateInput!) { idDocumentUploadUrlGenerate(input: $input) { errors { @@ -909,6 +969,16 @@ query BankAccounts { } } +query BridgeExternalAccounts { + bridgeExternalAccounts { + accountNumberLast4 + bankName + id + status + __typename + } +} + query BridgeKycStatus { bridgeKycStatus } @@ -928,6 +998,22 @@ query BridgeVirtualAccount { } } +query CashWalletCutover { + cashWalletCutover { + completedAt + cutoverVersion + pauseReason + pausedAt + runId + scheduledAt + startedAt + state + updatedAt + updatedBy + __typename + } +} + query ExportCsvSetting($walletIds: [WalletId!]!) { me { id @@ -1223,6 +1309,7 @@ query homeAuthed { language username phone + createdAt email { address verified diff --git a/app/graphql/generated.ts b/app/graphql/generated.ts index 5398a0032..1ab551b27 100644 --- a/app/graphql/generated.ts +++ b/app/graphql/generated.ts @@ -325,7 +325,17 @@ export type BankAccountInput = { export type BridgeAddExternalAccountPayload = { readonly __typename: 'BridgeAddExternalAccountPayload'; readonly errors: ReadonlyArray; - readonly externalAccount?: Maybe; + readonly externalAccount?: Maybe; +}; + +export type BridgeCancelWithdrawalRequestInput = { + readonly withdrawalId: Scalars['ID']['input']; +}; + +export type BridgeCancelWithdrawalRequestPayload = { + readonly __typename: 'BridgeCancelWithdrawalRequestPayload'; + readonly errors: ReadonlyArray; + readonly withdrawal?: Maybe; }; export type BridgeCreateVirtualAccountPayload = { @@ -342,6 +352,12 @@ export type BridgeExternalAccount = { readonly status: Scalars['String']['output']; }; +export type BridgeExternalAccountLink = { + readonly __typename: 'BridgeExternalAccountLink'; + readonly expiresAt: Scalars['String']['output']; + readonly linkUrl: Scalars['String']['output']; +}; + export type BridgeInitiateKycInput = { readonly email?: InputMaybe; readonly full_name?: InputMaybe; @@ -355,8 +371,7 @@ export type BridgeInitiateKycPayload = { }; export type BridgeInitiateWithdrawalInput = { - readonly amount: Scalars['String']['input']; - readonly externalAccountId: Scalars['ID']['input']; + readonly withdrawalId: Scalars['ID']['input']; }; export type BridgeInitiateWithdrawalPayload = { @@ -371,6 +386,17 @@ export type BridgeKycLink = { readonly tosLink: Scalars['String']['output']; }; +export type BridgeRequestWithdrawalInput = { + readonly amount: Scalars['String']['input']; + readonly externalAccountId: Scalars['ID']['input']; +}; + +export type BridgeRequestWithdrawalPayload = { + readonly __typename: 'BridgeRequestWithdrawalPayload'; + readonly errors: ReadonlyArray; + readonly withdrawal?: Maybe; +}; + export type BridgeVirtualAccount = { readonly __typename: 'BridgeVirtualAccount'; readonly accountNumber?: Maybe; @@ -387,8 +413,10 @@ export type BridgeVirtualAccount = { export type BridgeWithdrawal = { readonly __typename: 'BridgeWithdrawal'; readonly amount: Scalars['String']['output']; + readonly bridgeTransferId?: Maybe; readonly createdAt: Scalars['String']['output']; readonly currency: Scalars['String']['output']; + readonly externalAccountId?: Maybe; readonly failureReason?: Maybe; readonly id: Scalars['ID']['output']; readonly status: Scalars['String']['output']; @@ -452,6 +480,27 @@ export type CaptchaRequestAuthCodeInput = { readonly validationCode: Scalars['String']['input']; }; +export type CashWalletCutover = { + readonly __typename: 'CashWalletCutover'; + readonly completedAt?: Maybe; + readonly cutoverVersion: Scalars['Int']['output']; + readonly pauseReason?: Maybe; + readonly pausedAt?: Maybe; + readonly runId?: Maybe; + readonly scheduledAt?: Maybe; + readonly startedAt?: Maybe; + readonly state: CashWalletCutoverState; + readonly updatedAt: Scalars['Timestamp']['output']; + readonly updatedBy?: Maybe; +}; + +export const CashWalletCutoverState = { + Complete: 'COMPLETE', + InProgress: 'IN_PROGRESS', + Pre: 'PRE' +} as const; + +export type CashWalletCutoverState = typeof CashWalletCutoverState[keyof typeof CashWalletCutoverState]; export type CashoutOffer = { readonly __typename: 'CashoutOffer'; /** The rate used when withdrawing to a JMD bank account */ @@ -861,6 +910,17 @@ export type LnUsdInvoiceFeeProbeInput = { readonly walletId: Scalars['WalletId']['input']; }; +export type LnurlPaymentSendInput = { + /** Amount to spend from the USD/USDT wallet, in USD cents. */ + readonly amount: Scalars['FractionalCentAmount']['input']; + /** LNURL-pay value to decode and pay. */ + readonly lnurl: Scalars['Lnurl']['input']; + /** Optional memo for the Lightning payment. */ + readonly memo?: InputMaybe; + /** Wallet ID with sufficient balance. Must belong to the current user. */ + readonly walletId: Scalars['WalletId']['input']; +}; + export type MapInfo = { readonly __typename: 'MapInfo'; readonly coordinates: Coordinates; @@ -916,9 +976,11 @@ export type Mutation = { readonly accountUpdateDefaultWalletId: AccountUpdateDefaultWalletIdPayload; readonly accountUpdateDisplayCurrency: AccountUpdateDisplayCurrencyPayload; readonly bridgeAddExternalAccount: BridgeAddExternalAccountPayload; + readonly bridgeCancelWithdrawalRequest: BridgeCancelWithdrawalRequestPayload; readonly bridgeCreateVirtualAccount: BridgeCreateVirtualAccountPayload; readonly bridgeInitiateKyc: BridgeInitiateKycPayload; readonly bridgeInitiateWithdrawal: BridgeInitiateWithdrawalPayload; + readonly bridgeRequestWithdrawal: BridgeRequestWithdrawalPayload; readonly businessAccountUpgradeRequest: AccountUpgradePayload; readonly callbackEndpointAdd: CallbackEndpointAddPayload; readonly callbackEndpointDelete: SuccessPayload; @@ -1006,6 +1068,11 @@ export type Mutation = { */ readonly lnUsdInvoiceCreateOnBehalfOfRecipient: LnInvoicePayload; readonly lnUsdInvoiceFeeProbe: CentAmountPayload; + /** + * Pay a LNURL-pay endpoint using a USD/USDT wallet balance. + * The wallet amount is converted to whole-satoshi millisatoshis before calling IBEX. + */ + readonly lnurlPaymentSend: PaymentSendPayload; readonly merchantMapSuggest: MerchantPayload; readonly onChainAddressCreate: OnChainAddressPayload; readonly onChainAddressCurrent: OnChainAddressPayload; @@ -1073,6 +1140,11 @@ export type MutationAccountUpdateDisplayCurrencyArgs = { }; +export type MutationBridgeCancelWithdrawalRequestArgs = { + input: BridgeCancelWithdrawalRequestInput; +}; + + export type MutationBridgeInitiateKycArgs = { input: BridgeInitiateKycInput; }; @@ -1083,6 +1155,11 @@ export type MutationBridgeInitiateWithdrawalArgs = { }; +export type MutationBridgeRequestWithdrawalArgs = { + input: BridgeRequestWithdrawalInput; +}; + + export type MutationBusinessAccountUpgradeRequestArgs = { input: BusinessAccountUpgradeRequestInput; }; @@ -1198,6 +1275,11 @@ export type MutationLnUsdInvoiceFeeProbeArgs = { }; +export type MutationLnurlPaymentSendArgs = { + input: LnurlPaymentSendInput; +}; + + export type MutationMerchantMapSuggestArgs = { input: MerchantMapSuggestInput; }; @@ -1556,11 +1638,13 @@ export type Query = { readonly bridgeExternalAccounts?: Maybe>>; readonly bridgeKycStatus?: Maybe; readonly bridgeVirtualAccount?: Maybe; + readonly bridgeWithdrawalRequest?: Maybe; readonly bridgeWithdrawals?: Maybe>>; /** @deprecated Deprecated in favor of realtimePrice */ readonly btcPrice?: Maybe; readonly btcPriceList?: Maybe>>; readonly businessMapMarkers: ReadonlyArray; + readonly cashWalletCutover: CashWalletCutover; readonly colorScheme: Scalars['String']['output']; readonly currencyList: ReadonlyArray; readonly feedbackModalShown: Scalars['Boolean']['output']; @@ -1596,6 +1680,11 @@ export type QueryAccountDefaultWalletArgs = { }; +export type QueryBridgeWithdrawalRequestArgs = { + id: Scalars['ID']['input']; +}; + + export type QueryBtcPriceArgs = { currency?: Scalars['DisplayCurrency']['input']; }; @@ -1967,7 +2056,7 @@ export type UsdWalletTransactionsByAddressArgs = { export type UsdtWallet = Wallet & { readonly __typename: 'UsdtWallet'; readonly accountId: Scalars['ID']['output']; - readonly balance: Scalars['FractionalCentAmount']['output']; + readonly balance?: Maybe; readonly id: Scalars['ID']['output']; readonly isExternal: Scalars['Boolean']['output']; readonly lnurlp?: Maybe; @@ -2309,7 +2398,7 @@ export type AnalyticsQueryVariables = Exact<{ [key: string]: never; }>; export type AnalyticsQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly username?: string | null, readonly id: string } | null, readonly globals?: { readonly __typename: 'Globals', readonly network: Network } | null }; -export type MyWalletsFragment = { readonly __typename: 'ConsumerAccount', readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> }; +export type MyWalletsFragment = { readonly __typename: 'ConsumerAccount', readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> }; export type RealtimePriceQueryVariables = Exact<{ [key: string]: never; }>; @@ -2488,6 +2577,25 @@ export type BridgeInitiateKycMutationVariables = Exact<{ export type BridgeInitiateKycMutation = { readonly __typename: 'Mutation', readonly bridgeInitiateKyc: { readonly __typename: 'BridgeInitiateKycPayload', readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string }>, readonly kycLink?: { readonly __typename: 'BridgeKycLink', readonly kycLink: string, readonly tosLink: string } | null } }; +export type BridgeAddExternalAccountMutationVariables = Exact<{ [key: string]: never; }>; + + +export type BridgeAddExternalAccountMutation = { readonly __typename: 'Mutation', readonly bridgeAddExternalAccount: { readonly __typename: 'BridgeAddExternalAccountPayload', readonly externalAccount?: { readonly __typename: 'BridgeExternalAccountLink', readonly expiresAt: string, readonly linkUrl: string } | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string }> } }; + +export type BridgeRequestWithdrawalMutationVariables = Exact<{ + input: BridgeRequestWithdrawalInput; +}>; + + +export type BridgeRequestWithdrawalMutation = { readonly __typename: 'Mutation', readonly bridgeRequestWithdrawal: { readonly __typename: 'BridgeRequestWithdrawalPayload', readonly withdrawal?: { readonly __typename: 'BridgeWithdrawal', readonly amount: string, readonly createdAt: string, readonly bridgeTransferId?: string | null, readonly currency: string, readonly externalAccountId?: string | null, readonly failureReason?: string | null, readonly id: string, readonly status: string } | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string }> } }; + +export type BridgeInitiateWithdrawalMutationVariables = Exact<{ + input: BridgeInitiateWithdrawalInput; +}>; + + +export type BridgeInitiateWithdrawalMutation = { readonly __typename: 'Mutation', readonly bridgeInitiateWithdrawal: { readonly __typename: 'BridgeInitiateWithdrawalPayload', readonly withdrawal?: { readonly __typename: 'BridgeWithdrawal', readonly amount: string, readonly bridgeTransferId?: string | null, readonly createdAt: string, readonly currency: string, readonly externalAccountId?: string | null, readonly failureReason?: string | null, readonly id: string, readonly status: string } | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string }> } }; + export type AuthQueryVariables = Exact<{ [key: string]: never; }>; @@ -2496,7 +2604,7 @@ export type AuthQuery = { readonly __typename: 'Query', readonly me?: { readonly export type HomeAuthedQueryVariables = Exact<{ [key: string]: never; }>; -export type HomeAuthedQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly language: string, readonly username?: string | null, readonly phone?: string | null, readonly npub?: string | null, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly level: AccountLevel, readonly defaultWalletId: string, readonly transactions?: { readonly __typename: 'TransactionConnection', readonly pageInfo: { readonly __typename: 'PageInfo', readonly hasNextPage: boolean, readonly hasPreviousPage: boolean, readonly startCursor?: string | null, readonly endCursor?: string | null }, readonly edges?: ReadonlyArray<{ readonly __typename: 'TransactionEdge', readonly cursor: string, readonly node: { readonly __typename: 'Transaction', readonly id: string, readonly status: TxStatus, readonly direction: TxDirection, readonly memo?: string | null, readonly createdAt: number, readonly settlementAmount: number, readonly settlementFee: number, readonly settlementDisplayFee: string, readonly settlementCurrency: WalletCurrency, readonly settlementDisplayAmount: string, readonly settlementDisplayCurrency: string, readonly settlementPrice: { readonly __typename: 'PriceOfOneSettlementMinorUnitInDisplayMinorUnit', readonly base: number, readonly offset: number, readonly currencyUnit: string, readonly formattedAmount: string }, readonly initiationVia: { readonly __typename: 'InitiationViaIntraLedger', readonly counterPartyWalletId?: string | null, readonly counterPartyUsername?: string | null } | { readonly __typename: 'InitiationViaLn', readonly paymentHash: string } | { readonly __typename: 'InitiationViaOnChain', readonly address: string }, readonly settlementVia: { readonly __typename: 'SettlementViaIntraLedger', readonly counterPartyWalletId?: string | null, readonly counterPartyUsername?: string | null } | { readonly __typename: 'SettlementViaLn', readonly paymentSecret?: string | null } | { readonly __typename: 'SettlementViaOnChain', readonly transactionHash?: string | null } } }> | null } | null, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; +export type HomeAuthedQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly language: string, readonly username?: string | null, readonly phone?: string | null, readonly createdAt: number, readonly npub?: string | null, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly level: AccountLevel, readonly defaultWalletId: string, readonly transactions?: { readonly __typename: 'TransactionConnection', readonly pageInfo: { readonly __typename: 'PageInfo', readonly hasNextPage: boolean, readonly hasPreviousPage: boolean, readonly startCursor?: string | null, readonly endCursor?: string | null }, readonly edges?: ReadonlyArray<{ readonly __typename: 'TransactionEdge', readonly cursor: string, readonly node: { readonly __typename: 'Transaction', readonly id: string, readonly status: TxStatus, readonly direction: TxDirection, readonly memo?: string | null, readonly createdAt: number, readonly settlementAmount: number, readonly settlementFee: number, readonly settlementDisplayFee: string, readonly settlementCurrency: WalletCurrency, readonly settlementDisplayAmount: string, readonly settlementDisplayCurrency: string, readonly settlementPrice: { readonly __typename: 'PriceOfOneSettlementMinorUnitInDisplayMinorUnit', readonly base: number, readonly offset: number, readonly currencyUnit: string, readonly formattedAmount: string }, readonly initiationVia: { readonly __typename: 'InitiationViaIntraLedger', readonly counterPartyWalletId?: string | null, readonly counterPartyUsername?: string | null } | { readonly __typename: 'InitiationViaLn', readonly paymentHash: string } | { readonly __typename: 'InitiationViaOnChain', readonly address: string }, readonly settlementVia: { readonly __typename: 'SettlementViaIntraLedger', readonly counterPartyWalletId?: string | null, readonly counterPartyUsername?: string | null } | { readonly __typename: 'SettlementViaLn', readonly paymentSecret?: string | null } | { readonly __typename: 'SettlementViaOnChain', readonly transactionHash?: string | null } } }> | null } | null, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; export type HomeUnauthedQueryVariables = Exact<{ [key: string]: never; }>; @@ -2516,17 +2624,17 @@ export type TransactionListForDefaultAccountQuery = { readonly __typename: 'Quer export type SetDefaultAccountModalQueryVariables = Exact<{ [key: string]: never; }>; -export type SetDefaultAccountModalQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; +export type SetDefaultAccountModalQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; export type ConversionScreenQueryVariables = Exact<{ [key: string]: never; }>; -export type ConversionScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; +export type ConversionScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; export type CashoutScreenQueryVariables = Exact<{ [key: string]: never; }>; -export type CashoutScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; +export type CashoutScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; export type WalletCsvTransactionsQueryVariables = Exact<{ walletIds: ReadonlyArray | Scalars['WalletId']['input']; @@ -2538,7 +2646,7 @@ export type WalletCsvTransactionsQuery = { readonly __typename: 'Query', readonl export type WalletOverviewScreenQueryVariables = Exact<{ [key: string]: never; }>; -export type WalletOverviewScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; +export type WalletOverviewScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; export type SendBitcoinDestinationQueryVariables = Exact<{ [key: string]: never; }>; @@ -2555,7 +2663,7 @@ export type AccountDefaultWalletQuery = { readonly __typename: 'Query', readonly export type SendBitcoinDetailsScreenQueryVariables = Exact<{ [key: string]: never; }>; -export type SendBitcoinDetailsScreenQuery = { readonly __typename: 'Query', readonly globals?: { readonly __typename: 'Globals', readonly network: Network } | null, readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly walletCurrency: WalletCurrency, readonly balance?: number | null, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly walletCurrency: WalletCurrency, readonly balance?: number | null, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly walletCurrency: WalletCurrency, readonly balance: number, readonly isExternal: boolean }> } } | null }; +export type SendBitcoinDetailsScreenQuery = { readonly __typename: 'Query', readonly globals?: { readonly __typename: 'Globals', readonly network: Network } | null, readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly walletCurrency: WalletCurrency, readonly balance?: number | null, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly walletCurrency: WalletCurrency, readonly balance?: number | null, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly walletCurrency: WalletCurrency, readonly balance?: number | null, readonly isExternal: boolean }> } } | null }; export type SendBitcoinWithdrawalLimitsQueryVariables = Exact<{ [key: string]: never; }>; @@ -2570,7 +2678,7 @@ export type SendBitcoinInternalLimitsQuery = { readonly __typename: 'Query', rea export type SendBitcoinConfirmationScreenQueryVariables = Exact<{ [key: string]: never; }>; -export type SendBitcoinConfirmationScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; +export type SendBitcoinConfirmationScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; export type ScanningQrCodeScreenQueryVariables = Exact<{ [key: string]: never; }>; @@ -2616,6 +2724,16 @@ export type BankAccountsQueryVariables = Exact<{ [key: string]: never; }>; export type BankAccountsQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly bankAccounts: ReadonlyArray<{ readonly __typename: 'BankAccount', readonly accountName?: string | null, readonly accountNumber: string, readonly accountType: string, readonly bankBranch: string, readonly bankName: string, readonly currency: string, readonly id?: string | null, readonly isDefault: boolean }> } | null }; +export type BridgeExternalAccountsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type BridgeExternalAccountsQuery = { readonly __typename: 'Query', readonly bridgeExternalAccounts?: ReadonlyArray<{ readonly __typename: 'BridgeExternalAccount', readonly accountNumberLast4: string, readonly bankName: string, readonly id: string, readonly status: string } | null> | null }; + +export type CashWalletCutoverQueryVariables = Exact<{ [key: string]: never; }>; + + +export type CashWalletCutoverQuery = { readonly __typename: 'Query', readonly cashWalletCutover: { readonly __typename: 'CashWalletCutover', readonly completedAt?: number | null, readonly cutoverVersion: number, readonly pauseReason?: string | null, readonly pausedAt?: number | null, readonly runId?: string | null, readonly scheduledAt?: number | null, readonly startedAt?: number | null, readonly state: CashWalletCutoverState, readonly updatedAt: number, readonly updatedBy?: string | null } }; + export type RealtimePriceWsSubscriptionVariables = Exact<{ currency: Scalars['DisplayCurrency']['input']; }>; @@ -2822,7 +2940,7 @@ export type MyLnUpdatesSubscription = { readonly __typename: 'Subscription', rea export type PaymentRequestQueryVariables = Exact<{ [key: string]: never; }>; -export type PaymentRequestQuery = { readonly __typename: 'Query', readonly globals?: { readonly __typename: 'Globals', readonly network: Network, readonly feesInformation: { readonly __typename: 'FeesInformation', readonly deposit: { readonly __typename: 'DepositFeesInformation', readonly minBankFee: string, readonly minBankFeeThreshold: string } } } | null, readonly me?: { readonly __typename: 'User', readonly id: string, readonly username?: string | null, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; +export type PaymentRequestQuery = { readonly __typename: 'Query', readonly globals?: { readonly __typename: 'Globals', readonly network: Network, readonly feesInformation: { readonly __typename: 'FeesInformation', readonly deposit: { readonly __typename: 'DepositFeesInformation', readonly minBankFee: string, readonly minBankFeeThreshold: string } } } | null, readonly me?: { readonly __typename: 'User', readonly id: string, readonly username?: string | null, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; export type LnNoAmountInvoiceCreateMutationVariables = Exact<{ input: LnNoAmountInvoiceCreateInput; @@ -2862,7 +2980,7 @@ export type FeedbackSubmitMutation = { readonly __typename: 'Mutation', readonly export type AccountScreenQueryVariables = Exact<{ [key: string]: never; }>; -export type AccountScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly phone?: string | null, readonly totpEnabled: boolean, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly level: AccountLevel, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; +export type AccountScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly phone?: string | null, readonly totpEnabled: boolean, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly level: AccountLevel, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; export type UserEmailDeleteMutationVariables = Exact<{ [key: string]: never; }>; @@ -2884,7 +3002,7 @@ export type UserTotpDeleteMutation = { readonly __typename: 'Mutation', readonly export type WarningSecureAccountQueryVariables = Exact<{ [key: string]: never; }>; -export type WarningSecureAccountQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly level: AccountLevel, readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; +export type WarningSecureAccountQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly level: AccountLevel, readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; export type AccountUpdateDefaultWalletIdMutationVariables = Exact<{ input: AccountUpdateDefaultWalletIdInput; @@ -2896,7 +3014,7 @@ export type AccountUpdateDefaultWalletIdMutation = { readonly __typename: 'Mutat export type SetDefaultWalletScreenQueryVariables = Exact<{ [key: string]: never; }>; -export type SetDefaultWalletScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; +export type SetDefaultWalletScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> } } | null }; export type AccountUpdateDisplayCurrencyMutationVariables = Exact<{ input: AccountUpdateDisplayCurrencyInput; @@ -2953,7 +3071,7 @@ export type AccountDisableNotificationCategoryMutation = { readonly __typename: export type SettingsScreenQueryVariables = Exact<{ [key: string]: never; }>; -export type SettingsScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly phone?: string | null, readonly username?: string | null, readonly language: string, readonly totpEnabled: boolean, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> }, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null } | null }; +export type SettingsScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly phone?: string | null, readonly username?: string | null, readonly language: string, readonly totpEnabled: boolean, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean } | { readonly __typename: 'UsdtWallet', readonly id: string, readonly balance?: number | null, readonly walletCurrency: WalletCurrency, readonly isExternal: boolean }> }, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null } | null }; export type ExportCsvSettingQueryVariables = Exact<{ walletIds: ReadonlyArray | Scalars['WalletId']['input']; @@ -4264,6 +4382,137 @@ export function useBridgeInitiateKycMutation(baseOptions?: Apollo.MutationHookOp export type BridgeInitiateKycMutationHookResult = ReturnType; export type BridgeInitiateKycMutationResult = Apollo.MutationResult; export type BridgeInitiateKycMutationOptions = Apollo.BaseMutationOptions; +export const BridgeAddExternalAccountDocument = gql` + mutation BridgeAddExternalAccount { + bridgeAddExternalAccount { + externalAccount { + expiresAt + linkUrl + } + errors { + code + message + } + } +} + `; +export type BridgeAddExternalAccountMutationFn = Apollo.MutationFunction; + +/** + * __useBridgeAddExternalAccountMutation__ + * + * To run a mutation, you first call `useBridgeAddExternalAccountMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useBridgeAddExternalAccountMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [bridgeAddExternalAccountMutation, { data, loading, error }] = useBridgeAddExternalAccountMutation({ + * variables: { + * }, + * }); + */ +export function useBridgeAddExternalAccountMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(BridgeAddExternalAccountDocument, options); + } +export type BridgeAddExternalAccountMutationHookResult = ReturnType; +export type BridgeAddExternalAccountMutationResult = Apollo.MutationResult; +export type BridgeAddExternalAccountMutationOptions = Apollo.BaseMutationOptions; +export const BridgeRequestWithdrawalDocument = gql` + mutation BridgeRequestWithdrawal($input: BridgeRequestWithdrawalInput!) { + bridgeRequestWithdrawal(input: $input) { + withdrawal { + amount + createdAt + bridgeTransferId + currency + externalAccountId + failureReason + id + status + } + errors { + code + message + } + } +} + `; +export type BridgeRequestWithdrawalMutationFn = Apollo.MutationFunction; + +/** + * __useBridgeRequestWithdrawalMutation__ + * + * To run a mutation, you first call `useBridgeRequestWithdrawalMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useBridgeRequestWithdrawalMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [bridgeRequestWithdrawalMutation, { data, loading, error }] = useBridgeRequestWithdrawalMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useBridgeRequestWithdrawalMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(BridgeRequestWithdrawalDocument, options); + } +export type BridgeRequestWithdrawalMutationHookResult = ReturnType; +export type BridgeRequestWithdrawalMutationResult = Apollo.MutationResult; +export type BridgeRequestWithdrawalMutationOptions = Apollo.BaseMutationOptions; +export const BridgeInitiateWithdrawalDocument = gql` + mutation BridgeInitiateWithdrawal($input: BridgeInitiateWithdrawalInput!) { + bridgeInitiateWithdrawal(input: $input) { + withdrawal { + amount + bridgeTransferId + createdAt + currency + externalAccountId + failureReason + id + status + } + errors { + code + message + } + } +} + `; +export type BridgeInitiateWithdrawalMutationFn = Apollo.MutationFunction; + +/** + * __useBridgeInitiateWithdrawalMutation__ + * + * To run a mutation, you first call `useBridgeInitiateWithdrawalMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useBridgeInitiateWithdrawalMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [bridgeInitiateWithdrawalMutation, { data, loading, error }] = useBridgeInitiateWithdrawalMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useBridgeInitiateWithdrawalMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(BridgeInitiateWithdrawalDocument, options); + } +export type BridgeInitiateWithdrawalMutationHookResult = ReturnType; +export type BridgeInitiateWithdrawalMutationResult = Apollo.MutationResult; +export type BridgeInitiateWithdrawalMutationOptions = Apollo.BaseMutationOptions; export const AuthDocument = gql` query auth { me { @@ -4312,6 +4561,7 @@ export const HomeAuthedDocument = gql` language username phone + createdAt email { address verified @@ -5266,6 +5516,86 @@ export function useBankAccountsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptio export type BankAccountsQueryHookResult = ReturnType; export type BankAccountsLazyQueryHookResult = ReturnType; export type BankAccountsQueryResult = Apollo.QueryResult; +export const BridgeExternalAccountsDocument = gql` + query BridgeExternalAccounts { + bridgeExternalAccounts { + accountNumberLast4 + bankName + id + status + } +} + `; + +/** + * __useBridgeExternalAccountsQuery__ + * + * To run a query within a React component, call `useBridgeExternalAccountsQuery` and pass it any options that fit your needs. + * When your component renders, `useBridgeExternalAccountsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useBridgeExternalAccountsQuery({ + * variables: { + * }, + * }); + */ +export function useBridgeExternalAccountsQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(BridgeExternalAccountsDocument, options); + } +export function useBridgeExternalAccountsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(BridgeExternalAccountsDocument, options); + } +export type BridgeExternalAccountsQueryHookResult = ReturnType; +export type BridgeExternalAccountsLazyQueryHookResult = ReturnType; +export type BridgeExternalAccountsQueryResult = Apollo.QueryResult; +export const CashWalletCutoverDocument = gql` + query CashWalletCutover { + cashWalletCutover { + completedAt + cutoverVersion + pauseReason + pausedAt + runId + scheduledAt + startedAt + state + updatedAt + updatedBy + } +} + `; + +/** + * __useCashWalletCutoverQuery__ + * + * To run a query within a React component, call `useCashWalletCutoverQuery` and pass it any options that fit your needs. + * When your component renders, `useCashWalletCutoverQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useCashWalletCutoverQuery({ + * variables: { + * }, + * }); + */ +export function useCashWalletCutoverQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(CashWalletCutoverDocument, options); + } +export function useCashWalletCutoverLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(CashWalletCutoverDocument, options); + } +export type CashWalletCutoverQueryHookResult = ReturnType; +export type CashWalletCutoverLazyQueryHookResult = ReturnType; +export type CashWalletCutoverQueryResult = Apollo.QueryResult; export const RealtimePriceWsDocument = gql` subscription realtimePriceWs($currency: DisplayCurrency!) { realtimePrice(input: {currency: $currency}) { From 025ed86a06f4512354edd607cc786adc1a5c5884 Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Mon, 15 Jun 2026 13:19:58 +0500 Subject: [PATCH 61/95] BridgeExternalAccountWebView is implemented and added on the root navigation --- app/navigation/root-navigator.tsx | 9 + app/navigation/stack-param-lists.ts | 5 +- .../BridgeExternalAccountWebView.tsx | 217 ++++++++++++++++++ app/screens/topup-cashout-flow/index.ts | 2 + 4 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 app/screens/topup-cashout-flow/BridgeExternalAccountWebView.tsx diff --git a/app/navigation/root-navigator.tsx b/app/navigation/root-navigator.tsx index 4077a1a2f..00cc5c4ce 100644 --- a/app/navigation/root-navigator.tsx +++ b/app/navigation/root-navigator.tsx @@ -127,6 +127,7 @@ import { FeaturedProfileView } from "@app/screens/featured-profile-view" import { BankTransfer, BridgeKycWebView, + BridgeExternalAccountWebView, TopupDetails, TopupSuccess, TopupCashout, @@ -677,6 +678,14 @@ export const RootStack = () => { cardStyleInterpolator: CardStyleInterpolators.forModalPresentationIOS, }} /> + diff --git a/app/navigation/stack-param-lists.ts b/app/navigation/stack-param-lists.ts index 424d5cef5..3b17d336e 100644 --- a/app/navigation/stack-param-lists.ts +++ b/app/navigation/stack-param-lists.ts @@ -143,7 +143,7 @@ export type RootStackParamList = { UnclaimedDepositsList: undefined UnclaimedDepositDetails: { deposit: DepositInfo } RefundDeposit: { deposit: DepositInfo } - CashoutDetails: undefined + CashoutDetails: { type: "local" | "bridge" } CashoutConfirmation: { offer: CashoutOffer } CashoutSuccess: undefined EditNostrProfile: undefined @@ -166,6 +166,9 @@ export type RootStackParamList = { tosLink: string kycLink: string } + BridgeExternalAccountWebView: { + linkUrl: string + } TopupCashout: undefined TopupDetails: { paymentType: "card" | "bankTransfer" | "bridge" } BankTransfer: { diff --git a/app/screens/topup-cashout-flow/BridgeExternalAccountWebView.tsx b/app/screens/topup-cashout-flow/BridgeExternalAccountWebView.tsx new file mode 100644 index 000000000..f9ecf94ee --- /dev/null +++ b/app/screens/topup-cashout-flow/BridgeExternalAccountWebView.tsx @@ -0,0 +1,217 @@ +import React, { useState, useRef, useCallback } from "react" +import { + View, + ActivityIndicator, + Linking, + Platform, + TouchableOpacity, +} from "react-native" +import { StackScreenProps } from "@react-navigation/stack" +import { Text, makeStyles, useTheme } from "@rneui/themed" +import { RootStackParamList } from "@app/navigation/stack-param-lists" +import { WebView, WebViewNavigation } from "react-native-webview" +import Icon from "react-native-vector-icons/Ionicons" +import { Screen } from "@app/components/screen" +import { PrimaryBtn } from "@app/components/buttons" + +type Props = StackScreenProps + +// Viewport meta injection before content loads to prevent initial zoom. +const VIEWPORT_INJECTION_JS = `(function(){const forceViewport=()=>{const content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no';document.querySelectorAll('meta[name="viewport"]').forEach(m=>m.remove());const meta=document.createElement('meta');meta.name='viewport';meta.content=content;if(document.head){document.head.insertBefore(meta,document.head.firstChild)}else{document.documentElement.appendChild(meta)}};forceViewport();document.addEventListener('DOMContentLoaded',forceViewport);window.addEventListener('load',forceViewport);if(document.documentElement){document.documentElement.style.touchAction='pan-x pan-y'}true})();` + +// iOS zoom prevention +const ZOOM_PREVENTION_JS = `(function(){ + document.querySelectorAll('meta[name="viewport"]').forEach(m=>m.remove()); + var meta=document.createElement('meta'); + meta.name='viewport'; + meta.content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'; + document.head.insertBefore(meta,document.head.firstChild); + var style=document.createElement('style'); + style.innerHTML='input,textarea,select{font-size:16px!important}*{-webkit-text-size-adjust:100%!important;touch-action:manipulation!important}'; + document.head.appendChild(style); + var preventZoom=function(e){if(e.target&&(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA')){e.target.style.fontSize='16px';e.target.style.transform='none'}}; + document.addEventListener('focusin',preventZoom,true); + document.querySelectorAll('input,textarea,select').forEach(function(el){el.style.fontSize='16px'}); + var observer=new MutationObserver(function(mutations){mutations.forEach(function(m){m.addedNodes.forEach(function(n){if(n.nodeType===1){var inputs=n.querySelectorAll?n.querySelectorAll('input,textarea,select'):[];inputs.forEach(function(el){el.style.fontSize='16px'})}})})}); + if(document.body){observer.observe(document.body,{childList:true,subtree:true})} + true})();` + +const BridgeExternalAccountWebView: React.FC = ({ navigation, route }) => { + const styles = useStyles() + const { colors } = useTheme().theme + const webViewRef = useRef(null) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(false) + const isNavigating = useRef(false) + + const { linkUrl } = route.params + + const safeGoBack = useCallback(() => { + if (isNavigating.current) return + isNavigating.current = true + navigation.goBack() + }, [navigation]) + + const handleNavigationStateChange = useCallback( + (navState: WebViewNavigation) => { + const { url } = navState + + // Check if the external account linking is complete + if ( + url.includes("complete") || + url.includes("success") || + url.includes("connected") + ) { + safeGoBack() + } + }, + [safeGoBack], + ) + + const handleMessage = useCallback( + (event: { nativeEvent: { data: string } }) => { + try { + const data = JSON.parse(event.nativeEvent.data) + if (data.status === "completed" || data.type === "account_linked") { + safeGoBack() + } + } catch { + // Not JSON - ignore + } + }, + [safeGoBack], + ) + + const handleRetry = () => { + setError(false) + setIsLoading(true) + webViewRef.current?.reload() + } + + const closeButton = ( + + + + + + ) + + if (error) { + return ( + + {closeButton} + + + Failed to load + + + + + ) + } + + return ( + + {closeButton} + + {isLoading && ( + + + + Loading External Account Setup... + + + )} + { + setIsLoading(false) + setError(false) + }} + onError={() => { + setIsLoading(false) + setError(true) + }} + injectedJavaScriptBeforeContentLoaded={VIEWPORT_INJECTION_JS} + injectedJavaScript={ZOOM_PREVENTION_JS} + style={styles.webView} + javaScriptEnabled + domStorageEnabled + startInLoadingState + scalesPageToFit={false} + bounces={false} + sharedCookiesEnabled + thirdPartyCookiesEnabled + onShouldStartLoadWithRequest={(request) => { + const url = request.url.toLowerCase() + // Open external links in browser if needed + if (url.includes("www.bridge.xyz/legal")) { + Linking.openURL(request.url) + return false + } + return true + }} + originWhitelist={["https://*", "http://*"]} + userAgent={ + Platform.OS === "ios" + ? "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + : undefined + } + /> + + + ) +} + +const useStyles = makeStyles(({ colors }) => ({ + closeButtonWrapper: { + position: "absolute", + zIndex: 1, + top: 15, + right: 15, + }, + closeButton: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: colors.white, + justifyContent: "center", + alignItems: "center", + shadowColor: colors.black, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.15, + shadowRadius: 4, + elevation: 5, + }, + container: { flex: 1 }, + centerContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: 20, + }, + loadingContainer: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: "center", + alignItems: "center", + backgroundColor: "rgba(255, 255, 255, 0.9)", + zIndex: 1, + }, + loadingText: { marginTop: 16, textAlign: "center" }, + errorText: { textAlign: "center", marginBottom: 24 }, + retryButton: { marginTop: 16 }, + webView: { flex: 1 }, +})) + +export default BridgeExternalAccountWebView diff --git a/app/screens/topup-cashout-flow/index.ts b/app/screens/topup-cashout-flow/index.ts index 8a6dae363..be71ee9b0 100644 --- a/app/screens/topup-cashout-flow/index.ts +++ b/app/screens/topup-cashout-flow/index.ts @@ -1,4 +1,5 @@ import BridgeKycWebView from "./BridgeKycWebView" +import BridgeExternalAccountWebView from "./BridgeExternalAccountWebView" import TopupCashout from "./TopupCashout" import TopupDetails from "./TopupDetails" import BankTransfer from "./BankTransfer" @@ -11,6 +12,7 @@ import PaymentSuccessScreen from "./payment-success-screen" export { BridgeKycWebView, + BridgeExternalAccountWebView, TopupCashout, TopupDetails, BankTransfer, From d27a63ca65d8c832ba583db4c3e596fac25dbaee Mon Sep 17 00:00:00 2001 From: nodirbek75 Date: Mon, 15 Jun 2026 13:57:31 +0500 Subject: [PATCH 62/95] integrate bridgeAddExternalAccount mutation and bridgeExternalAccounts query into TopupCashout screen to add and fetch external account --- .../topup-cashout-flow/TopupCashout.tsx | 48 ++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/app/screens/topup-cashout-flow/TopupCashout.tsx b/app/screens/topup-cashout-flow/TopupCashout.tsx index a072ae8da..6ffe42bca 100644 --- a/app/screens/topup-cashout-flow/TopupCashout.tsx +++ b/app/screens/topup-cashout-flow/TopupCashout.tsx @@ -17,6 +17,8 @@ import ArrowUp from "@app/assets/icons/arrow-up-from-bracket.svg" // hooks import { + useBridgeAddExternalAccountMutation, + useBridgeExternalAccountsQuery, useBridgeInitiateKycMutation, useBridgeKycStatusQuery, } from "@app/graphql/generated" @@ -41,21 +43,29 @@ const TopupCashout: React.FC = ({ navigation }) => { const [refreshing, setRefreshing] = useState(false) const [initiateBridgeKyc] = useBridgeInitiateKycMutation() + const [addExternalAccount] = useBridgeAddExternalAccountMutation() + const { data: kycStatusData, refetch: refetchKycStatus } = useBridgeKycStatusQuery({ fetchPolicy: "cache-and-network", }) + const { data: externalAccountsData, refetch: refetchExternalAccounts } = + useBridgeExternalAccountsQuery({ + fetchPolicy: "cache-and-network", + }) useFocusEffect( useCallback(() => { refetchKycStatus() - }, [refetchKycStatus]), + refetchExternalAccounts() + }, [refetchKycStatus, refetchExternalAccounts]), ) const onRefresh = useCallback(async () => { setRefreshing(true) await refetchKycStatus() + await refetchExternalAccounts() setRefreshing(false) - }, [refetchKycStatus]) + }, [refetchKycStatus, refetchExternalAccounts]) const topupOptions: TransferOption[] = useMemo( () => [ @@ -131,7 +141,7 @@ const TopupCashout: React.FC = ({ navigation }) => { ) } else { if (type === "cashout") { - navigation.navigate("CashoutDetails") + navigation.navigate("CashoutDetails", { type: "local" }) } else { navigation.navigate("TopupDetails", { paymentType: type }) } @@ -139,13 +149,37 @@ const TopupCashout: React.FC = ({ navigation }) => { } } - const checkBridgeKyc = (type: "topup" | "settle") => { + const checkBridgeKyc = async (type: "topup" | "settle") => { if (kycStatusData?.bridgeKycStatus === "pending") { Alert.alert("KYC Pending", "Your KYC status is pending. Please wait for approval.") } else if (kycStatusData?.bridgeKycStatus === "approved") { - type === "topup" - ? navigation.navigate("TopupDetails", { paymentType: "bridge" }) - : navigation.navigate("CashoutDetails") + if (type === "topup") { + navigation.navigate("TopupDetails", { paymentType: "bridge" }) + } else { + if ( + externalAccountsData?.bridgeExternalAccounts && + externalAccountsData.bridgeExternalAccounts.length > 0 + ) { + navigation.navigate("CashoutDetails", { type: "bridge" }) + } else { + toggleActivityIndicator(true) + const res = await addExternalAccount() + toggleActivityIndicator(false) + + const errors = res.data?.bridgeAddExternalAccount?.errors + if (errors && errors.length > 0) { + Alert.alert("Error", errors[0].message) + return + } + + const linkUrl = res.data?.bridgeAddExternalAccount?.externalAccount?.linkUrl + if (linkUrl) { + navigation.navigate("BridgeExternalAccountWebView", { linkUrl }) + } else { + Alert.alert("Error", "Failed to get external account link. Please try again.") + } + } + } } else { setBridgeKycModalVisible(true) } From 8b4393bf96bfdeb2aa296ec3b8b7ef80685a4f69 Mon Sep 17 00:00:00 2001 From: Island Bitcoin <34528298+islandbitcoin@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:04:22 -0700 Subject: [PATCH 63/95] ci: run checks on the feat/fygaro integration branch (#637) Test/check workflows were filtered to `pull_request: branches: [main]`, so PRs targeting the long-lived `feat/fygaro` integration branch never triggered CI. Add the integration branch to the pull_request filter on the gating workflows, and add a push trigger on the core check/test workflows so the branch tip is checked as PRs stack onto it. Co-authored-by: Claude Opus 4.8 (1M context) --- .github/workflows/check-code.yml | 4 +++- .github/workflows/conventional.yaml | 2 +- .github/workflows/spelling.yml | 2 +- .github/workflows/test.yml | 4 +++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check-code.yml b/.github/workflows/check-code.yml index ccf0e9131..4c5ac4aa3 100644 --- a/.github/workflows/check-code.yml +++ b/.github/workflows/check-code.yml @@ -1,7 +1,9 @@ name: "Check code" on: + push: + branches: [feat/fygaro] pull_request: - branches: [main] + branches: [main, feat/fygaro] jobs: check-code: name: Check Code diff --git a/.github/workflows/conventional.yaml b/.github/workflows/conventional.yaml index 91b8e12b9..82bcd8ea8 100644 --- a/.github/workflows/conventional.yaml +++ b/.github/workflows/conventional.yaml @@ -1,7 +1,7 @@ name: "Conventional commits" on: pull_request: - branches: [main] + branches: [main, feat/fygaro] jobs: conventional: name: "Conventional commits" diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml index a8c9f9784..492b558e6 100644 --- a/.github/workflows/spelling.yml +++ b/.github/workflows/spelling.yml @@ -1,7 +1,7 @@ name: Spelling on: pull_request: - branches: [main] + branches: [main, feat/fygaro] jobs: spelling: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0d0756efe..f173f3e8b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,7 +1,9 @@ name: "Test" on: + push: + branches: [feat/fygaro] pull_request: - branches: [main] + branches: [main, feat/fygaro] jobs: check-code: name: Tests From e0d7aae664a3eace8c4b8a38fc8280326bffaa63 Mon Sep 17 00:00:00 2001 From: Island Bitcoin <34528298+islandbitcoin@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:04:55 -0700 Subject: [PATCH 64/95] fix(ci): bump CI Node to 20 and resolve spelling-check failures (#638) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Once Actions ran on feat/fygaro for the first time, three checks failed. All were pre-existing issues, not caused by enabling CI: - Check Code & Test both died at `yarn install`: `@noble/ciphers@2.1.1` requires Node >=20.19 but CI pinned Node 18. Bump check-code.yml, test.yml, .node-version, and package.json engines to 20. - Spelling flagged 13 typos. Fixed the safe ones (a local var and three code comments); excluded non-source files where the rest live (logcat.txt, *.storyboard); and allowlisted tokens that can't be renamed safely here — earns quiz IDs (backend contract), i18n message keys (need codegen pipeline), and cross-file identifiers. Co-authored-by: Claude Opus 4.8 (1M context) --- .github/workflows/check-code.yml | 2 +- .github/workflows/test.yml | 2 +- .node-version | 2 +- app/graphql/client.tsx | 2 +- .../phone-login-validation.tsx | 2 +- .../send-bitcoin-destination-screen.tsx | 2 +- app/utils/breez-sdk/migration.ts | 4 ++-- package.json | 2 +- typos.toml | 23 ++++++++++++++++++- 9 files changed, 31 insertions(+), 10 deletions(-) diff --git a/.github/workflows/check-code.yml b/.github/workflows/check-code.yml index 4c5ac4aa3..cf8a209eb 100644 --- a/.github/workflows/check-code.yml +++ b/.github/workflows/check-code.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 20 - run: yarn install - name: Run check code run: yarn check-code diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f173f3e8b..e7e80e585 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 20 - run: yarn install - name: Run test run: yarn test diff --git a/.node-version b/.node-version index 3c032078a..209e3ef4b 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -18 +20 diff --git a/app/graphql/client.tsx b/app/graphql/client.tsx index 4d70605fe..509755f6d 100644 --- a/app/graphql/client.tsx +++ b/app/graphql/client.tsx @@ -223,7 +223,7 @@ const GaloyClient: React.FC = ({ children }) => { } // persistedQuery provide client side bandwidth optimization by returning a hash - // of the uery instead of the whole query + // of the query instead of the whole query // // use the following line if you want to deactivate in dev // const persistedQueryLink = httpLink diff --git a/app/screens/phone-auth-screen/phone-login-validation.tsx b/app/screens/phone-auth-screen/phone-login-validation.tsx index 78da76f83..aa7449f43 100644 --- a/app/screens/phone-auth-screen/phone-login-validation.tsx +++ b/app/screens/phone-auth-screen/phone-login-validation.tsx @@ -223,7 +223,7 @@ export const PhoneLoginValidationScreen: React.FC = ({ navigation, route return undefined }) } - // enbale nostr chat + // enable nostr chat if (nsec) { const secretKey = hexToBytes(nsec) await Keychain.setInternetCredentials( diff --git a/app/screens/send-bitcoin-screen/send-bitcoin-destination-screen.tsx b/app/screens/send-bitcoin-screen/send-bitcoin-destination-screen.tsx index ff4260f56..759a2ad08 100644 --- a/app/screens/send-bitcoin-screen/send-bitcoin-destination-screen.tsx +++ b/app/screens/send-bitcoin-screen/send-bitcoin-destination-screen.tsx @@ -4,7 +4,7 @@ import { makeStyles } from "@rneui/themed" import { useI18nContext } from "@app/i18n/i18n-react" import { StackScreenProps } from "@react-navigation/stack" -// componenets +// components import { Screen } from "@app/components/screen" import { PrimaryBtn } from "@app/components/buttons" import { DestinationField } from "@app/components/send-flow" diff --git a/app/utils/breez-sdk/migration.ts b/app/utils/breez-sdk/migration.ts index 46a1bda8b..44e25fbbb 100644 --- a/app/utils/breez-sdk/migration.ts +++ b/app/utils/breez-sdk/migration.ts @@ -76,12 +76,12 @@ const drainLiquidWallet = async ( const feeReimbursement = async (estimatedFee: number): Promise => { try { - const lnurwRespons = await lnurlWithdraw( + const lnurwResponse = await lnurlWithdraw( MIGRATION_FEE_LNURL_W, Math.round(estimatedFee * 2), ) - return lnurwRespons.success + return lnurwResponse.success } catch (err) { throw `Fee reimbursement failed. The amount to reimburse is ${Math.round( estimatedFee * 2, diff --git a/package.json b/package.json index f2e3f1236..223d2c9fd 100644 --- a/package.json +++ b/package.json @@ -376,6 +376,6 @@ "tmp": "^0.2.4" }, "engines": { - "node": ">=18" + "node": ">=20" } } diff --git a/typos.toml b/typos.toml index ffbd632d1..05c8394c0 100644 --- a/typos.toml +++ b/typos.toml @@ -3,6 +3,27 @@ youreConverting = "youreConverting" hace = "hace" "BA" = "BA" "FO" = "FO" +# Earns quiz section IDs: these are a contract with the backend — matched +# against server quiz data in earns-utils.ts and sent to the `quizCompleted` +# mutation. Renaming would break quiz completion/rewards, so the historical +# misspelling is preserved intentionally. +moneySocialAggrement = "moneySocialAggrement" +moneyImportantGovernement = "moneyImportantGovernement" +GovernementCanPrintMoney = "GovernementCanPrintMoney" +# i18n message keys: a clean rename requires running the i18n codegen pipeline +# (raw-i18n + generated types + all locales). Deferred to a focused i18n PR so +# this CI-fix change stays reviewable. +apiAcess = "apiAcess" +noCantacts = "noCantacts" +# Pre-existing internal identifiers — safe to rename, but each is a cross-file +# refactor (component file + imports, analytics field + call sites). Deferred. +isEnjoingApp = "isEnjoingApp" +InforError = "InforError" + +[default.extend-words] +# Nostr terminology: "parameterized replaceable events" are referred to as +# "replaceables" in NIP discussions. +replaceables = "replaceables" [files] -extend-exclude = ["*.svg", "app/i18n/raw-i18n", "earns-utils.test.ts", "*.plist", "*.pbxproj", "Appfile", "google-services.json", "patches", "ci/tasks/*"] +extend-exclude = ["*.svg", "app/i18n/raw-i18n", "earns-utils.test.ts", "*.plist", "*.pbxproj", "Appfile", "google-services.json", "patches", "ci/tasks/*", "logcat.txt", "*.storyboard"] From b0dcdde42155db46bd1fd4ddb08d22f80aa338db Mon Sep 17 00:00:00 2001 From: Island Bitcoin <34528298+islandbitcoin@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:29:02 -0700 Subject: [PATCH 65/95] ci: stop conventional-pr from auto-closing invalid PRs (#639) The Namchee/conventional-pr action defaults to closing any non-draft PR whose title isn't conventional. With Actions re-enabled, this auto-closed the feat/fygaro umbrella PR #621 on every reopen. Set close: false so it comments/reports instead of closing. Co-authored-by: Claude Opus 4.8 (1M context) --- .github/workflows/conventional.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/conventional.yaml b/.github/workflows/conventional.yaml index 82bcd8ea8..664f73305 100644 --- a/.github/workflows/conventional.yaml +++ b/.github/workflows/conventional.yaml @@ -14,3 +14,6 @@ jobs: message: this PR needs to be updated to follow conventional commits message body: false issue: false + # Report on invalid PRs instead of closing them (avoids nuking + # long-lived PRs like the feat/fygaro umbrella #621). + close: false From c03dc3fcba631671cfafc48b300f0c9df9d0bbc0 Mon Sep 17 00:00:00 2001 From: Island Bitcoin <34528298+islandbitcoin@users.noreply.github.com> Date: Thu, 18 Jun 2026 07:57:42 -0700 Subject: [PATCH 66/95] fix: render USDT amounts and currency tags (#634) Co-authored-by: Vandana --- __tests__/components/currency-tag.spec.tsx | 25 +++++++ __tests__/hooks/use-display-currency.spec.tsx | 46 ++++++++++++- app/components/currency-tag/currency-tag.tsx | 62 ++++++++--------- app/components/transaction-item/TxItem.tsx | 66 +++++++++++++----- app/hooks/use-display-currency.ts | 69 ++----------------- app/hooks/use-price-conversion.ts | 18 +++-- app/types/amounts.ts | 4 ++ 7 files changed, 173 insertions(+), 117 deletions(-) create mode 100644 __tests__/components/currency-tag.spec.tsx diff --git a/__tests__/components/currency-tag.spec.tsx b/__tests__/components/currency-tag.spec.tsx new file mode 100644 index 000000000..36a8bfbe6 --- /dev/null +++ b/__tests__/components/currency-tag.spec.tsx @@ -0,0 +1,25 @@ +import * as React from "react" +import { render } from "@testing-library/react-native" +import { ThemeProvider } from "@rneui/themed" + +import { CurrencyTag } from "@app/components/currency-tag" +import { WalletCurrency } from "@app/graphql/generated" +import theme from "@app/rne-theme/theme" + +const renderCurrencyTag = (walletCurrency: WalletCurrency) => + render( + + + , + ) + +describe("CurrencyTag", () => { + it.each([WalletCurrency.Btc, WalletCurrency.Usd, WalletCurrency.Usdt])( + "renders a %s tag", + (walletCurrency) => { + const { getByText } = renderCurrencyTag(walletCurrency) + + expect(getByText(walletCurrency)).toBeTruthy() + }, + ) +}) diff --git a/__tests__/hooks/use-display-currency.spec.tsx b/__tests__/hooks/use-display-currency.spec.tsx index f471625b9..81b4d2def 100644 --- a/__tests__/hooks/use-display-currency.spec.tsx +++ b/__tests__/hooks/use-display-currency.spec.tsx @@ -5,7 +5,11 @@ import { MockedProvider } from "@apollo/client/testing" import { PropsWithChildren } from "react" import * as React from "react" import { IsAuthedContextProvider } from "@app/graphql/is-authed-context" -import { CurrencyListDocument, RealtimePriceDocument } from "@app/graphql/generated" +import { + CurrencyListDocument, + RealtimePriceDocument, + WalletCurrency, +} from "@app/graphql/generated" const mocksNgn = [ { @@ -135,6 +139,21 @@ const wrapWithMocks = describe("usePriceConversion", () => { describe("testing moneyAmountToMajorUnitOrSats", () => { + it("formats USDT micros as major units", async () => { + const { result } = renderHook(useDisplayCurrency, { + wrapper: wrapWithMocks([]), + }) + + const moneyAmount = { + amount: 179_554, + currency: WalletCurrency.Usdt, + currencyCode: "USDT", + } + + expect(result.current.moneyAmountToMajorUnitOrSats(moneyAmount)).toBe(0.179554) + expect(result.current.formatMoneyAmount({ moneyAmount })).toBe("$0.179554 USDT") + }) + it("with 0 digits", async () => { const { result, waitForNextUpdate } = renderHook(useDisplayCurrency, { wrapper: wrapWithMocks(mocksJpy), @@ -230,4 +249,29 @@ describe("usePriceConversion", () => { displayCurrency: "NGN", }) }) + + it("converts USDT micros to display currency using USD-cent price", async () => { + const { result, waitFor } = renderHook(useDisplayCurrency, { + wrapper: wrapWithMocks(mocksNgn), + }) + + await waitFor( + () => { + return result.current.displayCurrency === "NGN" + }, + { + timeout: 4000, + }, + ) + + const displayAmount = result.current.moneyAmountToDisplayCurrencyString({ + moneyAmount: { + amount: 1_000_000, + currency: WalletCurrency.Usdt, + currencyCode: "USDT", + }, + }) + + expect(displayAmount).toBe("₦100.00") + }) }) diff --git a/app/components/currency-tag/currency-tag.tsx b/app/components/currency-tag/currency-tag.tsx index 12ba4776c..9b96653c1 100644 --- a/app/components/currency-tag/currency-tag.tsx +++ b/app/components/currency-tag/currency-tag.tsx @@ -3,55 +3,55 @@ import { makeStyles, useTheme } from "@rneui/themed" import React, { FC } from "react" import { Text, View } from "react-native" -const useStyles = makeStyles(() => ({ - currencyTag: { - borderRadius: 10, - height: 30, - width: 50, - justifyContent: "center", - alignItems: "center", - }, - currencyText: { - fontSize: 12, - }, -})) - type CurrencyTagProps = { walletCurrency: WalletCurrency } +type CurrencyStyle = { + backgroundColor: string + textColor: string +} + export const CurrencyTag: FC = ({ walletCurrency }) => { - const styles = useStyles() const { theme: { colors }, } = useTheme() - const currencyStyling = { - BTC: { + const currencyStyling: Record = { + [WalletCurrency.Btc]: { textColor: colors.white, backgroundColor: colors.primary, }, - USD: { + [WalletCurrency.Usd]: { + textColor: colors.black, + backgroundColor: colors.green, + }, + [WalletCurrency.Usdt]: { textColor: colors.black, backgroundColor: colors.green, }, } + const styles = useStyles(currencyStyling[walletCurrency]) + return ( - - - {walletCurrency} - + + {walletCurrency} ) } + +const useStyles = makeStyles((_, currencyStyle: CurrencyStyle) => ({ + currencyTag: { + borderRadius: 10, + height: 30, + width: 50, + justifyContent: "center", + alignItems: "center", + backgroundColor: currencyStyle.backgroundColor, + }, + currencyText: { + fontSize: 12, + color: currencyStyle.textColor, + }, +})) diff --git a/app/components/transaction-item/TxItem.tsx b/app/components/transaction-item/TxItem.tsx index 59ffbd5d4..c80ccd961 100644 --- a/app/components/transaction-item/TxItem.tsx +++ b/app/components/transaction-item/TxItem.tsx @@ -2,7 +2,7 @@ import React from "react" import moment from "moment" import styled from "styled-components/native" import { Text, useTheme } from "@rneui/themed" -import { useHideBalanceQuery } from "@app/graphql/generated" +import { useHideBalanceQuery, WalletCurrency } from "@app/graphql/generated" import { StackNavigationProp } from "@react-navigation/stack" import { RootStackParamList } from "@app/navigation/stack-param-lists" import { PaymentType, PaymentStatus } from "@breeztech/breez-sdk-spark-react-native" @@ -20,7 +20,7 @@ import ArrowDown from "@app/assets/icons/arrow-down.svg" import Icon from "react-native-vector-icons/Ionicons" // utils -import { toBtcMoneyAmount, toUsdMoneyAmount } from "@app/types/amounts" +import { toBtcMoneyAmount, toWalletAmount } from "@app/types/amounts" // types import { @@ -40,11 +40,12 @@ const label = { SEND: "Sent", } -export const TxItem: React.FC = React.memo(({ tx }) => { +const TxItemComponent: React.FC = ({ tx }) => { const navigation = useNavigation>() const { colors } = useTheme().theme - const { formatMoneyAmount, moneyAmountToDisplayCurrencyString } = useDisplayCurrency() + const { formatMoneyAmount, moneyAmountToDisplayCurrencyString, formatCurrency } = + useDisplayCurrency() const { convertMoneyAmount } = usePriceConversion() const { data: { hideBalance = false } = {} } = useHideBalanceQuery() @@ -67,6 +68,7 @@ export const TxItem: React.FC = React.memo(({ tx }) => { // Calculate display amounts let primaryAmount = null let secondaryAmount = null + let feeAmount = null if (isBreezTransaction(tx)) { // Breez transaction - always BTC @@ -77,21 +79,34 @@ export const TxItem: React.FC = React.memo(({ tx }) => { }) secondaryAmount = formatMoneyAmount({ moneyAmount }) } else { - // Ibex transaction - always USD - const amount = tx.transaction.settlementAmount * 100 - const moneyAmount = toUsdMoneyAmount(amount ?? NaN) - primaryAmount = moneyAmountToDisplayCurrencyString({ - moneyAmount, + const moneyAmount = toWalletAmount({ + amount: Math.abs(tx.transaction.settlementAmount), + currency: tx.transaction.settlementCurrency, }) - if (convertMoneyAmount) { + primaryAmount = formatCurrency({ + amountInMajorUnits: tx.transaction.settlementDisplayAmount, + currency: tx.transaction.settlementDisplayCurrency, + }) + + if (tx.transaction.settlementCurrency === WalletCurrency.Usd && convertMoneyAmount) { secondaryAmount = formatMoneyAmount({ moneyAmount: convertMoneyAmount(moneyAmount, "BTC"), }) + } else { + secondaryAmount = formatMoneyAmount({ moneyAmount }) + } + + if (!isReceive && tx.transaction.settlementFee > 0) { + feeAmount = formatMoneyAmount({ + moneyAmount: toWalletAmount({ + amount: tx.transaction.settlementFee, + currency: tx.transaction.settlementCurrency, + }), + }) } } - // Get currency label - Breez is BTC, Ibex is USD - const currencyLabel = isBreezTransaction(tx) ? "BTC" : "USD" + const currencyLabel = isBreezTransaction(tx) ? "BTC" : tx.transaction.settlementCurrency const onPress = () => { if (isIbexTransaction(tx)) { @@ -120,30 +135,39 @@ export const TxItem: React.FC = React.memo(({ tx }) => { )} {memo && ( - + {memo} - + + )} + {feeAmount && ( + + + {`Fee ${feeAmount}`} + + )} {moment(moment.unix(timestamp)).fromNow()} - + {primaryAmount} {secondaryAmount} - + ) -}) +} + +export const TxItem = React.memo(TxItemComponent) const Wrapper = styled.TouchableOpacity` flex-direction: row; @@ -169,3 +193,11 @@ const RowWrapper = styled.View` flex-direction: row; align-items: center; ` + +const MetadataRowWrapper = styled(RowWrapper)` + margin-vertical: 2px; +` + +const AmountColumnWrapper = styled(ColumnWrapper)` + align-items: flex-end; +` diff --git a/app/hooks/use-display-currency.ts b/app/hooks/use-display-currency.ts index 5b874364e..52ba0c0eb 100644 --- a/app/hooks/use-display-currency.ts +++ b/app/hooks/use-display-currency.ts @@ -1,14 +1,11 @@ import { gql } from "@apollo/client" import { useCurrencyListQuery, WalletCurrency } from "@app/graphql/generated" import { useIsAuthed } from "@app/graphql/is-authed-context" -import { ConvertMoneyAmount } from "@app/screens/send-bitcoin-screen/payment-details" import { DisplayAmount, DisplayCurrency, - lessThan, MoneyAmount, - toBtcMoneyAmount, - toUsdMoneyAmount, + USDT_MICROS_PER_USDT, WalletAmount, WalletOrDisplayCurrency, } from "@app/types/amounts" @@ -82,36 +79,6 @@ const formatCurrencyHelper = ({ }${symbol}${amountStr}${currencyCode ? ` ${currencyCode}` : ""}` } -const displayCurrencyHasSignificantMinorUnits = ({ - convertMoneyAmount, - amountInMajorUnitOrSatsToMoneyAmount, -}: { - convertMoneyAmount?: ConvertMoneyAmount - amountInMajorUnitOrSatsToMoneyAmount: ( - amount: number, - currency: WalletOrDisplayCurrency, - ) => MoneyAmount -}) => { - if (!convertMoneyAmount) { - return true - } - - const oneMajorUnitOfDisplayCurrency = amountInMajorUnitOrSatsToMoneyAmount( - 1, - DisplayCurrency, - ) - - const oneUsdCentInDisplayCurrency = convertMoneyAmount( - toUsdMoneyAmount(1), - DisplayCurrency, - ) - - return lessThan({ - value: oneUsdCentInDisplayCurrency, - lessThan: oneMajorUnitOfDisplayCurrency, - }) -} - export const useDisplayCurrency = () => { const { LL } = useI18nContext() const isAuthed = useIsAuthed() @@ -136,8 +103,9 @@ export const useDisplayCurrency = () => { case WalletCurrency.Btc: return moneyAmount.amount case WalletCurrency.Usd: - case WalletCurrency.Usdt: return moneyAmount.amount / 100 + case WalletCurrency.Usdt: + return moneyAmount.amount / USDT_MICROS_PER_USDT case DisplayCurrency: return moneyAmount.amount / 10 ** displayCurrencyInfo.fractionDigits } @@ -145,31 +113,6 @@ export const useDisplayCurrency = () => { [displayCurrencyInfo], ) - const amountInMajorUnitOrSatsToMoneyAmount = useCallback( - ( - amount: number, - currency: WalletOrDisplayCurrency, - ): MoneyAmount => { - switch (currency) { - case WalletCurrency.Btc: - return toBtcMoneyAmount(Math.round(amount)) - case WalletCurrency.Usd: - case WalletCurrency.Usdt: - return toUsdMoneyAmount(Math.round(amount * 100)) - case DisplayCurrency: - return toDisplayMoneyAmount( - Math.round(amount * 10 ** displayCurrencyInfo.fractionDigits), - ) - } - }, - [displayCurrencyInfo, toDisplayMoneyAmount], - ) - - const displayCurrencyShouldDisplayDecimals = displayCurrencyHasSignificantMinorUnits({ - convertMoneyAmount, - amountInMajorUnitOrSatsToMoneyAmount, - }) - const currencyInfo: Record = useMemo(() => { return { [WalletCurrency.Usd]: { @@ -180,7 +123,7 @@ export const useDisplayCurrency = () => { }, [WalletCurrency.Usdt]: { symbol: usdDisplayCurrency.symbol, - minorUnitToMajorUnitOffset: usdDisplayCurrency.fractionDigits, + minorUnitToMajorUnitOffset: 6, showFractionDigits: true, currencyCode: "USDT", }, @@ -260,7 +203,9 @@ export const useDisplayCurrency = () => { symbol: noSymbol ? "" : symbol, fractionDigits: showFractionDigits ? minorUnitToMajorUnitOffset : 0, currencyCode: - moneyAmount.currency === WalletCurrency.Btc && !noSuffix + (moneyAmount.currency === WalletCurrency.Btc || + moneyAmount.currency === WalletCurrency.Usdt) && + !noSuffix ? currencyCode : undefined, }) diff --git a/app/hooks/use-price-conversion.ts b/app/hooks/use-price-conversion.ts index 6d4063e18..1cef1eb80 100644 --- a/app/hooks/use-price-conversion.ts +++ b/app/hooks/use-price-conversion.ts @@ -5,6 +5,7 @@ import { DisplayCurrency, MoneyAmount, moneyAmountIsCurrencyType, + USDT_MICROS_PER_USD_CENT, WalletOrDisplayCurrency, } from "@app/types/amounts" import { useMemo } from "react" @@ -62,25 +63,30 @@ export const usePriceConversion = () => { [WalletCurrency.Btc]: { [DisplayCurrency]: displayCurrencyPerSat, [WalletCurrency.Usd]: displayCurrencyPerSat * (1 / displayCurrencyPerCent), - [WalletCurrency.Usdt]: displayCurrencyPerSat * (1 / displayCurrencyPerCent), + [WalletCurrency.Usdt]: + displayCurrencyPerSat * + (1 / displayCurrencyPerCent) * + USDT_MICROS_PER_USD_CENT, [WalletCurrency.Btc]: 1, }, [WalletCurrency.Usd]: { [DisplayCurrency]: displayCurrencyPerCent, [WalletCurrency.Btc]: displayCurrencyPerCent * (1 / displayCurrencyPerSat), [WalletCurrency.Usd]: 1, - [WalletCurrency.Usdt]: 1, + [WalletCurrency.Usdt]: USDT_MICROS_PER_USD_CENT, }, [WalletCurrency.Usdt]: { - [DisplayCurrency]: displayCurrencyPerCent, - [WalletCurrency.Btc]: displayCurrencyPerCent * (1 / displayCurrencyPerSat), - [WalletCurrency.Usd]: 1, + [DisplayCurrency]: displayCurrencyPerCent / USDT_MICROS_PER_USD_CENT, + [WalletCurrency.Btc]: + (displayCurrencyPerCent / USDT_MICROS_PER_USD_CENT) * + (1 / displayCurrencyPerSat), + [WalletCurrency.Usd]: 1 / USDT_MICROS_PER_USD_CENT, [WalletCurrency.Usdt]: 1, }, [DisplayCurrency]: { [WalletCurrency.Btc]: 1 / displayCurrencyPerSat, [WalletCurrency.Usd]: 1 / displayCurrencyPerCent, - [WalletCurrency.Usdt]: 1 / displayCurrencyPerCent, + [WalletCurrency.Usdt]: (1 / displayCurrencyPerCent) * USDT_MICROS_PER_USD_CENT, [DisplayCurrency]: 1, }, } diff --git a/app/types/amounts.ts b/app/types/amounts.ts index 92c80b796..eebc7de7d 100644 --- a/app/types/amounts.ts +++ b/app/types/amounts.ts @@ -1,5 +1,9 @@ import { WalletCurrency } from "@app/graphql/generated" +export const CENTS_PER_USD = 100 +export const USDT_MICROS_PER_USDT = 1_000_000 +export const USDT_MICROS_PER_USD_CENT = USDT_MICROS_PER_USDT / CENTS_PER_USD + export const DisplayCurrency = "DisplayCurrency" as const export type DisplayCurrency = typeof DisplayCurrency From 0433e734023fc941f73b3c0b0ca08d0f40ace423 Mon Sep 17 00:00:00 2001 From: Island Bitcoin <34528298+islandbitcoin@users.noreply.github.com> Date: Thu, 18 Jun 2026 07:58:00 -0700 Subject: [PATCH 67/95] feat: add bridge cashout confirmation flow (#635) Co-authored-by: Vandana --- app/graphql/front-end-mutations.ts | 33 ++ app/graphql/front-end-queries.ts | 12 + app/graphql/generated.ts | 169 ++++++++ app/navigation/root-navigator.tsx | 11 +- app/navigation/stack-param-lists.ts | 8 +- .../topup-cashout-flow/BankTransfer.tsx | 22 +- .../BridgeAddExternalAccount.tsx | 372 ++++++++++++++++++ .../CashoutConfirmation.tsx | 167 +++++++- .../topup-cashout-flow/CashoutDetails.tsx | 120 ++++-- .../topup-cashout-flow/TopupCashout.tsx | 148 +++---- .../topup-cashout-flow/TopupDetails.tsx | 39 +- app/screens/topup-cashout-flow/index.ts | 2 + 12 files changed, 993 insertions(+), 110 deletions(-) create mode 100644 app/screens/topup-cashout-flow/BridgeAddExternalAccount.tsx diff --git a/app/graphql/front-end-mutations.ts b/app/graphql/front-end-mutations.ts index b8b9cf8b6..b24a78b2b 100644 --- a/app/graphql/front-end-mutations.ts +++ b/app/graphql/front-end-mutations.ts @@ -284,4 +284,37 @@ gql` } } } + + mutation BridgeCancelWithdrawalRequest($input: BridgeCancelWithdrawalRequestInput!) { + bridgeCancelWithdrawalRequest(input: $input) { + withdrawal { + amount + createdAt + currency + externalAccountId + failureReason + id + status + } + errors { + code + message + } + } + } + + mutation BridgeCreateExternalAccount($input: BridgeCreateExternalAccountInput!) { + bridgeCreateExternalAccount(input: $input) { + externalAccount { + id + bankName + accountNumberLast4 + status + } + errors { + code + message + } + } + } ` diff --git a/app/graphql/front-end-queries.ts b/app/graphql/front-end-queries.ts index 145150e32..0826571c5 100644 --- a/app/graphql/front-end-queries.ts +++ b/app/graphql/front-end-queries.ts @@ -373,6 +373,18 @@ gql` } } + query BridgeWithdrawalRequest($id: ID!) { + bridgeWithdrawalRequest(id: $id) { + amount + createdAt + currency + externalAccountId + failureReason + id + status + } + } + query CashWalletCutover { cashWalletCutover { completedAt diff --git a/app/graphql/generated.ts b/app/graphql/generated.ts index 1ab551b27..7bab4bccf 100644 --- a/app/graphql/generated.ts +++ b/app/graphql/generated.ts @@ -358,6 +358,25 @@ export type BridgeExternalAccountLink = { readonly linkUrl: Scalars['String']['output']; }; +export type BridgeCreateExternalAccountInput = { + readonly accountNumber: Scalars['String']['input']; + readonly accountOwnerName: Scalars['String']['input']; + readonly bankName: Scalars['String']['input']; + readonly checkingOrSavings?: InputMaybe; + readonly city: Scalars['String']['input']; + readonly country: Scalars['String']['input']; + readonly postalCode: Scalars['String']['input']; + readonly routingNumber: Scalars['String']['input']; + readonly state: Scalars['String']['input']; + readonly streetLine1: Scalars['String']['input']; +}; + +export type BridgeCreateExternalAccountPayload = { + readonly __typename: 'BridgeCreateExternalAccountPayload'; + readonly errors: ReadonlyArray; + readonly externalAccount?: Maybe; +}; + export type BridgeInitiateKycInput = { readonly email?: InputMaybe; readonly full_name?: InputMaybe; @@ -976,6 +995,7 @@ export type Mutation = { readonly accountUpdateDefaultWalletId: AccountUpdateDefaultWalletIdPayload; readonly accountUpdateDisplayCurrency: AccountUpdateDisplayCurrencyPayload; readonly bridgeAddExternalAccount: BridgeAddExternalAccountPayload; + readonly bridgeCreateExternalAccount?: Maybe; readonly bridgeCancelWithdrawalRequest: BridgeCancelWithdrawalRequestPayload; readonly bridgeCreateVirtualAccount: BridgeCreateVirtualAccountPayload; readonly bridgeInitiateKyc: BridgeInitiateKycPayload; @@ -2582,6 +2602,13 @@ export type BridgeAddExternalAccountMutationVariables = Exact<{ [key: string]: n export type BridgeAddExternalAccountMutation = { readonly __typename: 'Mutation', readonly bridgeAddExternalAccount: { readonly __typename: 'BridgeAddExternalAccountPayload', readonly externalAccount?: { readonly __typename: 'BridgeExternalAccountLink', readonly expiresAt: string, readonly linkUrl: string } | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string }> } }; +export type BridgeCreateExternalAccountMutationVariables = Exact<{ + input: BridgeCreateExternalAccountInput; +}>; + + +export type BridgeCreateExternalAccountMutation = { readonly __typename: 'Mutation', readonly bridgeCreateExternalAccount?: { readonly __typename: 'BridgeCreateExternalAccountPayload', readonly externalAccount?: { readonly __typename: 'BridgeExternalAccount', readonly accountNumberLast4: string, readonly bankName: string, readonly id: string, readonly status: string } | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string }> } | null }; + export type BridgeRequestWithdrawalMutationVariables = Exact<{ input: BridgeRequestWithdrawalInput; }>; @@ -2589,6 +2616,20 @@ export type BridgeRequestWithdrawalMutationVariables = Exact<{ export type BridgeRequestWithdrawalMutation = { readonly __typename: 'Mutation', readonly bridgeRequestWithdrawal: { readonly __typename: 'BridgeRequestWithdrawalPayload', readonly withdrawal?: { readonly __typename: 'BridgeWithdrawal', readonly amount: string, readonly createdAt: string, readonly bridgeTransferId?: string | null, readonly currency: string, readonly externalAccountId?: string | null, readonly failureReason?: string | null, readonly id: string, readonly status: string } | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string }> } }; +export type BridgeCancelWithdrawalRequestMutationVariables = Exact<{ + input: BridgeCancelWithdrawalRequestInput; +}>; + + +export type BridgeCancelWithdrawalRequestMutation = { readonly __typename: 'Mutation', readonly bridgeCancelWithdrawalRequest: { readonly __typename: 'BridgeCancelWithdrawalRequestPayload', readonly withdrawal?: { readonly __typename: 'BridgeWithdrawal', readonly amount: string, readonly createdAt: string, readonly currency: string, readonly externalAccountId?: string | null, readonly failureReason?: string | null, readonly id: string, readonly status: string } | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly code?: string | null, readonly message: string }> } }; + +export type BridgeWithdrawalRequestQueryVariables = Exact<{ + id: Scalars['ID']['input']; +}>; + + +export type BridgeWithdrawalRequestQuery = { readonly __typename: 'Query', readonly bridgeWithdrawalRequest?: { readonly __typename: 'BridgeWithdrawal', readonly amount: string, readonly createdAt: string, readonly currency: string, readonly externalAccountId?: string | null, readonly failureReason?: string | null, readonly id: string, readonly status: string } | null }; + export type BridgeInitiateWithdrawalMutationVariables = Exact<{ input: BridgeInitiateWithdrawalInput; }>; @@ -4421,6 +4462,48 @@ export function useBridgeAddExternalAccountMutation(baseOptions?: Apollo.Mutatio export type BridgeAddExternalAccountMutationHookResult = ReturnType; export type BridgeAddExternalAccountMutationResult = Apollo.MutationResult; export type BridgeAddExternalAccountMutationOptions = Apollo.BaseMutationOptions; +export const BridgeCreateExternalAccountDocument = gql` + mutation BridgeCreateExternalAccount($input: BridgeCreateExternalAccountInput!) { + bridgeCreateExternalAccount(input: $input) { + externalAccount { + id + bankName + accountNumberLast4 + status + } + errors { + code + message + } + } +} + `; +export type BridgeCreateExternalAccountMutationFn = Apollo.MutationFunction; + +/** + * __useBridgeCreateExternalAccountMutation__ + * + * To run a mutation, you first call `useBridgeCreateExternalAccountMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useBridgeCreateExternalAccountMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [bridgeCreateExternalAccountMutation, { data, loading, error }] = useBridgeCreateExternalAccountMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useBridgeCreateExternalAccountMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(BridgeCreateExternalAccountDocument, options); + } +export type BridgeCreateExternalAccountMutationHookResult = ReturnType; +export type BridgeCreateExternalAccountMutationResult = Apollo.MutationResult; +export type BridgeCreateExternalAccountMutationOptions = Apollo.BaseMutationOptions; export const BridgeRequestWithdrawalDocument = gql` mutation BridgeRequestWithdrawal($input: BridgeRequestWithdrawalInput!) { bridgeRequestWithdrawal(input: $input) { @@ -4467,6 +4550,51 @@ export function useBridgeRequestWithdrawalMutation(baseOptions?: Apollo.Mutation export type BridgeRequestWithdrawalMutationHookResult = ReturnType; export type BridgeRequestWithdrawalMutationResult = Apollo.MutationResult; export type BridgeRequestWithdrawalMutationOptions = Apollo.BaseMutationOptions; +export const BridgeCancelWithdrawalRequestDocument = gql` + mutation BridgeCancelWithdrawalRequest($input: BridgeCancelWithdrawalRequestInput!) { + bridgeCancelWithdrawalRequest(input: $input) { + withdrawal { + amount + createdAt + currency + externalAccountId + failureReason + id + status + } + errors { + code + message + } + } +} + `; +export type BridgeCancelWithdrawalRequestMutationFn = Apollo.MutationFunction; + +/** + * __useBridgeCancelWithdrawalRequestMutation__ + * + * To run a mutation, you first call `useBridgeCancelWithdrawalRequestMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useBridgeCancelWithdrawalRequestMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [bridgeCancelWithdrawalRequestMutation, { data, loading, error }] = useBridgeCancelWithdrawalRequestMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useBridgeCancelWithdrawalRequestMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(BridgeCancelWithdrawalRequestDocument, options); + } +export type BridgeCancelWithdrawalRequestMutationHookResult = ReturnType; +export type BridgeCancelWithdrawalRequestMutationResult = Apollo.MutationResult; +export type BridgeCancelWithdrawalRequestMutationOptions = Apollo.BaseMutationOptions; export const BridgeInitiateWithdrawalDocument = gql` mutation BridgeInitiateWithdrawal($input: BridgeInitiateWithdrawalInput!) { bridgeInitiateWithdrawal(input: $input) { @@ -5553,6 +5681,47 @@ export function useBridgeExternalAccountsLazyQuery(baseOptions?: Apollo.LazyQuer export type BridgeExternalAccountsQueryHookResult = ReturnType; export type BridgeExternalAccountsLazyQueryHookResult = ReturnType; export type BridgeExternalAccountsQueryResult = Apollo.QueryResult; +export const BridgeWithdrawalRequestDocument = gql` + query BridgeWithdrawalRequest($id: ID!) { + bridgeWithdrawalRequest(id: $id) { + amount + createdAt + currency + externalAccountId + failureReason + id + status + } +} + `; + +/** + * __useBridgeWithdrawalRequestQuery__ + * + * To run a query within a React component, call `useBridgeWithdrawalRequestQuery` and pass it any options that fit your needs. + * When your component renders, `useBridgeWithdrawalRequestQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useBridgeWithdrawalRequestQuery({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useBridgeWithdrawalRequestQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(BridgeWithdrawalRequestDocument, options); + } +export function useBridgeWithdrawalRequestLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(BridgeWithdrawalRequestDocument, options); + } +export type BridgeWithdrawalRequestQueryHookResult = ReturnType; +export type BridgeWithdrawalRequestLazyQueryHookResult = ReturnType; +export type BridgeWithdrawalRequestQueryResult = Apollo.QueryResult; export const CashWalletCutoverDocument = gql` query CashWalletCutover { cashWalletCutover { diff --git a/app/navigation/root-navigator.tsx b/app/navigation/root-navigator.tsx index 00cc5c4ce..82c59d404 100644 --- a/app/navigation/root-navigator.tsx +++ b/app/navigation/root-navigator.tsx @@ -14,7 +14,6 @@ import { ContactsDetailScreen, ContactsScreen } from "../screens/contacts-screen import { CardScreen, FlashcardTopup } from "../screens/card-screen" import { ChatList } from "@app/screens/chat" import { DeveloperScreen } from "../screens/developer-screen" -import { EarnMapScreen } from "../screens/earns-map-screen" import { EarnQuiz, EarnSection } from "../screens/earns-screen" import { SectionCompleted } from "../screens/earns-screen/section-completed" import { GetStartedScreen } from "../screens/get-started-screen" @@ -61,7 +60,6 @@ import { TotpRegistrationInitiateScreen, TotpRegistrationValidateScreen, } from "@app/screens/totp-screen" -import { testProps } from "@app/utils/testProps" import { makeStyles, useTheme } from "@rneui/themed" import { ScanningQRCodeScreen } from "../screens/send-bitcoin-screen" import { SettingsScreen } from "../screens/settings-screen" @@ -128,6 +126,7 @@ import { BankTransfer, BridgeKycWebView, BridgeExternalAccountWebView, + BridgeAddExternalAccount, TopupDetails, TopupSuccess, TopupCashout, @@ -686,6 +685,14 @@ export const RootStack = () => { cardStyleInterpolator: CardStyleInterpolators.forModalPresentationIOS, }} /> + diff --git a/app/navigation/stack-param-lists.ts b/app/navigation/stack-param-lists.ts index 3b17d336e..1cfa9389a 100644 --- a/app/navigation/stack-param-lists.ts +++ b/app/navigation/stack-param-lists.ts @@ -144,7 +144,12 @@ export type RootStackParamList = { UnclaimedDepositDetails: { deposit: DepositInfo } RefundDeposit: { deposit: DepositInfo } CashoutDetails: { type: "local" | "bridge" } - CashoutConfirmation: { offer: CashoutOffer } + CashoutConfirmation: + | { offer: CashoutOffer } + | { + bridgeWithdrawalId: string + bridgeAccountLabel?: string + } CashoutSuccess: undefined EditNostrProfile: undefined NostrSettingsScreen: undefined @@ -169,6 +174,7 @@ export type RootStackParamList = { BridgeExternalAccountWebView: { linkUrl: string } + BridgeAddExternalAccount: undefined TopupCashout: undefined TopupDetails: { paymentType: "card" | "bankTransfer" | "bridge" } BankTransfer: { diff --git a/app/screens/topup-cashout-flow/BankTransfer.tsx b/app/screens/topup-cashout-flow/BankTransfer.tsx index ece91e7da..18f8e456e 100644 --- a/app/screens/topup-cashout-flow/BankTransfer.tsx +++ b/app/screens/topup-cashout-flow/BankTransfer.tsx @@ -21,15 +21,19 @@ const BankTransfer: React.FC = ({ navigation, route }) => { const styles = useStyles() const { bottom } = useSafeAreaInsets() const { LL } = useI18nContext() + const backHomeButtonStyle = React.useMemo( + () => [styles.backHomeButton, { marginBottom: bottom + 20 }], + [bottom, styles.backHomeButton], + ) const { data } = useBridgeVirtualAccountQuery() - const { amount, wallet, paymentType } = route.params + const { amount, paymentType } = route.params const fee = amount * 0.02 if (paymentType === "bridge") { return ( - + {LL.BankTransfer.virtualBankTransfer()} @@ -54,12 +58,17 @@ const BankTransfer: React.FC = ({ navigation, route }) => { {data?.bridgeVirtualAccount?.routingNumber} + navigation.reset({ index: 0, routes: [{ name: "Primary" }] })} + btnStyle={backHomeButtonStyle} + /> ) } return ( - + {LL.BankTransfer.title()} @@ -70,7 +79,7 @@ const BankTransfer: React.FC = ({ navigation, route }) => { {LL.BankTransfer.desc2({ code: "UUM7MJRD" })} - {LL.BankTransfer.desc3({ amount: amount, fee: fee })} + {LL.BankTransfer.desc3({ amount, fee })} @@ -137,7 +146,7 @@ const BankTransfer: React.FC = ({ navigation, route }) => { navigation.reset({ index: 0, routes: [{ name: "Primary" }] })} - btnStyle={{ marginBottom: bottom + 20, marginTop: 20 }} + btnStyle={backHomeButtonStyle} /> ) @@ -182,4 +191,7 @@ const useStyles = makeStyles(({ colors }) => ({ buttonGroup: { marginTop: 8, }, + backHomeButton: { + marginTop: 20, + }, })) diff --git a/app/screens/topup-cashout-flow/BridgeAddExternalAccount.tsx b/app/screens/topup-cashout-flow/BridgeAddExternalAccount.tsx new file mode 100644 index 000000000..6c84ea015 --- /dev/null +++ b/app/screens/topup-cashout-flow/BridgeAddExternalAccount.tsx @@ -0,0 +1,372 @@ +import React, { useState } from "react" +import { + Alert, + KeyboardAvoidingView, + Platform, + ScrollView, + TextInput, + TouchableOpacity, + View, +} from "react-native" +import { StackScreenProps } from "@react-navigation/stack" +import { RootStackParamList } from "@app/navigation/stack-param-lists" +import { Text, makeStyles, useTheme } from "@rneui/themed" +import { Screen } from "@app/components/screen" +import { useBridgeCreateExternalAccountMutation } from "@app/graphql/generated" +import { useActivityIndicator } from "@app/hooks" + +type Props = StackScreenProps + +const BridgeAddExternalAccount: React.FC = ({ navigation }) => { + const styles = useStyles() + const { theme } = useTheme() + const { toggleActivityIndicator } = useActivityIndicator() + + const [createExternalAccount] = useBridgeCreateExternalAccountMutation() + + const [bankName, setBankName] = useState("") + const [accountOwnerName, setAccountOwnerName] = useState("") + const [routingNumber, setRoutingNumber] = useState("") + const [accountNumber, setAccountNumber] = useState("") + const [checkingOrSavings, setCheckingOrSavings] = useState<"checking" | "savings">( + "checking", + ) + const [streetLine1, setStreetLine1] = useState("") + const [city, setCity] = useState("") + const [state, setState] = useState("") + const [postalCode, setPostalCode] = useState("") + const [country, setCountry] = useState("USA") + + const handleSubmit = async () => { + if (!bankName || !accountOwnerName || !routingNumber || !accountNumber) { + Alert.alert("Error", "Please fill in all required fields") + return + } + + if (!streetLine1 || !city || !state || !postalCode || !country) { + Alert.alert("Error", "Please fill in your address details") + return + } + + toggleActivityIndicator(true) + try { + const res = await createExternalAccount({ + variables: { + input: { + bankName, + accountOwnerName, + routingNumber, + accountNumber, + checkingOrSavings, + streetLine1, + city, + state, + postalCode, + country, + }, + }, + }) + + toggleActivityIndicator(false) + + const errors = res.data?.bridgeCreateExternalAccount?.errors + if (errors && errors.length > 0) { + Alert.alert("Error", errors[0].message) + return + } + + const externalAccount = res.data?.bridgeCreateExternalAccount?.externalAccount + if (externalAccount) { + Alert.alert( + "Bank Account Added", + `Your ${externalAccount.bankName} account (ending in ${externalAccount.accountNumberLast4}) has been linked successfully.`, + [ + { + text: "Continue", + onPress: () => navigation.navigate("CashoutDetails", { type: "bridge" }), + }, + ], + ) + } else { + Alert.alert("Error", "Failed to add bank account. Please try again.") + } + } catch (error) { + toggleActivityIndicator(false) + Alert.alert("Error", "Something went wrong. Please try again.") + } + } + + return ( + + + + + Add Bank Account + + + Enter your US bank account details below to link it for withdrawals. + + + + Bank Name + + + + + Account Owner Name + + + + + + Routing Number + + + + Account Number + + + + + + Account Type + + setCheckingOrSavings("checking")} + > + + Checking + + + setCheckingOrSavings("savings")} + > + + Savings + + + + + + + Your Address + + + + Street Address + + + + + + City + + + + State + + + + + + + ZIP Code + + + + Country + + + + + + Link Bank Account + + + + + ) +} + +const useStyles = makeStyles(({ colors }) => ({ + container: { + flex: 1, + }, + scrollContent: { + padding: 20, + }, + title: { + marginBottom: 8, + }, + subtitle: { + color: colors.grey2, + marginBottom: 24, + }, + fieldGroup: { + marginBottom: 16, + }, + fieldRow: { + flexDirection: "row", + marginBottom: 0, + }, + fieldRowLeft: { + flex: 1, + marginRight: 8, + }, + fieldRowRight: { + flex: 1, + marginLeft: 8, + }, + fieldRowCity: { + flex: 2, + marginRight: 8, + }, + fieldRowState: { + flex: 1, + marginLeft: 8, + }, + label: { + fontSize: 14, + fontWeight: "600", + marginBottom: 6, + color: colors.grey0, + }, + input: { + borderWidth: 1, + borderColor: colors.grey3, + borderRadius: 8, + padding: 12, + fontSize: 16, + color: colors.grey0, + backgroundColor: colors.white, + }, + segmentRow: { + flexDirection: "row", + gap: 8, + }, + segmentButton: { + flex: 1, + paddingVertical: 12, + paddingHorizontal: 16, + borderRadius: 8, + borderWidth: 1, + borderColor: colors.grey3, + alignItems: "center", + }, + segmentButtonActive: { + backgroundColor: colors.primary, + borderColor: colors.primary, + }, + segmentText: { + fontSize: 15, + fontWeight: "500", + color: colors.grey1, + }, + segmentTextActive: { + color: colors.white, + }, + sectionTitle: { + marginTop: 8, + marginBottom: 16, + }, + submitButton: { + backgroundColor: colors.primary, + paddingVertical: 16, + borderRadius: 12, + alignItems: "center", + marginTop: 24, + marginBottom: 40, + }, + submitText: { + color: colors.white, + fontSize: 17, + fontWeight: "600", + }, +})) + +export default BridgeAddExternalAccount diff --git a/app/screens/topup-cashout-flow/CashoutConfirmation.tsx b/app/screens/topup-cashout-flow/CashoutConfirmation.tsx index a7bbd8bb6..b44553b70 100644 --- a/app/screens/topup-cashout-flow/CashoutConfirmation.tsx +++ b/app/screens/topup-cashout-flow/CashoutConfirmation.tsx @@ -1,7 +1,6 @@ import React, { useState } from "react" -import { ScrollView } from "react-native" -import { makeStyles } from "@rneui/themed" -import { Text, useTheme } from "@rneui/themed" +import { ScrollView, View } from "react-native" +import { makeStyles, Text, useTheme } from "@rneui/themed" import moment from "moment" // components @@ -17,9 +16,15 @@ import { import { useI18nContext } from "@app/i18n/i18n-react" import { useSafeAreaInsets } from "react-native-safe-area-context" import { useActivityIndicator, useDisplayCurrency } from "@app/hooks" -import { useCashoutScreenQuery, useInitiateCashoutMutation } from "@app/graphql/generated" +import { + useBridgeCancelWithdrawalRequestMutation, + useBridgeInitiateWithdrawalMutation, + useBridgeWithdrawalRequestQuery, + useCashoutScreenQuery, + useInitiateCashoutMutation, +} from "@app/graphql/generated" -//utils +// utils import { getCashWallet } from "@app/graphql/wallets-utils" import { toUsdMoneyAmount } from "@app/types/amounts" import { StackScreenProps } from "@react-navigation/stack" @@ -27,13 +32,29 @@ import { RootStackParamList } from "@app/navigation/stack-param-lists" type Props = StackScreenProps -const CashoutConfirmation: React.FC = ({ navigation, route }) => { +// The Bridge service fee charged by Flash (0.5%), included in the withdrawal amount. +const FLASH_BRIDGE_FEE_RATE = 0.005 + +const CashoutConfirmation: React.FC = (props) => { + if ("bridgeWithdrawalId" in props.route.params) { + return + } + return +} + +export default CashoutConfirmation + +const LocalCashoutConfirmation: React.FC = ({ navigation, route }) => { const styles = useStyles() const { colors } = useTheme().theme const { bottom } = useSafeAreaInsets() const { LL } = useI18nContext() - const { moneyAmountToDisplayCurrencyString, displayCurrency } = useDisplayCurrency() + const { moneyAmountToDisplayCurrencyString } = useDisplayCurrency() const { toggleActivityIndicator } = useActivityIndicator() + const confirmButtonStyle = React.useMemo( + () => [styles.confirmButton, { marginBottom: bottom + 10 }], + [bottom, styles.confirmButton], + ) const [errorMsg, setErrorMsg] = useState() @@ -47,6 +68,8 @@ const CashoutConfirmation: React.FC = ({ navigation, route }) => { const usdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) const usdBalance = toUsdMoneyAmount(usdWallet?.balance ?? NaN) + // Narrowed by the dispatcher: this component only renders for the offer variant. + const params = route.params as Extract const { walletId, offerId, @@ -56,7 +79,7 @@ const CashoutConfirmation: React.FC = ({ navigation, route }) => { receiveUsd, receiveJmd, flashFee, - } = route.params?.offer + } = params.offer const onConfirm = async () => { toggleActivityIndicator(true) @@ -83,7 +106,7 @@ const CashoutConfirmation: React.FC = ({ navigation, route }) => { return ( - + {LL.Cashout.valid({ time: moment(expiresAt).fromNow(true) })} @@ -103,7 +126,7 @@ const CashoutConfirmation: React.FC = ({ navigation, route }) => { )} - {!!errorMsg && ( + {Boolean(errorMsg) && ( {errorMsg} @@ -111,18 +134,138 @@ const CashoutConfirmation: React.FC = ({ navigation, route }) => { ) } -export default CashoutConfirmation +const BridgeCashoutConfirmation: React.FC = ({ navigation, route }) => { + const styles = useStyles() + const { colors } = useTheme().theme + const { bottom } = useSafeAreaInsets() + const { LL } = useI18nContext() + const { moneyAmountToDisplayCurrencyString } = useDisplayCurrency() + const { toggleActivityIndicator } = useActivityIndicator() + const actionButtonsStyle = React.useMemo( + () => [styles.actionButtons, { marginBottom: bottom + 10 }], + [bottom, styles.actionButtons], + ) + + const [errorMsg, setErrorMsg] = useState() + + // Narrowed by the dispatcher: this component only renders for the bridge variant. + const params = route.params as Extract< + typeof route.params, + { bridgeWithdrawalId: string } + > + const { bridgeWithdrawalId, bridgeAccountLabel } = params + + const [initiateWithdrawal] = useBridgeInitiateWithdrawalMutation() + const [cancelWithdrawal] = useBridgeCancelWithdrawalRequestMutation() + + const { data: cashoutData } = useCashoutScreenQuery({ + fetchPolicy: "cache-first", + returnPartialData: true, + }) + const usdWallet = getCashWallet(cashoutData?.me?.defaultAccount?.wallets) + const usdBalance = toUsdMoneyAmount(usdWallet?.balance ?? NaN) + + const { data, loading } = useBridgeWithdrawalRequestQuery({ + variables: { id: bridgeWithdrawalId }, + fetchPolicy: "network-only", + }) + + const withdrawal = data?.bridgeWithdrawalRequest + // Backend amount is a decimal USD-dollar string (e.g. "10.50"); convert to cents. + const amountCents = Math.round(parseFloat(withdrawal?.amount ?? "0") * 100) + const feeCents = Math.round(amountCents * FLASH_BRIDGE_FEE_RATE) + + const formattedAmount = moneyAmountToDisplayCurrencyString({ + moneyAmount: toUsdMoneyAmount(amountCents), + }) + const formattedFee = moneyAmountToDisplayCurrencyString({ + moneyAmount: toUsdMoneyAmount(feeCents), + }) + + const onConfirm = async () => { + setErrorMsg(undefined) + toggleActivityIndicator(true) + try { + const res = await initiateWithdrawal({ + variables: { input: { withdrawalId: bridgeWithdrawalId } }, + }) + const errors = res.data?.bridgeInitiateWithdrawal.errors + if (errors?.length) { + setErrorMsg(errors[0].message) + return + } + navigation.navigate("CashoutSuccess") + } finally { + toggleActivityIndicator(false) + } + } + + const onCancel = async () => { + setErrorMsg(undefined) + toggleActivityIndicator(true) + try { + await cancelWithdrawal({ + variables: { input: { withdrawalId: bridgeWithdrawalId } }, + }) + } finally { + toggleActivityIndicator(false) + navigation.goBack() + } + } + + return ( + + + + + + {Boolean(bridgeAccountLabel) && ( + + )} + {Boolean(errorMsg) && ( + + {errorMsg} + + )} + + + + + + + ) +} const useStyles = makeStyles(() => ({ + scrollView: { + padding: 20, + }, valid: { alignSelf: "center", marginBottom: 10, }, + confirmButton: { + marginHorizontal: 20, + }, + actionButtons: { + marginHorizontal: 20, + rowGap: 10, + }, })) diff --git a/app/screens/topup-cashout-flow/CashoutDetails.tsx b/app/screens/topup-cashout-flow/CashoutDetails.tsx index b87d91e11..5adb964af 100644 --- a/app/screens/topup-cashout-flow/CashoutDetails.tsx +++ b/app/screens/topup-cashout-flow/CashoutDetails.tsx @@ -12,6 +12,8 @@ import { CashoutFromWallet, CashoutPercentage } from "@app/components/topup-cash // hooks import { useBankAccountsQuery, + useBridgeExternalAccountsQuery, + useBridgeRequestWithdrawalMutation, useCashoutScreenQuery, useRequestCashoutMutation, } from "@app/graphql/generated" @@ -32,25 +34,40 @@ import { useSafeAreaInsets } from "react-native-safe-area-context" type Props = StackScreenProps -const CashoutDetails = ({ navigation }: Props) => { +const CashoutDetails = ({ navigation, route }: Props) => { const styles = useStyles() const { colors } = useTheme().theme const { bottom } = useSafeAreaInsets() + const nextButtonStyle = React.useMemo( + () => [styles.nextButton, { marginBottom: bottom + 10 }], + [bottom, styles.nextButton], + ) const { LL } = useI18nContext() const { zeroDisplayAmount } = useDisplayCurrency() const { convertMoneyAmount } = usePriceConversion() const { toggleActivityIndicator } = useActivityIndicator() + const { type } = route.params + const isBridge = type === "bridge" + const [errorMsg, setErrorMsg] = useState() const [moneyAmount, setMoneyAmount] = useState>(zeroDisplayAmount) const [requestCashout] = useRequestCashoutMutation() + const [bridgeRequestWithdrawal] = useBridgeRequestWithdrawalMutation() - const { data: bankAccountsData, loading } = useBankAccountsQuery({ + const { data: bankAccountsData, loading: bankLoading } = useBankAccountsQuery({ fetchPolicy: "network-only", + skip: isBridge, }) + const { data: externalAccountsData, loading: bridgeLoading } = + useBridgeExternalAccountsQuery({ + fetchPolicy: "network-only", + skip: !isBridge, + }) + const { data } = useCashoutScreenQuery({ fetchPolicy: "cache-first", returnPartialData: true, @@ -63,36 +80,89 @@ const CashoutDetails = ({ navigation }: Props) => { const usdWallet = getCashWallet(data?.me?.defaultAccount?.wallets) const usdBalance = toUsdMoneyAmount(usdWallet?.balance ?? NaN) const settlementSendAmount = convertMoneyAmount(moneyAmount, "USD") + const accountsLoading = isBridge ? bridgeLoading : bankLoading const isValidAmount = settlementSendAmount.amount > 0 && settlementSendAmount.amount <= usdBalance.amount && - !loading + !accountsLoading const onNext = async () => { + setErrorMsg(undefined) + + if (!usdWallet) { + setErrorMsg(LL.common.error()) + return + } + + if (isBridge) { + await onBridgeWithdraw() + return + } + const defaultBankAccount = bankAccountsData?.me?.bankAccounts.find((el) => el.isDefault) || bankAccountsData?.me?.bankAccounts[0] - if (usdWallet && defaultBankAccount?.id) { - toggleActivityIndicator(true) - const res = await requestCashout({ - variables: { - input: { - bankAccountId: defaultBankAccount.id, - walletId: usdWallet.id, - amount: settlementSendAmount.amount, - }, + if (!defaultBankAccount?.id) { + setErrorMsg("No bank account found. Please add a bank account first.") + return + } + + toggleActivityIndicator(true) + const res = await requestCashout({ + variables: { + input: { + bankAccountId: defaultBankAccount.id, + walletId: usdWallet.id, + amount: settlementSendAmount.amount, }, + }, + }) + if (res.data?.requestCashout.offer) { + navigation.navigate("CashoutConfirmation", { + offer: res.data.requestCashout.offer, }) - console.log("Response: ", res.data?.requestCashout) - if (res.data?.requestCashout.offer) { - navigation.navigate("CashoutConfirmation", { - offer: res.data.requestCashout.offer, - }) - } else { - setErrorMsg(res.data?.requestCashout.errors[0].message) + } else { + setErrorMsg(res.data?.requestCashout.errors[0].message) + } + + toggleActivityIndicator(false) + } + + const onBridgeWithdraw = async () => { + const externalAccount = externalAccountsData?.bridgeExternalAccounts?.find(Boolean) + + if (!externalAccount?.id) { + setErrorMsg("No external account found. Please add an external account first.") + return + } + + toggleActivityIndicator(true) + try { + // Backend expects a decimal USD-dollar string (e.g. "10.50"), max 6 decimals. + const amount = (settlementSendAmount.amount / 100).toFixed(2) + const requestRes = await bridgeRequestWithdrawal({ + variables: { input: { externalAccountId: externalAccount.id, amount } }, + }) + + const requestErrors = requestRes.data?.bridgeRequestWithdrawal.errors + const withdrawalId = requestRes.data?.bridgeRequestWithdrawal.withdrawal?.id + if (requestErrors?.length) { + setErrorMsg(requestErrors[0].message) + return + } + if (!withdrawalId) { + setErrorMsg(LL.common.error()) + return } + // bridgeRequestWithdrawal only stores a pending record; the user reviews and + // confirms (bridgeInitiateWithdrawal) on the confirmation screen. + navigation.navigate("CashoutConfirmation", { + bridgeWithdrawalId: withdrawalId, + bridgeAccountLabel: `${externalAccount.bankName} ••${externalAccount.accountNumberLast4}`, + }) + } finally { toggleActivityIndicator(false) } } @@ -111,7 +181,7 @@ const CashoutDetails = ({ navigation }: Props) => { - + {LL.SendBitcoinScreen.amount()} { /> - {!!errorMsg && ( + {Boolean(errorMsg) && ( {errorMsg} @@ -136,7 +206,7 @@ const CashoutDetails = ({ navigation }: Props) => { @@ -152,4 +222,10 @@ const useStyles = makeStyles(() => ({ flexDirection: "column", margin: 20, }, + amountLabel: { + marginBottom: 5, + }, + nextButton: { + marginHorizontal: 20, + }, })) diff --git a/app/screens/topup-cashout-flow/TopupCashout.tsx b/app/screens/topup-cashout-flow/TopupCashout.tsx index 6ffe42bca..acad8ac67 100644 --- a/app/screens/topup-cashout-flow/TopupCashout.tsx +++ b/app/screens/topup-cashout-flow/TopupCashout.tsx @@ -8,8 +8,11 @@ import { useFocusEffect } from "@react-navigation/native" // components import { Screen } from "@app/components/screen" -import { TransferOptionModal, BridgeKycModal } from "@app/components/topup-cashout-flow" -import type { TransferOption } from "@app/components/topup-cashout-flow" +import { + BridgeKycModal, + type TransferOption, + TransferOptionModal, +} from "@app/components/topup-cashout-flow" // assets import ArrowDown from "@app/assets/icons/arrow-down-to-bracket.svg" @@ -17,6 +20,7 @@ import ArrowUp from "@app/assets/icons/arrow-up-from-bracket.svg" // hooks import { + AccountLevel, useBridgeAddExternalAccountMutation, useBridgeExternalAccountsQuery, useBridgeInitiateKycMutation, @@ -25,9 +29,6 @@ import { import { useActivityIndicator } from "@app/hooks" import { useLevel } from "@app/graphql/level-context" -// utils -import { AccountLevel } from "@app/graphql/generated" - type Props = StackScreenProps const TopupCashout: React.FC = ({ navigation }) => { @@ -67,6 +68,79 @@ const TopupCashout: React.FC = ({ navigation }) => { setRefreshing(false) }, [refetchKycStatus, refetchExternalAccounts]) + const checkAccountLevel = useCallback( + (type: "card" | "bankTransfer" | "cashout") => { + if (type === "card") { + navigation.navigate("TopupDetails", { paymentType: type }) + } else if (currentLevel === AccountLevel.One) { + Alert.alert( + "Account upgrade required", + "You should upgrade your account to use this feature", + [ + { text: "Continue", onPress: () => navigation.navigate("AccountType") }, + { text: "Cancel", style: "cancel" }, + ], + ) + } else if (type === "cashout") { + navigation.navigate("CashoutDetails", { type: "local" }) + } else { + navigation.navigate("TopupDetails", { paymentType: type }) + } + }, + [currentLevel, navigation], + ) + + const checkBridgeKyc = useCallback( + async (type: "topup" | "settle") => { + if (kycStatusData?.bridgeKycStatus === "pending") { + Alert.alert( + "KYC Pending", + "Your KYC status is pending. Please wait for approval.", + ) + } else if (kycStatusData?.bridgeKycStatus === "approved") { + if (type === "topup") { + navigation.navigate("TopupDetails", { paymentType: "bridge" }) + } else if ( + externalAccountsData?.bridgeExternalAccounts && + externalAccountsData.bridgeExternalAccounts.length > 0 + ) { + navigation.navigate("CashoutDetails", { type: "bridge" }) + } else { + toggleActivityIndicator(true) + const res = await addExternalAccount() + toggleActivityIndicator(false) + + const errors = res.data?.bridgeAddExternalAccount?.errors + if (errors && errors.length > 0) { + // If Plaid Link is unavailable, offer manual bank entry as fallback + if (errors[0].code === "BRIDGE_PLAID_NOT_AVAILABLE") { + navigation.navigate("BridgeAddExternalAccount") + return + } + Alert.alert("Error", errors[0].message) + return + } + + const linkUrl = res.data?.bridgeAddExternalAccount?.externalAccount?.linkUrl + if (linkUrl) { + navigation.navigate("BridgeExternalAccountWebView", { linkUrl }) + } else { + Alert.alert("Error", "Failed to get external account link. Please try again.") + } + } + } else { + setBridgeKycModalVisible(true) + } + }, + [ + addExternalAccount, + externalAccountsData?.bridgeExternalAccounts, + kycStatusData?.bridgeKycStatus, + navigation, + toggleActivityIndicator, + ], + ) + const topupOptions: TransferOption[] = useMemo( () => [ { @@ -98,7 +172,7 @@ const TopupCashout: React.FC = ({ navigation }) => { }, }, ], - [LL, navigation, kycStatusData?.bridgeKycStatus], + [LL, checkAccountLevel, checkBridgeKyc, kycStatusData?.bridgeKycStatus], ) const settleOptions: TransferOption[] = useMemo( @@ -123,68 +197,9 @@ const TopupCashout: React.FC = ({ navigation }) => { }, }, ], - [LL, navigation, kycStatusData?.bridgeKycStatus], + [LL, checkAccountLevel, checkBridgeKyc, kycStatusData?.bridgeKycStatus], ) - const checkAccountLevel = (type: "card" | "bankTransfer" | "cashout") => { - if (type === "card") { - navigation.navigate("TopupDetails", { paymentType: type }) - } else { - if (currentLevel === AccountLevel.One) { - Alert.alert( - "Account upgrade required", - "You should upgrade your account to use this feature", - [ - { text: "Continue", onPress: () => navigation.navigate("AccountType") }, - { text: "Cancel", style: "cancel" }, - ], - ) - } else { - if (type === "cashout") { - navigation.navigate("CashoutDetails", { type: "local" }) - } else { - navigation.navigate("TopupDetails", { paymentType: type }) - } - } - } - } - - const checkBridgeKyc = async (type: "topup" | "settle") => { - if (kycStatusData?.bridgeKycStatus === "pending") { - Alert.alert("KYC Pending", "Your KYC status is pending. Please wait for approval.") - } else if (kycStatusData?.bridgeKycStatus === "approved") { - if (type === "topup") { - navigation.navigate("TopupDetails", { paymentType: "bridge" }) - } else { - if ( - externalAccountsData?.bridgeExternalAccounts && - externalAccountsData.bridgeExternalAccounts.length > 0 - ) { - navigation.navigate("CashoutDetails", { type: "bridge" }) - } else { - toggleActivityIndicator(true) - const res = await addExternalAccount() - toggleActivityIndicator(false) - - const errors = res.data?.bridgeAddExternalAccount?.errors - if (errors && errors.length > 0) { - Alert.alert("Error", errors[0].message) - return - } - - const linkUrl = res.data?.bridgeAddExternalAccount?.externalAccount?.linkUrl - if (linkUrl) { - navigation.navigate("BridgeExternalAccountWebView", { linkUrl }) - } else { - Alert.alert("Error", "Failed to get external account link. Please try again.") - } - } - } - } else { - setBridgeKycModalVisible(true) - } - } - const getBridgeKycLink = async (data: { fullName: string email: string @@ -195,6 +210,7 @@ const TopupCashout: React.FC = ({ navigation }) => { const res = await initiateBridgeKyc({ variables: { input: { + // eslint-disable-next-line camelcase full_name: data.fullName, email: data.email, type: data.kycType, diff --git a/app/screens/topup-cashout-flow/TopupDetails.tsx b/app/screens/topup-cashout-flow/TopupDetails.tsx index 827a4c0b9..9d7deb03d 100644 --- a/app/screens/topup-cashout-flow/TopupDetails.tsx +++ b/app/screens/topup-cashout-flow/TopupDetails.tsx @@ -14,7 +14,15 @@ */ import React, { useState } from "react" -import { View, TextInput, Alert } from "react-native" +import { + View, + TextInput, + Alert, + InputAccessoryView, + Keyboard, + TouchableOpacity, + Platform, +} from "react-native" import { Text, makeStyles, useTheme } from "@rneui/themed" import { StackScreenProps } from "@react-navigation/stack" import { RootStackParamList } from "@app/navigation/stack-param-lists" @@ -142,7 +150,7 @@ const TopupDetails: React.FC = ({ navigation, route }) => { } return ( - + {route.params.paymentType === "card" @@ -175,7 +183,17 @@ const TopupDetails: React.FC = ({ navigation, route }) => { value={amount} onChangeText={setAmount} keyboardType="numeric" + inputAccessoryViewID="topupAmountAccessory" /> + {Platform.OS === "ios" && ( + + + + Done + + + + )} = ({ navigation, route }) => { } const useStyles = makeStyles(({ colors }) => (props: { bottom: number }) => ({ + keyboardAccessory: { + flexDirection: "row" as const, + justifyContent: "flex-end" as const, + alignItems: "center" as const, + paddingHorizontal: 16, + paddingVertical: 8, + backgroundColor: colors.white, + borderTopWidth: 1, + borderTopColor: colors.grey3, + }, + doneButton: { + fontSize: 16, + fontWeight: "600" as const, + color: colors.primary, + paddingHorizontal: 8, + paddingVertical: 4, + }, container: { flex: 1, paddingHorizontal: 20, diff --git a/app/screens/topup-cashout-flow/index.ts b/app/screens/topup-cashout-flow/index.ts index be71ee9b0..2d1350c07 100644 --- a/app/screens/topup-cashout-flow/index.ts +++ b/app/screens/topup-cashout-flow/index.ts @@ -1,5 +1,6 @@ import BridgeKycWebView from "./BridgeKycWebView" import BridgeExternalAccountWebView from "./BridgeExternalAccountWebView" +import BridgeAddExternalAccount from "./BridgeAddExternalAccount" import TopupCashout from "./TopupCashout" import TopupDetails from "./TopupDetails" import BankTransfer from "./BankTransfer" @@ -13,6 +14,7 @@ import PaymentSuccessScreen from "./payment-success-screen" export { BridgeKycWebView, BridgeExternalAccountWebView, + BridgeAddExternalAccount, TopupCashout, TopupDetails, BankTransfer, From dfa55703efb1f7b26580804319c6c12ecb11eb6d Mon Sep 17 00:00:00 2001 From: Island Bitcoin <34528298+islandbitcoin@users.noreply.github.com> Date: Thu, 18 Jun 2026 07:58:14 -0700 Subject: [PATCH 68/95] update cutover label (#636) Co-authored-by: Vandana --- app/i18n/en/index.ts | 4 ++-- app/i18n/i18n-types.ts | 8 ++++---- app/i18n/raw-i18n/source/en.json | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index e6f22524b..e841bab3e 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -99,7 +99,7 @@ const en: BaseTranslation = { }, CashWalletCutover: { title: "Cash Wallet Updated", - body: "Your Cash Wallet has been upgraded from USD to USDT. Your balance has been automatically migrated. No action is needed on your part.", + body: "Your Cash Wallet has been automatically upgraded and now has additional capabilities. No action is needed on your part.", dismissButton: "Got it", }, ConversionDetailsScreen: { @@ -1660,7 +1660,7 @@ const en: BaseTranslation = { merchant: "Merchant", merchantDesc: "Give rewards, appear on the map, and settle to your bank. ID and bank info required.", international: "International", - internationalDesc: "Set up international transfers with Bridge KYC.", + internationalDesc: "Get an international account and routing number for bank transfers. ID required.", personalInfo: "Personal Information", fullName: "Full name", phoneNumber: "Phone Number", diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index 147911943..066663251 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -342,7 +342,7 @@ type RootTranslation = { */ title: string /** - * Y​o​u​r​ ​C​a​s​h​ ​W​a​l​l​e​t​ ​h​a​s​ ​b​e​e​n​ ​u​p​g​r​a​d​e​d​ ​f​r​o​m​ ​U​S​D​ ​t​o​ ​U​S​D​T​.​ ​Y​o​u​r​ ​b​a​l​a​n​c​e​ ​h​a​s​ ​b​e​e​n​ ​a​u​t​o​m​a​t​i​c​a​l​l​y​ ​m​i​g​r​a​t​e​d​.​ ​N​o​ ​a​c​t​i​o​n​ ​i​s​ ​n​e​e​d​e​d​ ​o​n​ ​y​o​u​r​ ​p​a​r​t​. + * Y​o​u​r​ ​C​a​s​h​ ​W​a​l​l​e​t​ ​h​a​s​ ​b​e​e​n​ ​a​u​t​o​m​a​t​i​c​a​l​l​y​ ​u​p​g​r​a​d​e​d​ ​a​n​d​ ​n​o​w​ ​h​a​s​ ​a​d​d​i​t​i​o​n​a​l​ ​c​a​p​a​b​i​l​i​t​i​e​s​.​ ​N​o​ ​a​c​t​i​o​n​ ​i​s​ ​n​e​e​d​e​d​ ​o​n​ ​y​o​u​r​ ​p​a​r​t​. */ body: string /** @@ -5518,7 +5518,7 @@ type RootTranslation = { */ international: string /** - * S​e​t​ ​u​p​ ​i​n​t​e​r​n​a​t​i​o​n​a​l​ ​t​r​a​n​s​f​e​r​s​ ​w​i​t​h​ ​B​r​i​d​g​e​ ​K​Y​C​. + * G​e​t​ ​a​n​ ​i​n​t​e​r​n​a​t​i​o​n​a​l​ ​a​c​c​o​u​n​t​ ​a​n​d​ ​r​o​u​t​i​n​g​ ​n​u​m​b​e​r​ ​f​o​r​ ​b​a​n​k​ ​t​r​a​n​s​f​e​r​s​.​ ​I​D​ ​r​e​q​u​i​r​e​d​. */ internationalDesc: string /** @@ -5940,7 +5940,7 @@ export type TranslationFunctions = { */ title: () => LocalizedString /** - * Your Cash Wallet has been upgraded from USD to USDT. Your balance has been automatically migrated. No action is needed on your part. + * Your Cash Wallet has been automatically upgraded and now has additional capabilities. No action is needed on your part. */ body: () => LocalizedString /** @@ -11028,7 +11028,7 @@ export type TranslationFunctions = { */ international: () => LocalizedString /** - * Set up international transfers with Bridge KYC. + * Get an international account and routing number for bank transfers. ID required. */ internationalDesc: () => LocalizedString /** diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index 17128f6b0..4ef15c1bd 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -87,7 +87,7 @@ }, "CashWalletCutover": { "title": "Cash Wallet Updated", - "body": "Your Cash Wallet has been upgraded from USD to USDT. Your balance has been automatically migrated. No action is needed on your part.", + "body": "Your Cash Wallet has been automatically upgraded and now has additional capabilities. No action is needed on your part.", "dismissButton": "Got it" }, "ConversionDetailsScreen": { @@ -1561,7 +1561,7 @@ "merchant": "Merchant", "merchantDesc": "Give rewards, appear on the map, and settle to your bank. ID and bank info required.", "international": "International", - "internationalDesc": "Set up international transfers with Bridge KYC.", + "internationalDesc": "Get an international account and routing number for bank transfers. ID required.", "personalInfo": "Personal Information", "fullName": "Full name", "phoneNumber": "Phone Number", From 3e80814f61f88ae22bcf50ba98a969792dc3f049 Mon Sep 17 00:00:00 2001 From: Island Bitcoin <34528298+islandbitcoin@users.noreply.github.com> Date: Thu, 18 Jun 2026 08:21:33 -0700 Subject: [PATCH 69/95] fix(ci): resolve feat/fygaro tsconfig TS5095 + spelling allowlist (#642) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tsconfig: module was 'Node16' while the extended @react-native config uses moduleResolution 'bundler', which requires an esnext-class module (TS5095). Set module=esnext + moduleResolution=bundler explicitly. - typos: accept the earns quiz-ID misspellings (Aggrement/Governement) as bare words too — they're a backend contract and can't be renamed. Addresses ENG-420. Does not cover Test (ttypescript — see #640) or the i18n drift (needs `yarn update-translations` run locally). Co-authored-by: Claude Opus 4.8 (1M context) --- tsconfig.json | 3 ++- typos.toml | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index e077f6b8d..b90cdc396 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,8 @@ "extends": "@react-native/typescript-config/tsconfig.json", "compilerOptions": { "target": "ES2017", - "module": "Node16", + "module": "esnext", + "moduleResolution": "bundler", "esModuleInterop": true, "allowJs": false, "sourceMap": true, diff --git a/typos.toml b/typos.toml index 05c8394c0..82951813e 100644 --- a/typos.toml +++ b/typos.toml @@ -24,6 +24,11 @@ InforError = "InforError" # Nostr terminology: "parameterized replaceable events" are referred to as # "replaceables" in NIP discussions. replaceables = "replaceables" +# Earns quiz-ID misspellings that also surface as bare words (i18n strings / +# generated types). The identifiers are a backend contract and can't be renamed +# (see extend-identifiers above), so accept the word form everywhere too. +Aggrement = "Aggrement" +Governement = "Governement" [files] extend-exclude = ["*.svg", "app/i18n/raw-i18n", "earns-utils.test.ts", "*.plist", "*.pbxproj", "Appfile", "google-services.json", "patches", "ci/tasks/*", "logcat.txt", "*.storyboard"] From f29db91ff05ae22a91dd429394ef0bed272941d6 Mon Sep 17 00:00:00 2001 From: Island Bitcoin <34528298+islandbitcoin@users.noreply.github.com> Date: Thu, 18 Jun 2026 09:22:42 -0700 Subject: [PATCH 70/95] ci: run Check Code + Test on push to main (#643) Add main to the push trigger for the core check-code and test workflows so main's own tip is continuously validated, not only PRs into it. Lands on main via #621 (feat/fygaro -> main). Co-authored-by: Claude Opus 4.8 (1M context) --- .github/workflows/check-code.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-code.yml b/.github/workflows/check-code.yml index cf8a209eb..6848a5e11 100644 --- a/.github/workflows/check-code.yml +++ b/.github/workflows/check-code.yml @@ -1,7 +1,7 @@ name: "Check code" on: push: - branches: [feat/fygaro] + branches: [feat/fygaro, main] pull_request: branches: [main, feat/fygaro] jobs: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e7e80e585..d06811457 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,7 +1,7 @@ name: "Test" on: push: - branches: [feat/fygaro] + branches: [feat/fygaro, main] pull_request: branches: [main, feat/fygaro] jobs: From fa75680f3590b7c3ae29a1c9df6dbc9ddcccf778 Mon Sep 17 00:00:00 2001 From: Vandana Date: Wed, 17 Jun 2026 12:41:35 -0700 Subject: [PATCH 71/95] fix(receive): convert USDT settlement amount from micros to cents for lnUsdInvoiceCreate USDT money amounts are denominated in smallest units (micros) by the price conversion layer, which are USDT_MICROS_PER_USD_CENT (10,000x) smaller than a cent. The lnUsdInvoiceCreate mutation expects USD cents, but the receive flow forwarded the raw USDT settlement amount, so a $5.00 request (500 cents) was sent as 5,000,000 and the backend minted a ~$50,000 invoice. Convert USDT micros back to cents at the mutation boundary (USD amounts are already in cents and pass through unchanged). Add a regression test asserting a $5.00 USDT request calls the mutation with amount=500. Manually verified on device: scanning a $5.00 USDT invoice now displays $5.00. --- __tests__/payment-request/helpers.ts | 4 ++ .../payment-request/payment-request.spec.ts | 67 ++++++++++++++++++- .../payment/payment-request.ts | 15 ++++- 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/__tests__/payment-request/helpers.ts b/__tests__/payment-request/helpers.ts index 1d498a15c..b28a635d6 100644 --- a/__tests__/payment-request/helpers.ts +++ b/__tests__/payment-request/helpers.ts @@ -14,6 +14,10 @@ export const usdWalletDescriptor = { id: "usd-wallet-id", currency: WalletCurrency.Usd, } +export const usdtWalletDescriptor = { + id: "usdt-wallet-id", + currency: WalletCurrency.Usdt, +} export const convertMoneyAmountFn = ( amount: MoneyAmount, toCurrency: T, diff --git a/__tests__/payment-request/payment-request.spec.ts b/__tests__/payment-request/payment-request.spec.ts index 50a1a74d6..4b88182cc 100644 --- a/__tests__/payment-request/payment-request.spec.ts +++ b/__tests__/payment-request/payment-request.spec.ts @@ -1,15 +1,25 @@ import { createPaymentRequestCreationData } from "@app/screens/receive-bitcoin-screen/payment/payment-request-creation-data" import { createMock } from "ts-auto-mock" -import { LnInvoice } from "@app/graphql/generated" +import { LnInvoice, WalletCurrency } from "@app/graphql/generated" import { GeneratePaymentRequestMutations, Invoice, PaymentRequestState, } from "@app/screens/receive-bitcoin-screen/payment/index.types" -import { btcWalletDescriptor, defaultParams, usdWalletDescriptor } from "./helpers" +import { + btcWalletDescriptor, + defaultParams, + usdWalletDescriptor, + usdtWalletDescriptor, +} from "./helpers" import { createPaymentRequest } from "@app/screens/receive-bitcoin-screen/payment/payment-request" -import { toUsdMoneyAmount } from "@app/types/amounts" +import { + MoneyAmount, + toUsdMoneyAmount, + USDT_MICROS_PER_USD_CENT, + WalletOrDisplayCurrency, +} from "@app/types/amounts" const usdAmountInvoice = "lnbc49100n1p3l2q6cpp5y8lc3dv7qnplxhc3z9j0sap4n0hu99g39tl3srx6zj0hrqy2snwsdqqcqzpuxqzfvsp5q6t5f3xeruu4k5sk5nlmxx2kzlw2pydmmjk9g4qqmsc9c6ffzldq9qyyssq9lesnumasvvlvwc7yckvuepklttlvwhjqw3539qqqttsyh5s5j246spy9gezng7ng3d40qsrn6dhsrgs7rccaftzulx5auqqd5lz0psqfskeg4" @@ -86,6 +96,10 @@ export const clearMocks = () => { } describe("payment request", () => { + beforeEach(() => { + clearMocks() + }) + it("ln with btc receiving wallet", async () => { const prcd = createPaymentRequestCreationData({ ...defaultParams, @@ -164,6 +178,53 @@ describe("payment request", () => { expect(prNew.info?.data?.getFullUriFn({})).toBe(usdAmountInvoice) }) + it("ln with usdt receiving wallet - set amount sends cents, not micros", async () => { + // Regression for the $5.00 -> $50,583 USDT invoice bug. The price + // conversion denominates USDT money amounts in smallest units (micros), + // which are USDT_MICROS_PER_USD_CENT (10,000x) smaller than a cent. The + // lnUsdInvoiceCreate mutation expects USD cents, so the receive flow must + // convert USDT micros back to cents before calling it. Otherwise a $5.00 + // request (500 cents) is sent as 5,000,000 and the backend mints a + // $50,000 invoice. + const convertToUsdtMicros = ( + amount: MoneyAmount, + toCurrency: T, + ): MoneyAmount => { + const converted = + toCurrency === WalletCurrency.Usdt + ? amount.amount * USDT_MICROS_PER_USD_CENT + : amount.amount + return { amount: converted, currency: toCurrency, currencyCode: toCurrency } + } + + const prcd = createPaymentRequestCreationData({ + ...defaultParams, + convertMoneyAmount: convertToUsdtMicros, + receivingWalletDescriptor: usdtWalletDescriptor, + // $5.00 entered as 500 USD cents in the unit of account + unitOfAccountAmount: toUsdMoneyAmount(500), + }) + + // sanity: the settlement amount is in USDT micros (500 cents * 10,000) + expect(prcd.settlementAmount?.currency).toBe(WalletCurrency.Usdt) + expect(prcd.settlementAmount?.amount).toBe(500 * USDT_MICROS_PER_USD_CENT) + + const pr = createPaymentRequest({ creationData: prcd, mutations }) + const prNew = await pr.generateRequest() + + expect(prNew.info).not.toBeUndefined() + expect(mockLnUsdInvoiceCreate).toHaveBeenCalledWith({ + variables: { + input: expect.objectContaining({ + walletId: usdtWalletDescriptor.id, + // must be 500 cents, NOT 5,000,000 micros + amount: 500, + }), + }, + }) + expect(prNew.state).toBe(PaymentRequestState.Created) + }) + it("paycode/lnurl", async () => { const prcd = createPaymentRequestCreationData({ ...defaultParams, diff --git a/app/screens/receive-bitcoin-screen/payment/payment-request.ts b/app/screens/receive-bitcoin-screen/payment/payment-request.ts index d8ba18cd0..10a143709 100644 --- a/app/screens/receive-bitcoin-screen/payment/payment-request.ts +++ b/app/screens/receive-bitcoin-screen/payment/payment-request.ts @@ -8,7 +8,7 @@ import { PaymentRequestStateType, PaymentRequestInformation, } from "./index.types" -import { BtcMoneyAmount } from "@app/types/amounts" +import { BtcMoneyAmount, USDT_MICROS_PER_USD_CENT } from "@app/types/amounts" import { getPaymentRequestFullUri, prToDateString } from "./helpers" import { bech32 } from "bech32" @@ -122,12 +122,21 @@ export const createPaymentRequest = ( (pr.settlementAmount?.currency === WalletCurrency.Usd || pr.settlementAmount?.currency === WalletCurrency.Usdt) ) { - console.log("Invoice create amount: ", pr.settlementAmount.amount) + // lnUsdInvoiceCreate expects the amount in USD cents. USD settlement + // amounts are already in cents, but USDT settlement amounts are + // denominated in USDT smallest units (micros), which are + // USDT_MICROS_PER_USD_CENT (10,000x) smaller than a cent. Convert USDT + // micros back to cents at the mutation boundary so a $5.00 invoice is + // created for 500 cents, not 5,000,000. + const amountInCents = + pr.settlementAmount.currency === WalletCurrency.Usdt + ? Math.round(pr.settlementAmount.amount / USDT_MICROS_PER_USD_CENT) + : pr.settlementAmount.amount const { data, errors } = await mutations.lnUsdInvoiceCreate({ variables: { input: { walletId: pr.receivingWalletDescriptor.id, - amount: pr.settlementAmount.amount, + amount: amountInCents, memo: pr.memo, }, }, From 1561a7e0bbe9ba9871f2eefcd383af455fb2eaa3 Mon Sep 17 00:00:00 2001 From: Vandana Date: Thu, 18 Jun 2026 10:34:01 -0700 Subject: [PATCH 72/95] fix(cashout): prefer JMD bank account for local cashout --- .../topup-cashout-flow/CashoutWithdrawTo.tsx | 15 +++++++++++++-- .../topup-cashout-flow/CashoutConfirmation.tsx | 2 +- app/screens/topup-cashout-flow/CashoutDetails.tsx | 6 ++++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/components/topup-cashout-flow/CashoutWithdrawTo.tsx b/app/components/topup-cashout-flow/CashoutWithdrawTo.tsx index e54e64a2a..8f44a3a12 100644 --- a/app/components/topup-cashout-flow/CashoutWithdrawTo.tsx +++ b/app/components/topup-cashout-flow/CashoutWithdrawTo.tsx @@ -5,7 +5,11 @@ import { Icon, makeStyles, Text, useTheme } from "@rneui/themed" import { useI18nContext } from "@app/i18n/i18n-react" import { useBankAccountsQuery } from "@app/graphql/generated" -const CashoutWithdrawTo: React.FC = () => { +type Props = { + preferredCurrency?: string +} + +const CashoutWithdrawTo: React.FC = ({ preferredCurrency }) => { const styles = useStyles() const { colors } = useTheme().theme const { LL } = useI18nContext() @@ -13,8 +17,15 @@ const CashoutWithdrawTo: React.FC = () => { const { data } = useBankAccountsQuery({ fetchPolicy: "cache-only" }) + const bankAccounts = data?.me?.bankAccounts ?? [] + const preferredCurrencyCode = preferredCurrency?.toUpperCase() const bankAccount = - data?.me?.bankAccounts.find((el) => el.isDefault) || data?.me?.bankAccounts[0] + bankAccounts.find( + (el) => + preferredCurrencyCode && el.currency.toUpperCase() === preferredCurrencyCode, + ) || + bankAccounts.find((el) => el.isDefault) || + bankAccounts[0] if (!bankAccount) return null diff --git a/app/screens/topup-cashout-flow/CashoutConfirmation.tsx b/app/screens/topup-cashout-flow/CashoutConfirmation.tsx index b44553b70..089442300 100644 --- a/app/screens/topup-cashout-flow/CashoutConfirmation.tsx +++ b/app/screens/topup-cashout-flow/CashoutConfirmation.tsx @@ -125,7 +125,7 @@ const LocalCashoutConfirmation: React.FC = ({ navigation, route }) => { /> )} - + {Boolean(errorMsg) && ( {errorMsg} diff --git a/app/screens/topup-cashout-flow/CashoutDetails.tsx b/app/screens/topup-cashout-flow/CashoutDetails.tsx index 5adb964af..e8c965def 100644 --- a/app/screens/topup-cashout-flow/CashoutDetails.tsx +++ b/app/screens/topup-cashout-flow/CashoutDetails.tsx @@ -99,9 +99,11 @@ const CashoutDetails = ({ navigation, route }: Props) => { return } + const localBankAccounts = bankAccountsData?.me?.bankAccounts ?? [] const defaultBankAccount = - bankAccountsData?.me?.bankAccounts.find((el) => el.isDefault) || - bankAccountsData?.me?.bankAccounts[0] + localBankAccounts.find((el) => el.currency.toUpperCase() === "JMD") || + localBankAccounts.find((el) => el.isDefault) || + localBankAccounts[0] if (!defaultBankAccount?.id) { setErrorMsg("No bank account found. Please add a bank account first.") From a1c07eb11c04ce9e3ee91721a98de75e0a0e2f9f Mon Sep 17 00:00:00 2001 From: Vandana Date: Thu, 18 Jun 2026 12:01:34 -0700 Subject: [PATCH 73/95] test(payment-request): run spec without ttypescript --- __mocks__/breez-sdk.js | 4 ++ .../payment-request/payment-request.spec.ts | 38 +++++++++++++++---- jest.config.js | 14 ++++++- test/ts-auto-mock-transformer.js | 9 +++++ tsconfig.jest.json | 1 + 5 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 __mocks__/breez-sdk.js create mode 100644 test/ts-auto-mock-transformer.js diff --git a/__mocks__/breez-sdk.js b/__mocks__/breez-sdk.js new file mode 100644 index 000000000..b8843aa56 --- /dev/null +++ b/__mocks__/breez-sdk.js @@ -0,0 +1,4 @@ +module.exports = { + receiveOnchainBreez: jest.fn(() => Promise.resolve({ paymentRequest: "" })), + receivePaymentBreez: jest.fn(() => Promise.resolve({ paymentRequest: "" })), +} diff --git a/__tests__/payment-request/payment-request.spec.ts b/__tests__/payment-request/payment-request.spec.ts index 4b88182cc..c9e6158f4 100644 --- a/__tests__/payment-request/payment-request.spec.ts +++ b/__tests__/payment-request/payment-request.spec.ts @@ -20,6 +20,7 @@ import { USDT_MICROS_PER_USD_CENT, WalletOrDisplayCurrency, } from "@app/types/amounts" +import { receiveOnchainBreez, receivePaymentBreez } from "@app/utils/breez-sdk" const usdAmountInvoice = "lnbc49100n1p3l2q6cpp5y8lc3dv7qnplxhc3z9j0sap4n0hu99g39tl3srx6zj0hrqy2snwsdqqcqzpuxqzfvsp5q6t5f3xeruu4k5sk5nlmxx2kzlw2pydmmjk9g4qqmsc9c6ffzldq9qyyssq9lesnumasvvlvwc7yckvuepklttlvwhjqw3539qqqttsyh5s5j246spy9gezng7ng3d40qsrn6dhsrgs7rccaftzulx5auqqd5lz0psqfskeg4" @@ -28,6 +29,10 @@ const noAmountInvoice = const btcAmountInvoice = "lnbc23690n1p3l2qugpp5jeflfqjpxhe0hg3tzttc325j5l6czs9vq9zqx5edpt0yf7k6cypsdqqcqzpuxqyz5vqsp5lteanmnwddszwut839etrgjenfr3dv5tnvz2d2ww2mvggq7zn46q9qyyssqzcz0rvt7r30q7jul79xqqwpr4k2e8mgd23fkjm422sdgpndwql93d4wh3lap9yfwahue9n7ju80ynkqly0lrqqd2978dr8srkrlrjvcq2v5s6k" const mockOnChainAddress = "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx" +const defaultMemo = "Pay to Flash Wallet User" + +const mockReceiveOnchainBreez = receiveOnchainBreez as jest.Mock +const mockReceivePaymentBreez = receivePaymentBreez as jest.Mock const mockLnInvoice = createMock({ paymentRequest: btcAmountInvoice, @@ -93,11 +98,21 @@ export const clearMocks = () => { mockLnUsdInvoiceCreate.mockClear() mockLnNoAmountInvoiceCreate.mockClear() mockOnChainAddressCurrent.mockClear() + mockReceiveOnchainBreez.mockReset() + mockReceivePaymentBreez.mockReset() } describe("payment request", () => { beforeEach(() => { clearMocks() + mockReceiveOnchainBreez.mockResolvedValue({ + paymentRequest: mockOnChainAddress, + }) + mockReceivePaymentBreez.mockImplementation((amount?: number) => + Promise.resolve({ + paymentRequest: amount ? btcAmountInvoice : noAmountInvoice, + }), + ) }) it("ln with btc receiving wallet", async () => { @@ -113,7 +128,7 @@ describe("payment request", () => { const prNew = await pr.generateRequest() expect(prNew.info).not.toBeUndefined() - expect(mockLnNoAmountInvoiceCreate).toHaveBeenCalled() + expect(mockReceivePaymentBreez).toHaveBeenCalledWith(undefined, defaultMemo) expect(prNew.state).toBe(PaymentRequestState.Created) expect(prNew.info?.data?.invoiceType).toBe(Invoice.Lightning) expect(prNew.info?.data?.getFullUriFn({})).toBe(noAmountInvoice) @@ -132,10 +147,18 @@ describe("payment request", () => { const prNew = await pr.generateRequest() expect(prNew.info).not.toBeUndefined() - expect(mockLnNoAmountInvoiceCreate).toHaveBeenCalled() + expect(mockLnUsdInvoiceCreate).toHaveBeenCalledWith({ + variables: { + input: { + walletId: usdWalletDescriptor.id, + amount: 0, + memo: defaultMemo, + }, + }, + }) expect(prNew.state).toBe(PaymentRequestState.Created) expect(prNew.info?.data?.invoiceType).toBe(Invoice.Lightning) - expect(prNew.info?.data?.getFullUriFn({})).toBe(noAmountInvoice) + expect(prNew.info?.data?.getFullUriFn({})).toBe(usdAmountInvoice) }) it("ln with btc receiving wallet - set amount", async () => { @@ -152,7 +175,7 @@ describe("payment request", () => { const prNew = await pr.generateRequest() expect(prNew.info).not.toBeUndefined() - expect(mockLnInvoiceCreate).toHaveBeenCalled() + expect(mockReceivePaymentBreez).toHaveBeenCalledWith(1, defaultMemo) expect(prNew.state).toBe(PaymentRequestState.Created) expect(prNew.info?.data?.invoiceType).toBe(Invoice.Lightning) expect(prNew.info?.data?.getFullUriFn({})).toBe(btcAmountInvoice) @@ -228,6 +251,7 @@ describe("payment request", () => { it("paycode/lnurl", async () => { const prcd = createPaymentRequestCreationData({ ...defaultParams, + receivingWalletDescriptor: usdWalletDescriptor, type: Invoice.PayCode, username: "username", posUrl: "posUrl", @@ -259,11 +283,9 @@ describe("payment request", () => { const prNew = await pr.generateRequest() expect(prNew.info).not.toBeUndefined() - expect(mockOnChainAddressCurrent).toHaveBeenCalled() + expect(mockReceiveOnchainBreez).toHaveBeenCalled() expect(prNew.state).toBe(PaymentRequestState.Created) expect(prNew.info?.data?.invoiceType).toBe(Invoice.OnChain) - expect( - prNew.info?.data?.getFullUriFn({}).startsWith(`bitcoin:${mockOnChainAddress}`), - ).toBe(true) + expect(prNew.info?.data?.getFullUriFn({})).toBe(mockOnChainAddress) }) }) diff --git a/jest.config.js b/jest.config.js index 41d2b8c60..f629e876d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -9,8 +9,19 @@ module.exports = { "\\.(ts|tsx)$": [ "ts-jest", { - compiler: "ttypescript", tsconfig: "tsconfig.jest.json", + diagnostics: false, + astTransformers: { + before: [ + { + path: "./test/ts-auto-mock-transformer.js", + options: { + cacheBetweenTests: false, + features: ["random"], + }, + }, + ], + }, }, ], "^.+\\.svg$": "jest-transform-stub", @@ -19,6 +30,7 @@ module.exports = { moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], rootDir: ".", moduleNameMapper: { + "^@app/utils/breez-sdk$": ["__mocks__/breez-sdk.js"], "^@app/(.*)$": ["app/$1"], "^@mocks/(.*)$": ["__mocks__/$1"], }, diff --git a/test/ts-auto-mock-transformer.js b/test/ts-auto-mock-transformer.js new file mode 100644 index 000000000..ed32ae8f2 --- /dev/null +++ b/test/ts-auto-mock-transformer.js @@ -0,0 +1,9 @@ +const tsAutoMockTransformer = require("ts-auto-mock/transformer").default + +module.exports = { + name: "ts-auto-mock-transformer", + version: 1, + factory(tsCompiler, options) { + return tsAutoMockTransformer(tsCompiler.program, options) + }, +} diff --git a/tsconfig.jest.json b/tsconfig.jest.json index e423f23e3..d9e3bc3f5 100644 --- a/tsconfig.jest.json +++ b/tsconfig.jest.json @@ -2,6 +2,7 @@ "extends": "./tsconfig", "compilerOptions": { "jsx": "react", + "isolatedModules": false, "plugins": [ { "transform": "ts-auto-mock/transformer", From 55b8a3829b7e11ec2eda8f6a9f0bdae6b3e3ca19 Mon Sep 17 00:00:00 2001 From: Island Bitcoin <34528298+islandbitcoin@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:45:41 -0700 Subject: [PATCH 74/95] feat(ui): display stablecoins as fiat currency in all screens (#648) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stablecoin wallets (USDT today; USDC, USDB, PYUSD, EURC, etc. in the future) should display as their underlying fiat currency — not the stablecoin ticker. Users see "USD" whether their cash wallet is USDT, USDC, or any future USD-pegged stablecoin. This is a UI-only change. Underlying wallet routing, currency enums, amount conversions, and settlement logic remain unchanged. Changes: - Add currency-display.ts with extensible STABLECOIN_DISPLAY_MAP (USDT → "USD" today; add new stablecoins with one line) - CurrencyTag: render displayCurrencyCode() instead of raw enum - use-display-currency: set currencyCode "USD" for USDT, suppress suffix so USDT amounts render identically to USD amounts - amounts.ts: set currencyCode "USD" in USDT amount objects - Balance.tsx: map currency code through displayCurrencyCode() - CashoutWithdrawTo: map bank account currency through displayCurrencyCode() - Update currency-tag test to assert display mapping Adding a future stablecoin = one line in STABLECOIN_DISPLAY_MAP. No other display code needs to change. Co-authored-by: Vandana --- __tests__/components/currency-tag.spec.tsx | 6 ++- app/components/cards/Balance.tsx | 5 +- app/components/currency-tag/currency-tag.tsx | 3 +- .../topup-cashout-flow/CashoutWithdrawTo.tsx | 3 +- app/hooks/use-display-currency.ts | 7 ++- app/types/amounts.ts | 6 +-- app/utils/currency-display.ts | 46 +++++++++++++++++++ 7 files changed, 64 insertions(+), 12 deletions(-) create mode 100644 app/utils/currency-display.ts diff --git a/__tests__/components/currency-tag.spec.tsx b/__tests__/components/currency-tag.spec.tsx index 36a8bfbe6..ceb764cf0 100644 --- a/__tests__/components/currency-tag.spec.tsx +++ b/__tests__/components/currency-tag.spec.tsx @@ -4,6 +4,7 @@ import { ThemeProvider } from "@rneui/themed" import { CurrencyTag } from "@app/components/currency-tag" import { WalletCurrency } from "@app/graphql/generated" +import { displayCurrencyCode } from "@app/utils/currency-display" import theme from "@app/rne-theme/theme" const renderCurrencyTag = (walletCurrency: WalletCurrency) => @@ -16,10 +17,11 @@ const renderCurrencyTag = (walletCurrency: WalletCurrency) => describe("CurrencyTag", () => { it.each([WalletCurrency.Btc, WalletCurrency.Usd, WalletCurrency.Usdt])( "renders a %s tag", - (walletCurrency) => { + (walletCurrency: WalletCurrency) => { const { getByText } = renderCurrencyTag(walletCurrency) - expect(getByText(walletCurrency)).toBeTruthy() + // USDT displays as USD (stablecoin → fiat mapping) + expect(getByText(displayCurrencyCode(walletCurrency))).toBeTruthy() }, ) }) diff --git a/app/components/cards/Balance.tsx b/app/components/cards/Balance.tsx index 12c6afd72..b8cc68e09 100644 --- a/app/components/cards/Balance.tsx +++ b/app/components/cards/Balance.tsx @@ -8,6 +8,9 @@ import HideableArea from "../hideable-area/hideable-area" // hooks import { useHideBalanceQuery } from "@app/graphql/generated" +// utils +import { displayCurrencyCode } from "@app/utils/currency-display" + // assets import Cash from "@app/assets/icons/cash.svg" import Bitcoin from "@app/assets/icons/bitcoin.svg" @@ -67,7 +70,7 @@ const Balance: React.FC = ({ {amount}{" "} - {currency} + {displayCurrencyCode(currency)} diff --git a/app/components/currency-tag/currency-tag.tsx b/app/components/currency-tag/currency-tag.tsx index 9b96653c1..e4088964b 100644 --- a/app/components/currency-tag/currency-tag.tsx +++ b/app/components/currency-tag/currency-tag.tsx @@ -1,4 +1,5 @@ import { WalletCurrency } from "@app/graphql/generated" +import { displayCurrencyCode } from "@app/utils/currency-display" import { makeStyles, useTheme } from "@rneui/themed" import React, { FC } from "react" import { Text, View } from "react-native" @@ -36,7 +37,7 @@ export const CurrencyTag: FC = ({ walletCurrency }) => { return ( - {walletCurrency} + {displayCurrencyCode(walletCurrency)} ) } diff --git a/app/components/topup-cashout-flow/CashoutWithdrawTo.tsx b/app/components/topup-cashout-flow/CashoutWithdrawTo.tsx index 8f44a3a12..c1edcf5a7 100644 --- a/app/components/topup-cashout-flow/CashoutWithdrawTo.tsx +++ b/app/components/topup-cashout-flow/CashoutWithdrawTo.tsx @@ -4,6 +4,7 @@ import { Icon, makeStyles, Text, useTheme } from "@rneui/themed" import { useI18nContext } from "@app/i18n/i18n-react" import { useBankAccountsQuery } from "@app/graphql/generated" +import { displayCurrencyCode } from "@app/utils/currency-display" type Props = { preferredCurrency?: string @@ -59,7 +60,7 @@ const CashoutWithdrawTo: React.FC = ({ preferredCurrency }) => { value={String(bankAccount.accountNumber)} /> - + )} diff --git a/app/hooks/use-display-currency.ts b/app/hooks/use-display-currency.ts index 52ba0c0eb..da3271d7e 100644 --- a/app/hooks/use-display-currency.ts +++ b/app/hooks/use-display-currency.ts @@ -9,6 +9,7 @@ import { WalletAmount, WalletOrDisplayCurrency, } from "@app/types/amounts" +import { displayCurrencyCode } from "@app/utils/currency-display" import { useCallback, useMemo } from "react" import { usePriceConversion } from "./use-price-conversion" import { useI18nContext } from "@app/i18n/i18n-react" @@ -125,7 +126,7 @@ export const useDisplayCurrency = () => { symbol: usdDisplayCurrency.symbol, minorUnitToMajorUnitOffset: 6, showFractionDigits: true, - currencyCode: "USDT", + currencyCode: usdDisplayCurrency.id, }, [WalletCurrency.Btc]: { symbol: "₿", @@ -203,9 +204,7 @@ export const useDisplayCurrency = () => { symbol: noSymbol ? "" : symbol, fractionDigits: showFractionDigits ? minorUnitToMajorUnitOffset : 0, currencyCode: - (moneyAmount.currency === WalletCurrency.Btc || - moneyAmount.currency === WalletCurrency.Usdt) && - !noSuffix + moneyAmount.currency === WalletCurrency.Btc && !noSuffix ? currencyCode : undefined, }) diff --git a/app/types/amounts.ts b/app/types/amounts.ts index eebc7de7d..e032f649a 100644 --- a/app/types/amounts.ts +++ b/app/types/amounts.ts @@ -39,7 +39,7 @@ export const ZeroUsdMoneyAmount: UsdMoneyAmount = { export const ZeroUsdtMoneyAmount: UsdtMoneyAmount = { amount: 0, currency: WalletCurrency.Usdt, - currencyCode: "USDT", + currencyCode: "USD", } export const ZeroBtcMoneyAmount: BtcMoneyAmount = { @@ -83,13 +83,13 @@ export const toUsdtMoneyAmount = (amount: number | undefined | null): UsdtMoneyA return { amount: NaN, currency: WalletCurrency.Usdt, - currencyCode: "USDT", + currencyCode: "USD", } } return { amount, currency: WalletCurrency.Usdt, - currencyCode: "USDT", + currencyCode: "USD", } } diff --git a/app/utils/currency-display.ts b/app/utils/currency-display.ts new file mode 100644 index 000000000..72f395a3f --- /dev/null +++ b/app/utils/currency-display.ts @@ -0,0 +1,46 @@ +// app/utils/currency-display.ts + +import { WalletCurrency } from "@app/graphql/generated" + +/** + * Stablecoin → fiat display mapping. + * + * The backend may add new stablecoin wallets (USDC, USDB, PYUSD, EURC, etc). + * In the UI, these should always display as their underlying fiat currency, + * not the stablecoin ticker. This keeps the user experience consistent — + * they see "USD" whether their cash wallet is USDT, USDC, or any future USD + * stablecoin. + * + * To add a new stablecoin: add its WalletCurrency string value to this map + * with the fiat code it should display as. + * + * NOTE: As of now only USDT exists in the enum. When new currencies are added + * to WalletCurrency, extend this map. Any currency not in the map displays as-is. + */ + +const STABLECOIN_DISPLAY_MAP: Record = { + // USD-pegged stablecoins → display as "USD" + USDT: "USD", + // USDC: "USD", // future + // USDB: "USD", // future + // PYUSD: "USD", // future + + // EUR-pegged stablecoins → display as "EUR" + // EURC: "EUR", // future +} + +/** + * Returns the user-facing currency code for a wallet currency. + * Stablecoins map to their fiat equivalent; everything else passes through. + * + * @example + * displayCurrencyCode("USDT") → "USD" + * displayCurrencyCode("USD") → "USD" + * displayCurrencyCode("BTC") → "BTC" + * displayCurrencyCode("USDC") → "USD" (once added to map) + */ +export const displayCurrencyCode = ( + currency: WalletCurrency | string, +): string => { + return STABLECOIN_DISPLAY_MAP[currency] ?? currency +} From b12d538d19497df061a6fd7f58173c763e364a22 Mon Sep 17 00:00:00 2001 From: Vandana Date: Fri, 19 Jun 2026 11:55:58 -0700 Subject: [PATCH 75/95] fix(tx-history): map USDT to USD in transaction list items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues in TxItem.tsx: 1. 'Received USDT' / 'Sent USDT' label used raw settlementCurrency — now passes through displayCurrencyCode() to show 'USD' 2. Secondary amount for USDT transactions was rendering the raw USDT settlement amount with no currency indicator (after removing the suffix). USDT transactions now follow the same path as USD — showing the BTC equivalent as the secondary amount. --- app/components/transaction-item/TxItem.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/components/transaction-item/TxItem.tsx b/app/components/transaction-item/TxItem.tsx index c80ccd961..ff2ab5f77 100644 --- a/app/components/transaction-item/TxItem.tsx +++ b/app/components/transaction-item/TxItem.tsx @@ -21,6 +21,7 @@ import Icon from "react-native-vector-icons/Ionicons" // utils import { toBtcMoneyAmount, toWalletAmount } from "@app/types/amounts" +import { displayCurrencyCode } from "@app/utils/currency-display" // types import { @@ -88,7 +89,11 @@ const TxItemComponent: React.FC = ({ tx }) => { currency: tx.transaction.settlementDisplayCurrency, }) - if (tx.transaction.settlementCurrency === WalletCurrency.Usd && convertMoneyAmount) { + if ( + (tx.transaction.settlementCurrency === WalletCurrency.Usd || + tx.transaction.settlementCurrency === WalletCurrency.Usdt) && + convertMoneyAmount + ) { secondaryAmount = formatMoneyAmount({ moneyAmount: convertMoneyAmount(moneyAmount, "BTC"), }) @@ -106,7 +111,9 @@ const TxItemComponent: React.FC = ({ tx }) => { } } - const currencyLabel = isBreezTransaction(tx) ? "BTC" : tx.transaction.settlementCurrency + const currencyLabel = isBreezTransaction(tx) + ? "BTC" + : displayCurrencyCode(tx.transaction.settlementCurrency) const onPress = () => { if (isIbexTransaction(tx)) { From 25b054e2694a7016e7dc8772aba92043925ac88b Mon Sep 17 00:00:00 2001 From: Vandana Date: Fri, 19 Jun 2026 11:59:45 -0700 Subject: [PATCH 76/95] =?UTF-8?q?fix(tx-history):=20USDT=20secondary=20amo?= =?UTF-8?q?unt=20shows=20$1.00=20not=20=E2=82=BF1,587?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes: 1. TxItem.tsx: revert USDT secondary amount to raw settlement amount (not BTC conversion). USD transactions show BTC as secondary because their wallet settlement is USD. USDT transactions should show the settlement amount formatted as USD. 2. use-display-currency.ts: set USDT minorUnitToMajorUnitOffset to usdDisplayCurrency.fractionDigits (2) instead of hardcoded 6. The raw→major conversion uses /USDT_MICROS_PER_USDT (1M) which is correct. The offset is only used for display fraction digits, so it should match USD (2 digits) to show '$1.00' not '$1.0000000'. --- app/components/transaction-item/TxItem.tsx | 6 +----- app/hooks/use-display-currency.ts | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/components/transaction-item/TxItem.tsx b/app/components/transaction-item/TxItem.tsx index ff2ab5f77..479d8ce93 100644 --- a/app/components/transaction-item/TxItem.tsx +++ b/app/components/transaction-item/TxItem.tsx @@ -89,11 +89,7 @@ const TxItemComponent: React.FC = ({ tx }) => { currency: tx.transaction.settlementDisplayCurrency, }) - if ( - (tx.transaction.settlementCurrency === WalletCurrency.Usd || - tx.transaction.settlementCurrency === WalletCurrency.Usdt) && - convertMoneyAmount - ) { + if (tx.transaction.settlementCurrency === WalletCurrency.Usd && convertMoneyAmount) { secondaryAmount = formatMoneyAmount({ moneyAmount: convertMoneyAmount(moneyAmount, "BTC"), }) diff --git a/app/hooks/use-display-currency.ts b/app/hooks/use-display-currency.ts index da3271d7e..b1fb2ee77 100644 --- a/app/hooks/use-display-currency.ts +++ b/app/hooks/use-display-currency.ts @@ -124,7 +124,7 @@ export const useDisplayCurrency = () => { }, [WalletCurrency.Usdt]: { symbol: usdDisplayCurrency.symbol, - minorUnitToMajorUnitOffset: 6, + minorUnitToMajorUnitOffset: usdDisplayCurrency.fractionDigits, showFractionDigits: true, currencyCode: usdDisplayCurrency.id, }, From 4d70de39a4c397f0b7797a1b6431929251266960 Mon Sep 17 00:00:00 2001 From: Vandana Date: Fri, 19 Jun 2026 12:03:58 -0700 Subject: [PATCH 77/95] fix(display): show '.0000000 USD' for USDT amounts Restore 6 decimal places (minorUnitToMajorUnitOffset: 6) and re-add currencyCode suffix for USDT so users can see exact stablecoin amounts and rounding. Suffix now reads 'USD' (from usdDisplayCurrency.id) instead of 'USDT'. --- app/hooks/use-display-currency.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/hooks/use-display-currency.ts b/app/hooks/use-display-currency.ts index b1fb2ee77..e7614ad64 100644 --- a/app/hooks/use-display-currency.ts +++ b/app/hooks/use-display-currency.ts @@ -124,7 +124,7 @@ export const useDisplayCurrency = () => { }, [WalletCurrency.Usdt]: { symbol: usdDisplayCurrency.symbol, - minorUnitToMajorUnitOffset: usdDisplayCurrency.fractionDigits, + minorUnitToMajorUnitOffset: 6, showFractionDigits: true, currencyCode: usdDisplayCurrency.id, }, @@ -204,7 +204,9 @@ export const useDisplayCurrency = () => { symbol: noSymbol ? "" : symbol, fractionDigits: showFractionDigits ? minorUnitToMajorUnitOffset : 0, currencyCode: - moneyAmount.currency === WalletCurrency.Btc && !noSuffix + (moneyAmount.currency === WalletCurrency.Btc || + moneyAmount.currency === WalletCurrency.Usdt) && + !noSuffix ? currencyCode : undefined, }) From b1fee0ed5fe804cf99f6d798720687c00974d1b4 Mon Sep 17 00:00:00 2001 From: Island Bitcoin <34528298+islandbitcoin@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:07:36 -0700 Subject: [PATCH 78/95] chore(bridge): drop KYC-response console.logs (#649) Removes two `console.log("BRIDGE INITIATE KYC RESPONSE: ", res)` statements in the Bridge KYC-initiation flow (AccountType.tsx, TopupCashout.tsx). Logging the KYC response writes sensitive identity data to the console; it serves no purpose in production. `res` is still used for error handling, so behavior is unchanged. Surfaced during review of #621. The transaction-details TODO stub on the cashout success screen is intentionally left as-is. Co-authored-by: Dread Co-authored-by: Claude Opus 4.8 (1M context) --- app/screens/account-upgrade-flow/AccountType.tsx | 1 - app/screens/topup-cashout-flow/TopupCashout.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/app/screens/account-upgrade-flow/AccountType.tsx b/app/screens/account-upgrade-flow/AccountType.tsx index 182ef6713..0e807d39e 100644 --- a/app/screens/account-upgrade-flow/AccountType.tsx +++ b/app/screens/account-upgrade-flow/AccountType.tsx @@ -82,7 +82,6 @@ const AccountType: React.FC = ({ navigation }) => { }, }) toggleActivityIndicator(false) - console.log("BRIDGE INITIATE KYC RESPONSE: ", res) const errors = res.data?.bridgeInitiateKyc?.errors if (errors && errors.length > 0) { Alert.alert("Error", errors[0].message) diff --git a/app/screens/topup-cashout-flow/TopupCashout.tsx b/app/screens/topup-cashout-flow/TopupCashout.tsx index acad8ac67..917a4d8ba 100644 --- a/app/screens/topup-cashout-flow/TopupCashout.tsx +++ b/app/screens/topup-cashout-flow/TopupCashout.tsx @@ -218,7 +218,6 @@ const TopupCashout: React.FC = ({ navigation }) => { }, }) toggleActivityIndicator(false) - console.log("BRIDGE INITIATE KYC RESPONSE: ", res) const errors = res.data?.bridgeInitiateKyc?.errors if (errors && errors.length > 0) { Alert.alert("Error", errors[0].message) From 4877abe6f20914c2ad27c8358f898d66271ca40c Mon Sep 17 00:00:00 2001 From: Vandana Date: Fri, 19 Jun 2026 12:08:41 -0700 Subject: [PATCH 79/95] chore: remove unused displayCurrencyCode import from use-display-currency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Leftover from iteration — the hook uses usdDisplayCurrency.id directly instead of the helper, so the import was unused. --- app/hooks/use-display-currency.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/hooks/use-display-currency.ts b/app/hooks/use-display-currency.ts index e7614ad64..2f69c1860 100644 --- a/app/hooks/use-display-currency.ts +++ b/app/hooks/use-display-currency.ts @@ -9,7 +9,6 @@ import { WalletAmount, WalletOrDisplayCurrency, } from "@app/types/amounts" -import { displayCurrencyCode } from "@app/utils/currency-display" import { useCallback, useMemo } from "react" import { usePriceConversion } from "./use-price-conversion" import { useI18nContext } from "@app/i18n/i18n-react" From d48a3587053146c85ee457c52235a9e871d91bed Mon Sep 17 00:00:00 2001 From: Vandana Date: Fri, 19 Jun 2026 12:21:35 -0700 Subject: [PATCH 80/95] fix(receive): remove duplicate 'Cash' wallet option in wallet picker The WalletBottomSheet showed two 'Cash' entries (USD + USDT) in advanced mode. Since setReceivingWallet routes both USD and USDT to the same cash wallet (first USD or USDT wallet from the query), the duplicate was redundant and confusing. Now shows just 'Cash' and 'Bitcoin'. --- app/components/receive-screen/WalletBottomSheet.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/components/receive-screen/WalletBottomSheet.tsx b/app/components/receive-screen/WalletBottomSheet.tsx index 6f65d2dca..74631c58e 100644 --- a/app/components/receive-screen/WalletBottomSheet.tsx +++ b/app/components/receive-screen/WalletBottomSheet.tsx @@ -18,7 +18,6 @@ const icons: Record = { const wallets = [ { title: "Cash", key: "USD" }, - { title: "Cash", key: "USDT" }, { title: "Bitcoin", key: "BTC" }, ] From 1763f81580a39886da45b77b80e27d642b5d55f4 Mon Sep 17 00:00:00 2001 From: Vandana Date: Fri, 19 Jun 2026 12:28:05 -0700 Subject: [PATCH 81/95] fix(tx-history): cash transactions respect user's display currency Cash wallet transactions were using the backend-provided settlementDisplayAmount/settlementDisplayCurrency for the primary line, which always showed USD regardless of the user's configured display currency. Now uses moneyAmountToDisplayCurrencyString (same as BTC transactions) to convert through the price feed to the user's actual display currency. --- app/components/transaction-item/TxItem.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/components/transaction-item/TxItem.tsx b/app/components/transaction-item/TxItem.tsx index 479d8ce93..a9167e44a 100644 --- a/app/components/transaction-item/TxItem.tsx +++ b/app/components/transaction-item/TxItem.tsx @@ -45,7 +45,7 @@ const TxItemComponent: React.FC = ({ tx }) => { const navigation = useNavigation>() const { colors } = useTheme().theme - const { formatMoneyAmount, moneyAmountToDisplayCurrencyString, formatCurrency } = + const { formatMoneyAmount, moneyAmountToDisplayCurrencyString } = useDisplayCurrency() const { convertMoneyAmount } = usePriceConversion() @@ -84,9 +84,9 @@ const TxItemComponent: React.FC = ({ tx }) => { amount: Math.abs(tx.transaction.settlementAmount), currency: tx.transaction.settlementCurrency, }) - primaryAmount = formatCurrency({ - amountInMajorUnits: tx.transaction.settlementDisplayAmount, - currency: tx.transaction.settlementDisplayCurrency, + primaryAmount = moneyAmountToDisplayCurrencyString({ + moneyAmount, + isApproximate: true, }) if (tx.transaction.settlementCurrency === WalletCurrency.Usd && convertMoneyAmount) { From d218df26db8f6123778b9422f2e9d146639c2b66 Mon Sep 17 00:00:00 2001 From: Vandana Date: Fri, 19 Jun 2026 13:46:43 -0700 Subject: [PATCH 82/95] =?UTF-8?q?test:=20update=20USDT=20display=20asserti?= =?UTF-8?q?on=20for=20stablecoin=E2=86=92USD=20mapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The formatMoneyAmount output for USDT changed from '$0.179554 USDT' to '$0.179554 USD' after mapping USDT currencyCode to usdDisplayCurrency.id. --- __tests__/hooks/use-display-currency.spec.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/__tests__/hooks/use-display-currency.spec.tsx b/__tests__/hooks/use-display-currency.spec.tsx index 81b4d2def..aaceebab3 100644 --- a/__tests__/hooks/use-display-currency.spec.tsx +++ b/__tests__/hooks/use-display-currency.spec.tsx @@ -147,11 +147,12 @@ describe("usePriceConversion", () => { const moneyAmount = { amount: 179_554, currency: WalletCurrency.Usdt, - currencyCode: "USDT", + currencyCode: "USD", } expect(result.current.moneyAmountToMajorUnitOrSats(moneyAmount)).toBe(0.179554) - expect(result.current.formatMoneyAmount({ moneyAmount })).toBe("$0.179554 USDT") + // USDT displays as USD (stablecoin → fiat display mapping) + expect(result.current.formatMoneyAmount({ moneyAmount })).toBe("$0.179554 USD") }) it("with 0 digits", async () => { From f07b83d2d5f43291e40dac13a08c7376a2612350 Mon Sep 17 00:00:00 2001 From: Nodirbek <57404324+Nodirbek75@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:49:29 +0500 Subject: [PATCH 83/95] fix: display the overlay during the selfie step of the Bridge KYC flow (#650) * fix selfie step on the bridge kyc flow not showing the overlay with informative texts * fix term of services and privacy policy links not opening on the external browser * fix(bridge-kyc): scope ToS legal-link external-open to bridge.xyz host The ToS-step handler opened any URL containing '/legal' in the external browser. Restrict it to bridge.xyz (and subdomains) + a /legal path so an unrelated host can't be shelled out via Linking.openURL. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Dread Co-authored-by: Claude Opus 4.8 (1M context) --- .../topup-cashout-flow/BridgeKycWebView.tsx | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/app/screens/topup-cashout-flow/BridgeKycWebView.tsx b/app/screens/topup-cashout-flow/BridgeKycWebView.tsx index dff28ea63..6d438eca1 100644 --- a/app/screens/topup-cashout-flow/BridgeKycWebView.tsx +++ b/app/screens/topup-cashout-flow/BridgeKycWebView.tsx @@ -78,14 +78,15 @@ const TOS_INJECTED_JS = `(function() { // iOS zoom prevention: force 16px font on inputs, disable text-size-adjust, // and use MutationObserver for dynamically added inputs. +// Note: Removed touch-action manipulation to allow camera auto-capture to work properly const KYC_ZOOM_PREVENTION_JS = `(function(){ document.querySelectorAll('meta[name="viewport"]').forEach(m=>m.remove()); var meta=document.createElement('meta'); meta.name='viewport'; - meta.content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'; + meta.content='width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5, user-scalable=yes'; document.head.insertBefore(meta,document.head.firstChild); var style=document.createElement('style'); - style.innerHTML='input,textarea,select{font-size:16px!important}*{-webkit-text-size-adjust:100%!important;touch-action:manipulation!important}'; + style.innerHTML='input,textarea,select{font-size:16px!important}*{-webkit-text-size-adjust:100%!important}'; document.head.appendChild(style); var preventZoom=function(e){if(e.target&&(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA')){e.target.style.fontSize='16px';e.target.style.transform='none'}}; document.addEventListener('focusin',preventZoom,true); @@ -95,7 +96,8 @@ const KYC_ZOOM_PREVENTION_JS = `(function(){ true})();` // Viewport meta injection before content loads to prevent initial zoom. -const VIEWPORT_INJECTION_JS = `(function(){const forceViewport=()=>{const content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no';document.querySelectorAll('meta[name="viewport"]').forEach(m=>m.remove());const meta=document.createElement('meta');meta.name='viewport';meta.content=content;if(document.head){document.head.insertBefore(meta,document.head.firstChild)}else{document.documentElement.appendChild(meta)}};forceViewport();document.addEventListener('DOMContentLoaded',forceViewport);window.addEventListener('load',forceViewport);if(document.documentElement){document.documentElement.style.touchAction='pan-x pan-y'}true})();` +// Updated to allow scaling and removed restrictive touch-action for camera functionality +const VIEWPORT_INJECTION_JS = `(function(){const forceViewport=()=>{const content='width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5, user-scalable=yes';document.querySelectorAll('meta[name="viewport"]').forEach(m=>m.remove());const meta=document.createElement('meta');meta.name='viewport';meta.content=content;if(document.head){document.head.insertBefore(meta,document.head.firstChild)}else{document.documentElement.appendChild(meta)}};forceViewport();document.addEventListener('DOMContentLoaded',forceViewport);window.addEventListener('load',forceViewport);true})();` const BridgeKycWebView: React.FC = ({ navigation, route }) => { const styles = useStyles() @@ -220,13 +222,24 @@ const BridgeKycWebView: React.FC = ({ navigation, route }) => { startInLoadingState scalesPageToFit={false} bounces={false} + scrollEnabled sharedCookiesEnabled thirdPartyCookiesEnabled + allowsInlineMediaPlayback + mediaPlaybackRequiresUserAction={false} + allowsFullscreenVideo={false} + automaticallyAdjustContentInsets={false} + contentInsetAdjustmentBehavior="never" onShouldStartLoadWithRequest={(request) => { // During ToS step, open terms/privacy links in external browser if (currentStep === "tos" && request.url !== tosLink) { const url = request.url.toLowerCase() - if (url.includes("www.bridge.xyz/legal")) { + // Only shell out for legal links on bridge.xyz (any subdomain) — + // not any URL that merely contains "/legal". + const isBridgeLegal = + /^https?:\/\/([a-z0-9.-]+\.)?bridge\.xyz\//.test(url) && + url.includes("/legal") + if (isBridgeLegal) { Linking.openURL(request.url) return false } From 9ccc6aea41f7700a5f86b133b4dfb750ddedea44 Mon Sep 17 00:00:00 2001 From: Patoo <262265744+patoo0x@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:11:22 -0700 Subject: [PATCH 84/95] test: repair frontend CI checks --- .storybook/views/story-screen.tsx | 5 +- .../breez-sdk-spark-react-native.js | 46 + __mocks__/@env.js | 12 + .../async-storage.js | 2 +- __mocks__/@react-native-community/netinfo.js | 1 + __mocks__/react-native-fs.js | 19 + ...react-native-google-places-autocomplete.js | 11 + __mocks__/react-native-haptic-feedback.js | 7 + __mocks__/react-native-image-picker.js | 23 + __mocks__/react-native-keychain.js | 26 + __mocks__/react-native-nfc-manager.js | 21 + __mocks__/react-native-vision-camera.js | 19 + __mocks__/react-native-walkthrough-tooltip.js | 6 + __tests__/components/currency-tag.spec.tsx | 1 + __tests__/config/galoy-instances.spec.ts | 1 + __tests__/hooks/use-display-currency.spec.tsx | 8 +- .../use-show-warning-secure-account.spec.tsx | 6 + .../payment-destination/intraledger.spec.ts | 2 + __tests__/payment-destination/lnurl.spec.ts | 25 +- .../lnurl-payment-details.spec.ts | 11 +- .../payment-request-creation-data.spec.ts | 2 +- .../payment-request/payment-request.spec.ts | 7 +- __tests__/persistent-storage.spec.ts | 13 +- __tests__/screens/helper.tsx | 22 +- __tests__/screens/send-confirmation.spec.tsx | 2 +- __tests__/screens/send-destination.spec.tsx | 6 +- .../galoy-primary-button.tsx | 5 +- .../galoy-secondary-button.tsx | 5 +- app/components/buttons/PrimaryBtn.tsx | 12 +- .../contact-support-button.tsx | 2 +- .../explainer-video/ExplainerVideo.tsx | 4 +- app/components/nostr-feed/FeedItem.tsx | 4 +- app/components/notification/bulletins.tsx | 51 +- .../price-history/price-history.tsx | 3 +- app/components/redeem-flow/InforError.tsx | 9 +- .../swap-flow/ConfirmationDetails.tsx | 2 +- .../topup-cashout-flow/CashoutFromWallet.tsx | 2 +- app/graphql/generated.gql | 52 + app/graphql/generated.ts | 191 +- app/graphql/local-schema.gql | 2 +- app/graphql/mocks.ts | 109 +- app/graphql/network-error-component.tsx | 11 +- app/graphql/public-schema.graphql | 2082 +++++++++++++++++ app/hooks/use-unauthed-price-conversion.ts | 15 + app/i18n/raw-i18n/translations/af.json | 110 +- app/i18n/raw-i18n/translations/ar.json | 110 +- app/i18n/raw-i18n/translations/ca.json | 110 +- app/i18n/raw-i18n/translations/cs.json | 110 +- app/i18n/raw-i18n/translations/da.json | 110 +- app/i18n/raw-i18n/translations/de.json | 110 +- app/i18n/raw-i18n/translations/el.json | 110 +- app/i18n/raw-i18n/translations/es.json | 110 +- app/i18n/raw-i18n/translations/fr.json | 110 +- app/i18n/raw-i18n/translations/hr.json | 110 +- app/i18n/raw-i18n/translations/hu.json | 110 +- app/i18n/raw-i18n/translations/hy.json | 112 +- app/i18n/raw-i18n/translations/it.json | 110 +- app/i18n/raw-i18n/translations/ms.json | 110 +- app/i18n/raw-i18n/translations/nl.json | 110 +- app/i18n/raw-i18n/translations/pt.json | 110 +- app/i18n/raw-i18n/translations/qu.json | 110 +- app/i18n/raw-i18n/translations/sr.json | 110 +- app/i18n/raw-i18n/translations/sw.json | 110 +- app/i18n/raw-i18n/translations/th.json | 110 +- app/i18n/raw-i18n/translations/tr.json | 110 +- app/i18n/raw-i18n/translations/vi.json | 110 +- app/i18n/raw-i18n/translations/xh.json | 110 +- .../navigation-container-wrapper.tsx | 2 +- app/navigation/stack-param-lists.ts | 4 +- app/rne-theme/themed.d.ts | 2 + .../chat/GroupChat/SupportGroupChat.tsx | 4 +- app/screens/chat/NIP17Chat.tsx | 10 +- app/screens/chat/chatContext.tsx | 4 +- app/screens/chat/messages.tsx | 4 +- .../contacts-screen/contact-transactions.tsx | 4 +- .../conversion-details-screen.tsx | 2 +- app/screens/developer-screen/debug-screen.tsx | 1 + .../earns-map-screen/earns-map-screen.tsx | 4 +- .../device-account-modal.tsx | 23 +- .../get-started-screen/use-device-token.ts | 7 +- .../receive-bitcoin-screen/payment/helpers.ts | 41 +- .../payment/index.types.ts | 2 +- .../payment/payment-request-creation-data.ts | 2 +- .../use-receive-bitcoin.ts | 2 +- .../payment-details/lightning.ts | 23 +- .../payment-details/onchain.ts | 42 +- ...nd-bitcoin-confirmation-screen.stories.tsx | 8 +- .../send-bitcoin-details-screen.stories.tsx | 8 +- .../send-bitcoin-success-screen.tsx | 2 +- app/screens/send-bitcoin-screen/use-fee.ts | 2 +- .../account/settings/upgrade.tsx | 2 +- .../nostr-settings/nostr-settings-screen.tsx | 4 +- .../settings-screen/settings/nostr-secret.tsx | 4 +- .../topup-cashout-flow/CashoutDetails.tsx | 13 +- .../BTCTransactionHistory.tsx | 4 +- app/utils/nostr.ts | 6 +- app/utils/nostr/publish-helpers.ts | 2 +- codegen.yml | 9 +- e2e/story-book/open-all-screens.e2e.spec.ts | 2 +- jest.config.js | 10 + package.json | 11 +- tsconfig.json | 8 +- types/react-native-html-to-pdf.d.ts | 21 + types/react-native-indicators.d.ts | 18 + types/react-native-randombytes.d.ts | 7 + types/react-native-vector-icons/Ionicons.d.ts | 13 + yarn.lock | 455 +--- 107 files changed, 5300 insertions(+), 889 deletions(-) create mode 100644 __mocks__/@breeztech/breez-sdk-spark-react-native.js create mode 100644 __mocks__/@env.js create mode 100644 __mocks__/@react-native-community/netinfo.js create mode 100644 __mocks__/react-native-fs.js create mode 100644 __mocks__/react-native-google-places-autocomplete.js create mode 100644 __mocks__/react-native-haptic-feedback.js create mode 100644 __mocks__/react-native-image-picker.js create mode 100644 __mocks__/react-native-keychain.js create mode 100644 __mocks__/react-native-nfc-manager.js create mode 100644 __mocks__/react-native-vision-camera.js create mode 100644 __mocks__/react-native-walkthrough-tooltip.js create mode 100644 app/graphql/public-schema.graphql create mode 100644 types/react-native-html-to-pdf.d.ts create mode 100644 types/react-native-indicators.d.ts create mode 100644 types/react-native-randombytes.d.ts create mode 100644 types/react-native-vector-icons/Ionicons.d.ts diff --git a/.storybook/views/story-screen.tsx b/.storybook/views/story-screen.tsx index 631f86398..7cd2f0722 100644 --- a/.storybook/views/story-screen.tsx +++ b/.storybook/views/story-screen.tsx @@ -5,11 +5,14 @@ const PersistentStateWrapper: React.FC = ({ children }) {}, resetState: () => {}, diff --git a/__mocks__/@breeztech/breez-sdk-spark-react-native.js b/__mocks__/@breeztech/breez-sdk-spark-react-native.js new file mode 100644 index 000000000..a9acf7505 --- /dev/null +++ b/__mocks__/@breeztech/breez-sdk-spark-react-native.js @@ -0,0 +1,46 @@ +const SdkEvent_Tags = { + PaymentPending: "PaymentPending", + PaymentSucceeded: "PaymentSucceeded", + PaymentFailed: "PaymentFailed", + Synced: "Synced", + Optimization: "Optimization", +} + +const PaymentType = { + Send: "Send", + Receive: "Receive", +} + +const PaymentStatus = { + Pending: "Pending", + Complete: "Complete", + Failed: "Failed", +} + +class Bolt11Invoice { + constructor(args) { + Object.assign(this, args) + } +} + +class BitcoinAddress { + constructor(args) { + Object.assign(this, args) + } +} + +module.exports = { + __esModule: true, + BitcoinAddress, + Bolt11Invoice, + PaymentStatus, + PaymentType, + ReceivePaymentMethod: { + BitcoinAddress, + Bolt11Invoice, + }, + SdkEvent_Tags, + connect: jest.fn(), + defaultConfig: jest.fn(), + disconnect: jest.fn(), +} diff --git a/__mocks__/@env.js b/__mocks__/@env.js new file mode 100644 index 000000000..a94a98dd7 --- /dev/null +++ b/__mocks__/@env.js @@ -0,0 +1,12 @@ +module.exports = { + API_KEY: "test-api-key", + APP_CHECK_ANDROID_DEBUG_TOKEN: "test-android-app-check-token", + APP_CHECK_IOS_DEBUG_TOKEN: "test-ios-app-check-token", + BREEZ_LNURL_DOMAIN: "test.flashapp.me", + GOOGLE_PLACE_API_KEY: "test-google-place-api-key", + GREENLIGHT_PARTNER_CERT: "test-greenlight-partner-cert", + GREENLIGHT_PARTNER_KEY: "test-greenlight-partner-key", + INVITE_CODE: "test-invite-code", + MIGRATION_FEE_LNURL_W: "https://test.flashapp.me/lnurlw", + MNEMONIC_WORDS: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", +} diff --git a/__mocks__/@react-native-async-storage/async-storage.js b/__mocks__/@react-native-async-storage/async-storage.js index e19713d4a..4acc6acdc 100644 --- a/__mocks__/@react-native-async-storage/async-storage.js +++ b/__mocks__/@react-native-async-storage/async-storage.js @@ -1 +1 @@ -export { AsyncStorageMock as default } from "@react-native-async-storage/async-storage/jest/async-storage-mock" +module.exports = require("@react-native-async-storage/async-storage/jest/async-storage-mock") diff --git a/__mocks__/@react-native-community/netinfo.js b/__mocks__/@react-native-community/netinfo.js new file mode 100644 index 000000000..e61651efd --- /dev/null +++ b/__mocks__/@react-native-community/netinfo.js @@ -0,0 +1 @@ +module.exports = require("@react-native-community/netinfo/jest/netinfo-mock") diff --git a/__mocks__/react-native-fs.js b/__mocks__/react-native-fs.js new file mode 100644 index 000000000..4ed04c796 --- /dev/null +++ b/__mocks__/react-native-fs.js @@ -0,0 +1,19 @@ +const RNFS = { + CachesDirectoryPath: "/tmp", + DocumentDirectoryPath: "/tmp", + DownloadDirectoryPath: "/tmp", + TemporaryDirectoryPath: "/tmp", + appendFile: jest.fn(() => Promise.resolve()), + copyFile: jest.fn(() => Promise.resolve()), + downloadFile: jest.fn(() => ({ + jobId: 1, + promise: Promise.resolve({ statusCode: 200 }), + })), + exists: jest.fn(() => Promise.resolve(false)), + readFile: jest.fn(() => Promise.resolve("")), + unlink: jest.fn(() => Promise.resolve()), + writeFile: jest.fn(() => Promise.resolve()), +} + +module.exports = RNFS +module.exports.default = RNFS diff --git a/__mocks__/react-native-google-places-autocomplete.js b/__mocks__/react-native-google-places-autocomplete.js new file mode 100644 index 000000000..ae93854da --- /dev/null +++ b/__mocks__/react-native-google-places-autocomplete.js @@ -0,0 +1,11 @@ +const React = require("react") +const { View } = require("react-native") + +const GooglePlacesAutocomplete = React.forwardRef((props, ref) => + React.createElement(View, { ...props, ref }), +) + +module.exports = { + __esModule: true, + GooglePlacesAutocomplete, +} diff --git a/__mocks__/react-native-haptic-feedback.js b/__mocks__/react-native-haptic-feedback.js new file mode 100644 index 000000000..f11686a7f --- /dev/null +++ b/__mocks__/react-native-haptic-feedback.js @@ -0,0 +1,7 @@ +const trigger = jest.fn() + +module.exports = { + __esModule: true, + default: { trigger }, + trigger, +} diff --git a/__mocks__/react-native-image-picker.js b/__mocks__/react-native-image-picker.js new file mode 100644 index 000000000..36673002d --- /dev/null +++ b/__mocks__/react-native-image-picker.js @@ -0,0 +1,23 @@ +const emptyResult = { assets: [], didCancel: true } + +const launchImageLibrary = jest.fn((_, callback) => { + if (typeof callback === "function") { + callback(emptyResult) + } + + return Promise.resolve(emptyResult) +}) + +const launchCamera = jest.fn((_, callback) => { + if (typeof callback === "function") { + callback(emptyResult) + } + + return Promise.resolve(emptyResult) +}) + +module.exports = { + __esModule: true, + launchCamera, + launchImageLibrary, +} diff --git a/__mocks__/react-native-keychain.js b/__mocks__/react-native-keychain.js new file mode 100644 index 000000000..6d90f6730 --- /dev/null +++ b/__mocks__/react-native-keychain.js @@ -0,0 +1,26 @@ +const credentialsByServer = new Map() + +const getServer = (serverOrOptions) => + typeof serverOrOptions === "string" ? serverOrOptions : serverOrOptions?.server + +const getInternetCredentials = jest.fn((serverOrOptions) => + Promise.resolve(credentialsByServer.get(getServer(serverOrOptions)) || false), +) + +const setInternetCredentials = jest.fn((server, username, password) => { + credentialsByServer.set(server, { password, server, username }) + return Promise.resolve(true) +}) + +const resetInternetCredentials = jest.fn((serverOrOptions) => { + credentialsByServer.delete(getServer(serverOrOptions)) + return Promise.resolve(true) +}) + +module.exports = { + __esModule: true, + ACCESSIBLE: {}, + getInternetCredentials, + resetInternetCredentials, + setInternetCredentials, +} diff --git a/__mocks__/react-native-nfc-manager.js b/__mocks__/react-native-nfc-manager.js new file mode 100644 index 000000000..3b57fc103 --- /dev/null +++ b/__mocks__/react-native-nfc-manager.js @@ -0,0 +1,21 @@ +const NfcManager = { + cancelTechnologyRequest: jest.fn(), + getTag: jest.fn(), + isEnabled: jest.fn(async () => false), + isSupported: jest.fn(async () => false), + requestTechnology: jest.fn(), + start: jest.fn(), +} + +module.exports = { + __esModule: true, + default: NfcManager, + Ndef: { + text: { + decodePayload: jest.fn(() => ""), + }, + }, + NfcTech: { + Ndef: "Ndef", + }, +} diff --git a/__mocks__/react-native-vision-camera.js b/__mocks__/react-native-vision-camera.js new file mode 100644 index 000000000..138a34e41 --- /dev/null +++ b/__mocks__/react-native-vision-camera.js @@ -0,0 +1,19 @@ +const React = require("react") + +const requestPermission = jest.fn(() => Promise.resolve(true)) + +const Camera = React.forwardRef((props, ref) => + React.createElement("Camera", { ...props, ref }), +) + +module.exports = { + __esModule: true, + Camera, + CameraRuntimeError: Error, + useCameraDevice: jest.fn(() => ({ id: "back" })), + useCameraPermission: jest.fn(() => ({ + hasPermission: true, + requestPermission, + })), + useCodeScanner: jest.fn((config) => config), +} diff --git a/__mocks__/react-native-walkthrough-tooltip.js b/__mocks__/react-native-walkthrough-tooltip.js new file mode 100644 index 000000000..a2c654189 --- /dev/null +++ b/__mocks__/react-native-walkthrough-tooltip.js @@ -0,0 +1,6 @@ +const React = require("react") + +const Tooltip = ({ children }) => React.createElement(React.Fragment, null, children) + +module.exports = Tooltip +module.exports.default = Tooltip diff --git a/__tests__/components/currency-tag.spec.tsx b/__tests__/components/currency-tag.spec.tsx index ceb764cf0..3538ef7aa 100644 --- a/__tests__/components/currency-tag.spec.tsx +++ b/__tests__/components/currency-tag.spec.tsx @@ -1,4 +1,5 @@ import * as React from "react" +import { describe, expect, it } from "@jest/globals" import { render } from "@testing-library/react-native" import { ThemeProvider } from "@rneui/themed" diff --git a/__tests__/config/galoy-instances.spec.ts b/__tests__/config/galoy-instances.spec.ts index 327081a02..df3d0d4b4 100644 --- a/__tests__/config/galoy-instances.spec.ts +++ b/__tests__/config/galoy-instances.spec.ts @@ -22,6 +22,7 @@ it("get a full object with Custom", () => { posUrl: "https://pay.custom.com/", lnAddressHostname: "custom.com", blockExplorer: "https://mempool.space/tx/", + relayUrl: "wss://relay.custom.com", } as const const res = resolveGaloyInstanceOrDefault(CustomInstance) diff --git a/__tests__/hooks/use-display-currency.spec.tsx b/__tests__/hooks/use-display-currency.spec.tsx index aaceebab3..4e38cb460 100644 --- a/__tests__/hooks/use-display-currency.spec.tsx +++ b/__tests__/hooks/use-display-currency.spec.tsx @@ -156,11 +156,15 @@ describe("usePriceConversion", () => { }) it("with 0 digits", async () => { - const { result, waitForNextUpdate } = renderHook(useDisplayCurrency, { + const { result, waitFor } = renderHook(useDisplayCurrency, { wrapper: wrapWithMocks(mocksJpy), }) - await waitForNextUpdate() + await waitFor( + () => + result.current.displayCurrency === "JPY" && + result.current.fractionDigits === 0, + ) const res = result.current.moneyAmountToMajorUnitOrSats({ amount: 100, diff --git a/__tests__/hooks/use-show-warning-secure-account.spec.tsx b/__tests__/hooks/use-show-warning-secure-account.spec.tsx index c67067ac7..983639490 100644 --- a/__tests__/hooks/use-show-warning-secure-account.spec.tsx +++ b/__tests__/hooks/use-show-warning-secure-account.spec.tsx @@ -92,12 +92,14 @@ const mockLevelZeroLowBalance = [ id: "f79792e3-282b-45d4-85d5-7486d020def5", balance: 100, walletCurrency: "BTC", + isExternal: false, __typename: "BTCWallet", }, { id: "f091c102-6277-4cc6-8d81-87ebf6aaad1b", balance: 100, walletCurrency: "USD", + isExternal: false, __typename: "UsdWallet", }, ], @@ -128,12 +130,14 @@ const mockLevelZeroHighBalance = [ id: "f79792e3-282b-45d4-85d5-7486d020def5", balance: 100, walletCurrency: "BTC", + isExternal: false, __typename: "BTCWallet", }, { id: "f091c102-6277-4cc6-8d81-87ebf6aaad1b", balance: 600, walletCurrency: "USD", + isExternal: false, __typename: "UsdWallet", }, ], @@ -164,12 +168,14 @@ const mockLevelOneHighBalance = [ id: "f79792e3-282b-45d4-85d5-7486d020def5", balance: 100, walletCurrency: "BTC", + isExternal: false, __typename: "BTCWallet", }, { id: "f091c102-6277-4cc6-8d81-87ebf6aaad1b", balance: 600, walletCurrency: "USD", + isExternal: false, __typename: "UsdWallet", }, ], diff --git a/__tests__/payment-destination/intraledger.spec.ts b/__tests__/payment-destination/intraledger.spec.ts index 7be139450..2ebc53041 100644 --- a/__tests__/payment-destination/intraledger.spec.ts +++ b/__tests__/payment-destination/intraledger.spec.ts @@ -27,6 +27,7 @@ describe("resolve intraledger", () => { parsedIntraledgerDestination: { paymentType: "intraledger", handle: "testhandle", + valid: true, } as const, accountDefaultWalletQuery: jest.fn(), myWalletIds: ["testwalletid"], @@ -78,6 +79,7 @@ describe("create intraledger destination", () => { parsedIntraledgerDestination: { paymentType: "intraledger", handle: "testhandle", + valid: true, }, walletId: "testwalletid", } as const diff --git a/__tests__/payment-destination/lnurl.spec.ts b/__tests__/payment-destination/lnurl.spec.ts index 12ceb1708..65ec979bc 100644 --- a/__tests__/payment-destination/lnurl.spec.ts +++ b/__tests__/payment-destination/lnurl.spec.ts @@ -7,23 +7,20 @@ import { } from "@app/screens/send-bitcoin-screen/payment-destination" import { DestinationDirection } from "@app/screens/send-bitcoin-screen/payment-destination/index.types" import { ZeroBtcMoneyAmount } from "@app/types/amounts" -import { PaymentType, fetchLnurlPaymentParams } from "@galoymoney/client" +import { PaymentType } from "@galoymoney/client" import { LNURLPayParams, LNURLResponse, LNURLWithdrawParams, getParams } from "js-lnurl" +import { requestPayServiceParams } from "lnurl-pay" import { LnUrlPayServiceResponse } from "lnurl-pay/dist/types/types" import { defaultPaymentDetailParams } from "./helpers" -jest.mock("@galoymoney/client", () => { - const actualModule = jest.requireActual("@galoymoney/client") - +jest.mock("js-lnurl", () => { return { - ...actualModule, - fetchLnurlPaymentParams: jest.fn(), + getParams: jest.fn(), } }) - -jest.mock("js-lnurl", () => { +jest.mock("lnurl-pay", () => { return { - getParams: jest.fn(), + requestPayServiceParams: jest.fn(), } }) jest.mock("@app/screens/send-bitcoin-screen/payment-details", () => { @@ -32,8 +29,8 @@ jest.mock("@app/screens/send-bitcoin-screen/payment-details", () => { } }) -const mockFetchLnurlPaymentParams = fetchLnurlPaymentParams as jest.MockedFunction< - typeof fetchLnurlPaymentParams +const mockRequestPayServiceParams = requestPayServiceParams as jest.MockedFunction< + typeof requestPayServiceParams > const mockGetParams = getParams as jest.MockedFunction const mockCreateLnurlPaymentDetail = createLnurlPaymentDetails as jest.MockedFunction< @@ -61,7 +58,7 @@ describe("resolve lnurl destination", () => { const lnurlPayParams = createMock({ identifier: lnurlPaymentDestinationParams.parsedLnurlDestination.lnurl, }) - mockFetchLnurlPaymentParams.mockResolvedValue(lnurlPayParams) + mockRequestPayServiceParams.mockResolvedValue(lnurlPayParams) mockGetParams.mockResolvedValue(createMock()) const destination = await resolveLnurlDestination(lnurlPaymentDestinationParams) @@ -96,7 +93,7 @@ describe("resolve lnurl destination", () => { const lnurlPayParams = createMock({ identifier: lnurlPaymentDestinationParams.parsedLnurlDestination.lnurl, }) - mockFetchLnurlPaymentParams.mockResolvedValue(lnurlPayParams) + mockRequestPayServiceParams.mockResolvedValue(lnurlPayParams) mockGetParams.mockResolvedValue(createMock()) const destination = await resolveLnurlDestination(lnurlPaymentDestinationParams) @@ -128,7 +125,7 @@ describe("resolve lnurl destination", () => { } it("creates lnurl withdraw destination", async () => { - mockFetchLnurlPaymentParams.mockImplementation(throwError) + mockRequestPayServiceParams.mockImplementation(throwError) const mockLnurlWithdrawParams = createMock() mockGetParams.mockResolvedValue(mockLnurlWithdrawParams) diff --git a/__tests__/payment-details/lnurl-payment-details.spec.ts b/__tests__/payment-details/lnurl-payment-details.spec.ts index c3a8d017f..b03455483 100644 --- a/__tests__/payment-details/lnurl-payment-details.spec.ts +++ b/__tests__/payment-details/lnurl-payment-details.spec.ts @@ -1,6 +1,6 @@ import { WalletCurrency } from "@app/graphql/generated" import * as PaymentDetails from "@app/screens/send-bitcoin-screen/payment-details/lightning" -import { LnUrlPayServiceResponse } from "lnurl-pay/dist/types/types" +import { LnUrlPayServiceResponse, Satoshis } from "lnurl-pay/dist/types/types" import { createMock } from "ts-auto-mock" import { btcSendingWalletDescriptor, @@ -16,9 +16,11 @@ import { usdSendingWalletDescriptor, } from "./helpers" +const sats = (amount: number) => amount as Satoshis + const defaultParamsWithoutInvoice = { lnurl: "testlnurl", - lnurlParams: createMock({ min: 1, max: 1000 }), + lnurlParams: createMock({ min: sats(1), max: sats(1000) }), convertMoneyAmount: convertMoneyAmountMock, sendingWalletDescriptor: btcSendingWalletDescriptor, unitOfAccountAmount: testAmount, @@ -32,7 +34,10 @@ const defaultParamsWithInvoice = { const defaultParamsWithEqualMinMaxAmount = { ...defaultParamsWithoutInvoice, - lnurlParams: createMock({ min: 100, max: 100 }), + lnurlParams: createMock({ + min: sats(100), + max: sats(100), + }), } const spy = jest.spyOn(PaymentDetails, "createLnurlPaymentDetails") diff --git a/__tests__/payment-request/payment-request-creation-data.spec.ts b/__tests__/payment-request/payment-request-creation-data.spec.ts index 44b5e4dfc..5901f7f97 100644 --- a/__tests__/payment-request/payment-request-creation-data.spec.ts +++ b/__tests__/payment-request/payment-request-creation-data.spec.ts @@ -67,7 +67,7 @@ describe("create payment request creation data", () => { expect(prcd.canSetAmount).toBe(false) expect(prcd.canSetMemo).toBe(false) expect(prcd.canSetReceivingWalletDescriptor).toBe(false) - expect(prcd.receivingWalletDescriptor).toBe(btcWalletDescriptor) + expect(prcd.receivingWalletDescriptor).toBe(usdWalletDescriptor) }) it("onchain set", () => { diff --git a/__tests__/payment-request/payment-request.spec.ts b/__tests__/payment-request/payment-request.spec.ts index c9e6158f4..81c9f1452 100644 --- a/__tests__/payment-request/payment-request.spec.ts +++ b/__tests__/payment-request/payment-request.spec.ts @@ -286,6 +286,11 @@ describe("payment request", () => { expect(mockReceiveOnchainBreez).toHaveBeenCalled() expect(prNew.state).toBe(PaymentRequestState.Created) expect(prNew.info?.data?.invoiceType).toBe(Invoice.OnChain) - expect(prNew.info?.data?.getFullUriFn({})).toBe(mockOnChainAddress) + expect(prNew.info?.data?.getFullUriFn({})).toBe( + `bitcoin:${mockOnChainAddress}?message=Pay%2520to%2520Flash%2520Wallet%2520User`, + ) + expect(prNew.info?.data?.getFullUriFn({ prefix: false })).toBe( + `${mockOnChainAddress}?message=Pay%2520to%2520Flash%2520Wallet%2520User`, + ) }) }) diff --git a/__tests__/persistent-storage.spec.ts b/__tests__/persistent-storage.spec.ts index 6f8888797..57a3b5597 100644 --- a/__tests__/persistent-storage.spec.ts +++ b/__tests__/persistent-storage.spec.ts @@ -15,6 +15,7 @@ it("migrates persistent state", async () => { }) expect(state).toEqual({ ...defaultPersistentState, + galoyInstance: { id: "Main" }, }) }) @@ -25,20 +26,24 @@ it("returns default when schema is not present", async () => { expect(state).toEqual(defaultPersistentState) }) -it("migration from 5 to 6", async () => { +it("migration from 5 to current", async () => { const state5 = { schemaVersion: 5, galoyInstance: { id: "Main" }, galoyAuthToken: "myToken", } - const state6 = { - schemaVersion: 6, + const currentState = { + schemaVersion: 7, galoyInstance: { id: "Main" }, galoyAuthToken: "myToken", + hasInitializedBreezSDK: false, + helpTriggered: false, + unclaimedDeposits: 0, + closedQuickStartTypes: [], } const res = await migrateAndGetPersistentState(state5) - expect(res).toStrictEqual(state6) + expect(res).toStrictEqual(currentState) }) diff --git a/__tests__/screens/helper.tsx b/__tests__/screens/helper.tsx index fff1ee20e..51b9e405a 100644 --- a/__tests__/screens/helper.tsx +++ b/__tests__/screens/helper.tsx @@ -12,6 +12,8 @@ import mocks from "@app/graphql/mocks" import { createStackNavigator } from "@react-navigation/stack" import TypesafeI18n from "@app/i18n/i18n-react" import { detectDefaultLocale } from "@app/utils/locale-detector" +import { Provider } from "react-redux" +import { store } from "@app/store/redux" const Stack = createStackNavigator() @@ -21,15 +23,17 @@ export const ContextForScreen: React.FC = ({ children }) => ( {() => ( - - - - - {children} - - - - + + + + + + {children} + + + + + )} diff --git a/__tests__/screens/send-confirmation.spec.tsx b/__tests__/screens/send-confirmation.spec.tsx index 7a8cf8aee..2bb14289f 100644 --- a/__tests__/screens/send-confirmation.spec.tsx +++ b/__tests__/screens/send-confirmation.spec.tsx @@ -17,5 +17,5 @@ it("SendScreen Confirmation", async () => { await act(async () => {}) const { children } = await findByLabelText("Successful Fee") - expect(children).toEqual(["₦0 ($0.00)"]) + expect(children).toEqual(["₦0.00 ($0.00)"]) }) diff --git a/__tests__/screens/send-destination.spec.tsx b/__tests__/screens/send-destination.spec.tsx index edff26538..76e366b79 100644 --- a/__tests__/screens/send-destination.spec.tsx +++ b/__tests__/screens/send-destination.spec.tsx @@ -16,7 +16,11 @@ const sendBitcoinDestination = { it("SendScreen Destination", async () => { render( - + , ) await act(async () => {}) diff --git a/app/components/atomic/galoy-primary-button/galoy-primary-button.tsx b/app/components/atomic/galoy-primary-button/galoy-primary-button.tsx index 1eff458a1..f469c0181 100644 --- a/app/components/atomic/galoy-primary-button/galoy-primary-button.tsx +++ b/app/components/atomic/galoy-primary-button/galoy-primary-button.tsx @@ -3,6 +3,9 @@ import { Button, ButtonProps, makeStyles } from "@rneui/themed" import { TouchableHighlight } from "react-native" import { testProps } from "@app/utils/testProps" +const TouchableHighlightComponent = + TouchableHighlight as unknown as ButtonProps["TouchableComponent"] + export const GaloyPrimaryButton: FC> = (props) => { const styles = useStyles() @@ -10,7 +13,7 @@ export const GaloyPrimaryButton: FC> = (props) =>