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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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());