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-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/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/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
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/components/home-screen/Buttons.tsx b/app/components/home-screen/Buttons.tsx
index 61d274f40..3fc8a696f 100644
--- a/app/components/home-screen/Buttons.tsx
+++ b/app/components/home-screen/Buttons.tsx
@@ -68,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 || currentLevel === AccountLevel.Three) {
+ buttons.push({
+ title: LL.HomeScreen.transfer(),
+ target: "BuySellBitcoin",
+ icon: "upDown",
+ })
+ }
return (
diff --git a/app/graphql/front-end-mutations.ts b/app/graphql/front-end-mutations.ts
index 9f3cf05ad..4b5ae3d07 100644
--- a/app/graphql/front-end-mutations.ts
+++ b/app/graphql/front-end-mutations.ts
@@ -117,36 +117,36 @@ 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 {
+ exchangeRate
+ 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 81163878e..59a95c769 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,29 @@ mutation MerchantMapSuggest($input: MerchantMapSuggestInput!) {
}
}
+mutation RequestCashout($input: RequestCashoutInput!) {
+ requestCashout(input: $input) {
+ errors {
+ code
+ message
+ path
+ __typename
+ }
+ offer {
+ exchangeRate
+ 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 a8cb4512e..6c8791a71 100644
--- a/app/graphql/generated.ts
+++ b/app/graphql/generated.ts
@@ -2093,6 +2093,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 exchangeRate: number, readonly expiresAt: number, readonly flashFee: number, readonly offerId: string, readonly receiveJmd: number, readonly receiveUsd: number, readonly send: number, readonly walletId: string } | null } };
+
+export type InitiateCashoutMutationVariables = Exact<{
+ input: InitiateCashoutInput;
+}>;
+
+
+export type InitiateCashoutMutation = { readonly __typename: 'Mutation', readonly initiateCashout: { readonly __typename: 'SuccessPayload', readonly success?: boolean | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly path?: ReadonlyArray | null, readonly message: string, readonly code?: string | null }> } };
+
export type AccountDeleteMutationVariables = Exact<{ [key: string]: never; }>;
@@ -3506,6 +3520,91 @@ 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 {
+ exchangeRate
+ 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/app/i18n/en/index.ts b/app/i18n/en/index.ts
index e627b6aca..960982432 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,62 @@ 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",
+ debitCreditCard: "Debit/Credit Card",
+ debitCreditCardDesc: "Pay with your debit or credit card"
+ },
+ BuyBitcoinDetails: {
+ title: "Card Payment",
+ bankTransfer: "Bank Transfer",
+ 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"
+ },
+ 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",
+ 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.",
@@ -1086,6 +1143,10 @@ const en: BaseTranslation = {
TransferScreen: {
title: "Transfer",
percentageToConvert: "% to convert",
+ topUp: "Top Up",
+ topUpDesc: "Add funds to your wallet",
+ settle: "Settle",
+ settleDesc: "Cashout funds from your wallet"
},
UpgradeAccountModal: {
title: "Upgrade your account",
@@ -1337,7 +1398,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 67d0e2ca8..a99a615cd 100644
--- a/app/i18n/i18n-types.ts
+++ b/app/i18n/i18n-types.ts
@@ -1667,6 +1667,10 @@ type RootTranslation = {
* Send
*/
send: string
+ /**
+ * Transfer
+ */
+ transfer: string
/**
* Sweep to Wallet
*/
@@ -1777,6 +1781,205 @@ type RootTranslation = {
*/
backupDesc: string
}
+ TopUpScreen: {
+ /**
+ * Top Up
+ */
+ title: string
+ /**
+ * Bank Transfer
+ */
+ bankTransfer: string
+ /**
+ * Transfer funds from your bank account
+ */
+ bankTransferDesc: string
+ /**
+ * Debit/Credit Card
+ */
+ debitCreditCard: string
+ /**
+ * Pay with your card via Fygaro
+ */
+ debitCreditCardDesc: string
+ }
+ BuyBitcoinDetails: {
+ /**
+ * Card Payment
+ */
+ title: string
+ /**
+ * Bank Transfer
+ */
+ bankTransfer: string
+ /**
+ * Email
+ */
+ email: string
+ /**
+ * Enter your email address
+ */
+ emailPlaceholder: string
+ /**
+ * Wallet
+ */
+ wallet: string
+ /**
+ * Select wallet
+ */
+ walletPlaceholder: string
+ /**
+ * Amount (USD)
+ */
+ amount: string
+ /**
+ * Enter amount
+ */
+ amountPlaceholder: string
+ /**
+ * Continue
+ */
+ 'continue': string
+ /**
+ * USD Wallet
+ */
+ usdWallet: string
+ /**
+ * BTC Wallet
+ */
+ btcWallet: string
+ /**
+ * Please enter a valid email address
+ */
+ invalidEmail: string
+ /**
+ * Please enter a valid amount
+ */
+ invalidAmount: string
+ /**
+ * Minimum amount is $1.00
+ */
+ minimumAmount: string
+ }
+ FygaroWebViewScreen: {
+ /**
+ * Fygaro Payment
+ */
+ title: string
+ /**
+ * Loading payment page...
+ */
+ loading: string
+ /**
+ * Failed to load payment page
+ */
+ error: string
+ /**
+ * Retry
+ */
+ 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: {
+ /**
+ * Payment Successful
+ */
+ title: string
+ /**
+ * Your payment has been processed successfully
+ */
+ successMessage: string
+ /**
+ * Amount Sent
+ */
+ amountSent: string
+ /**
+ * Deposited to
+ */
+ depositedTo: string
+ /**
+ * Transaction ID
+ */
+ transactionId: string
+ /**
+ * Done
+ */
+ done: string
+ /**
+ * View Transaction
+ */
+ viewTransaction: string
+ }
PinScreen: {
/**
* Incorrect PIN. {attemptsRemaining} attempts remaining.
@@ -3508,6 +3711,22 @@ type RootTranslation = {
* % to convert
*/
percentageToConvert: string
+ /**
+ * Top Up
+ */
+ topUp: string
+ /**
+ * Add funds to your wallet
+ */
+ topUpDesc: string
+ /**
+ * Settle
+ */
+ settle: string
+ /**
+ * Settle pending transactions
+ */
+ settleDesc: string
}
UpgradeAccountModal: {
/**
@@ -6399,6 +6618,10 @@ export type TranslationFunctions = {
* Send
*/
send: () => LocalizedString
+ /**
+ * Transfer
+ */
+ transfer: () => LocalizedString
/**
* Sweep to Wallet
*/
@@ -6509,6 +6732,205 @@ export type TranslationFunctions = {
*/
backupDesc: () => LocalizedString
}
+ TopUpScreen: {
+ /**
+ * Top Up
+ */
+ title: () => LocalizedString
+ /**
+ * Bank Transfer
+ */
+ bankTransfer: () => LocalizedString
+ /**
+ * Transfer funds from your bank
+ */
+ bankTransferDesc: () => LocalizedString
+ /**
+ * Debit/Credit Card
+ */
+ debitCreditCard: () => LocalizedString
+ /**
+ * Pay with your debit or credit card
+ */
+ debitCreditCardDesc: () => LocalizedString
+ }
+ BuyBitcoinDetails: {
+ /**
+ * Card Payment
+ */
+ title: () => LocalizedString
+ /**
+ * Bank Transfer
+ */
+ bankTransfer: () => 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
+ }
+ 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
+ */
+ 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.
@@ -8191,6 +8613,22 @@ export type TranslationFunctions = {
* % to convert
*/
percentageToConvert: () => LocalizedString
+ /**
+ * Top Up
+ */
+ topUp: () => LocalizedString
+ /**
+ * Add funds to your wallet
+ */
+ topUpDesc: () => LocalizedString
+ /**
+ * Settle
+ */
+ settle: () => LocalizedString
+ /**
+ * Cashout funds from your wallet
+ */
+ settleDesc: () => LocalizedString
}
UpgradeAccountModal: {
/**
diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json
index 1416165e3..e9de6c403 100644
--- a/app/i18n/raw-i18n/source/en.json
+++ b/app/i18n/raw-i18n/source/en.json
@@ -506,6 +506,7 @@
"balance": "Refresh Balance",
"showQrCode": "Topup via QR",
"send": "Send",
+ "transfer": "Transfer",
"sweep": "Sweep to Wallet",
"pay": "Pay",
"title": "Home",
@@ -534,6 +535,62 @@
"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",
+ "debitCreditCard": "Debit/Credit Card",
+ "debitCreditCardDesc": "Pay with your debit or credit card"
+ },
+ "BuyBitcoinDetails": {
+ "title": "Card Payment",
+ "bankTransfer": "Bank Transfer",
+ "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"
+ },
+ "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",
+ "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.",
@@ -703,7 +760,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",
@@ -1011,7 +1067,11 @@
},
"TransferScreen": {
"title": "Transfer",
- "percentageToConvert": "% to convert"
+ "percentageToConvert": "% to convert",
+ "topUp": "Top Up",
+ "topUpDesc": "Add funds to your wallet",
+ "settle": "Settle",
+ "settleDesc": "Cashout funds from your wallet"
},
"UpgradeAccountModal": {
"title": "Upgrade your account",
diff --git a/app/navigation/root-navigator.tsx b/app/navigation/root-navigator.tsx
index 5ccb6108e..dc9a9d604 100644
--- a/app/navigation/root-navigator.tsx
+++ b/app/navigation/root-navigator.tsx
@@ -116,6 +116,14 @@ 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,
+ BuyBitcoinSuccess,
+ BuySellBitcoin,
+ CardPayment,
+} from "@app/screens/buy-bitcoin-flow"
const useStyles = makeStyles(({ colors }) => ({
bottomNavigatorStyle: {
@@ -575,6 +583,23 @@ export const RootStack = () => {
cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS,
}}
/>
+
+
+
+
+
+
+
+
)
}
diff --git a/app/navigation/stack-param-lists.ts b/app/navigation/stack-param-lists.ts
index 58e3530fa..0987f5fe1 100644
--- a/app/navigation/stack-param-lists.ts
+++ b/app/navigation/stack-param-lists.ts
@@ -151,6 +151,23 @@ export type RootStackParamList = {
EditNostrProfile: undefined
NostrSettingsScreen: undefined
SignInViaQRCode: undefined
+ BuySellBitcoin: undefined
+ BuyBitcoin: undefined
+ BuyBitcoinDetails: { paymentType: "card" | "bankTransfer" }
+ BankTransfer: {
+ amount: number
+ wallet: string
+ }
+ CardPayment: {
+ amount: number
+ wallet: string
+ }
+ BuyBitcoinSuccess: undefined
+ paymentSuccess: {
+ amount: number
+ wallet: string
+ transactionId: string
+ }
}
export type ChatStackParamList = {
diff --git a/app/screens/buy-bitcoin-flow/BankTransfer.tsx b/app/screens/buy-bitcoin-flow/BankTransfer.tsx
new file mode 100644
index 000000000..c5e4d96d9
--- /dev/null
+++ b/app/screens/buy-bitcoin-flow/BankTransfer.tsx
@@ -0,0 +1,149 @@
+import React from "react"
+import { View } from "react-native"
+import { makeStyles, Text } from "@rneui/themed"
+import { StackScreenProps } from "@react-navigation/stack"
+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, route }) => {
+ const styles = useStyles()
+ const { bottom } = useSafeAreaInsets()
+ const { LL } = useI18nContext()
+
+ const { email, amount, wallet } = route.params
+ const fee = amount * 0.02
+
+ return (
+
+
+ {LL.BankTransfer.title()}
+
+
+ {LL.BankTransfer.desc1({ amount: amount + fee })}
+
+
+ {LL.BankTransfer.desc2({ code: "UUM7MJRD" })}
+
+
+ {LL.BankTransfer.desc3({ amount: amount, fee: fee })}
+
+
+
+ {LL.BankTransfer.accountType()}
+
+ Checking
+
+
+
+ {LL.BankTransfer.destinationBank()}
+
+ Banco Hipotecario
+
+
+
+ {LL.BankTransfer.accountNumber()}
+
+ 00210312362
+
+
+
+ {LL.BankTransfer.typeOfClient()}
+
+ Corporate
+
+
+
+ {LL.BankTransfer.receiverName()}
+
+ BBW SA de CV
+
+
+
+ {LL.BankTransfer.email()}
+
+ fiat@blink.sv
+
+
+
+ {LL.BankTransfer.amount()}
+
+ {`${amount + fee} USD`}
+
+
+
+ {LL.BankTransfer.uniqueCode()}
+
+ UUM7MJRD
+
+
+
+ {LL.BankTransfer.fees()}
+
+ {`${fee} USD`}
+
+
+
+
+ {LL.BankTransfer.desc4({ email: "fiat@blink.sv" })}
+
+
+ {LL.BankTransfer.desc5()}
+
+ navigation.reset({ index: 0, routes: [{ name: "Primary" }] })}
+ btnStyle={{ marginBottom: bottom + 20, marginTop: 20 }}
+ />
+
+ )
+}
+
+export default BankTransfer
+
+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/buy-bitcoin-flow/BuyBitcoin.tsx b/app/screens/buy-bitcoin-flow/BuyBitcoin.tsx
new file mode 100644
index 000000000..fc0f7b2f5
--- /dev/null
+++ b/app/screens/buy-bitcoin-flow/BuyBitcoin.tsx
@@ -0,0 +1,86 @@
+import React from "react"
+import { TouchableOpacity, View } from "react-native"
+import { RootStackParamList } from "@app/navigation/stack-param-lists"
+import { Icon, Text, makeStyles, useTheme } from "@rneui/themed"
+import { StackScreenProps } from "@react-navigation/stack"
+import { useI18nContext } from "@app/i18n/i18n-react"
+
+// components
+import { Screen } from "@app/components/screen"
+
+type Props = StackScreenProps
+
+const BuyBitcoin: React.FC = ({ navigation }) => {
+ const styles = useStyles()
+ const { LL } = useI18nContext()
+ const { colors } = useTheme().theme
+
+ return (
+
+
+
+ {LL.TopUpScreen.title()}
+
+
+ navigation.navigate("BuyBitcoinDetails", { paymentType: "card" })
+ }
+ >
+
+
+ {LL.TopUpScreen.debitCreditCard()}
+
+ {LL.TopUpScreen.debitCreditCardDesc()}
+
+
+
+
+
+ navigation.navigate("BuyBitcoinDetails", { paymentType: "bankTransfer" })
+ }
+ >
+
+
+
+ {LL.TopUpScreen.bankTransfer()}
+
+ {LL.TopUpScreen.bankTransferDesc()}
+
+
+
+
+
+
+ )
+}
+
+const useStyles = makeStyles(({ colors }) => ({
+ container: {
+ flex: 1,
+ paddingHorizontal: 20,
+ },
+ title: {
+ textAlign: "center",
+ marginBottom: 30,
+ },
+ btn: {
+ flexDirection: "row",
+ alignItems: "center",
+ borderRadius: 20,
+ borderWidth: 1,
+ borderColor: "#dedede",
+ marginBottom: 20,
+ minHeight: 100,
+ paddingHorizontal: 20,
+ },
+ btnTextWrapper: {
+ flex: 1,
+ rowGap: 5,
+ marginHorizontal: 15,
+ },
+}))
+
+export default BuyBitcoin
diff --git a/app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx b/app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx
new file mode 100644
index 000000000..be83593e7
--- /dev/null
+++ b/app/screens/buy-bitcoin-flow/BuyBitcoinDetails.tsx
@@ -0,0 +1,210 @@
+/**
+ * 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"
+import { StackScreenProps } from "@react-navigation/stack"
+import { RootStackParamList } from "@app/navigation/stack-param-lists"
+
+// 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 { useSafeAreaInsets } from "react-native-safe-area-context"
+
+// assets
+import Cash from "@app/assets/icons/cash.svg"
+import Bitcoin from "@app/assets/icons/bitcoin.svg"
+
+type Props = StackScreenProps
+
+const BuyBitcoinDetails: React.FC = ({ navigation, route }) => {
+ const { colors } = useTheme().theme
+ const { LL } = useI18nContext()
+ 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())
+ return
+ }
+
+ setIsLoading(true)
+
+ try {
+ if (route.params.paymentType === "bankTransfer") {
+ navigation.navigate("BankTransfer", {
+ amount: parseFloat(amount),
+ wallet: selectedWallet,
+ })
+ } else {
+ // Card payment flow via Fygaro WebView
+ navigation.navigate("CardPayment", {
+ amount: parseFloat(amount),
+ wallet: selectedWallet, // Will be sent to webhook via metadata
+ })
+ }
+ } catch (error) {
+ Alert.alert("Error", "Failed to initiate payment. Please try again.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ /**
+ * 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",
+ text: LL.BuyBitcoinDetails.usdWallet(),
+ icon: {
+ selected: ,
+ normal: ,
+ },
+ },
+ {
+ id: "BTC",
+ text: LL.BuyBitcoinDetails.btcWallet(),
+ icon: {
+ selected: ,
+ normal: ,
+ },
+ },
+ ]
+
+ return (
+
+
+
+ {route.params.paymentType === "card"
+ ? LL.BuyBitcoinDetails.title()
+ : LL.BuyBitcoinDetails.bankTransfer()}
+
+
+
+
+ {LL.BuyBitcoinDetails.wallet()}
+
+
+
+
+
+
+ {LL.BuyBitcoinDetails.amount()}
+
+
+
+
+
+
+ )
+}
+
+const useStyles = makeStyles(({ colors }) => (props: { bottom: number }) => ({
+ container: {
+ flex: 1,
+ paddingHorizontal: 20,
+ },
+ title: {
+ textAlign: "center" as const,
+ marginBottom: 30,
+ },
+ fieldContainer: {
+ marginBottom: 24,
+ },
+ input: {
+ borderWidth: 1,
+ borderColor: colors.grey3,
+ borderRadius: 12,
+ padding: 16,
+ fontSize: 16,
+ backgroundColor: colors.white,
+ color: colors.black,
+ marginTop: 8,
+ },
+ buttonGroup: {
+ marginTop: 8,
+ },
+ primaryButton: {
+ marginHorizontal: 20,
+ marginBottom: Math.max(20, props.bottom),
+ },
+}))
+
+export default BuyBitcoinDetails
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/BuySellBitcoin.tsx b/app/screens/buy-bitcoin-flow/BuySellBitcoin.tsx
new file mode 100644
index 000000000..f2a4c3169
--- /dev/null
+++ b/app/screens/buy-bitcoin-flow/BuySellBitcoin.tsx
@@ -0,0 +1,87 @@
+import React from "react"
+import { TouchableOpacity, View } from "react-native"
+import { RootStackParamList } from "@app/navigation/stack-param-lists"
+import { Icon, Text, makeStyles, useTheme } from "@rneui/themed"
+import { StackScreenProps } from "@react-navigation/stack"
+import { useI18nContext } from "@app/i18n/i18n-react"
+
+// components
+import { Screen } from "@app/components/screen"
+
+// 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 }) => {
+ const styles = useStyles()
+ const { LL } = useI18nContext()
+ const { colors } = useTheme().theme
+
+ const handleTopUp = () => {
+ navigation.navigate("BuyBitcoin")
+ }
+
+ const handleSettle = () => {
+ navigation.navigate("CashoutDetails")
+ }
+
+ return (
+
+
+
+ {LL.TransferScreen.title()}
+
+
+
+
+ {LL.TransferScreen.topUp()}
+
+ {LL.TransferScreen.topUpDesc()}
+
+
+
+
+
+
+
+ {LL.TransferScreen.settle()}
+
+ {LL.TransferScreen.settleDesc()}
+
+
+
+
+
+
+ )
+}
+
+const useStyles = makeStyles(({ colors }) => ({
+ container: {
+ flex: 1,
+ paddingHorizontal: 20,
+ },
+ title: {
+ textAlign: "center",
+ marginBottom: 30,
+ },
+ btn: {
+ flexDirection: "row",
+ alignItems: "center",
+ borderRadius: 20,
+ borderWidth: 1,
+ borderColor: "#dedede",
+ marginBottom: 20,
+ minHeight: 100,
+ paddingHorizontal: 20,
+ },
+ btnTextWrapper: {
+ flex: 1,
+ rowGap: 5,
+ marginHorizontal: 15,
+ },
+}))
+
+export default BuySellBitcoin
diff --git a/app/screens/buy-bitcoin-flow/CardPayment.tsx b/app/screens/buy-bitcoin-flow/CardPayment.tsx
new file mode 100644
index 000000000..1104a936b
--- /dev/null
+++ b/app/screens/buy-bitcoin-flow/CardPayment.tsx
@@ -0,0 +1,374 @@
+/**
+ * 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"
+import { Text, makeStyles, useTheme } from "@rneui/themed"
+import { RootStackParamList } from "@app/navigation/stack-param-lists"
+import { WebView } from "react-native-webview"
+import { Screen } from "@app/components/screen"
+import { PrimaryBtn } from "@app/components/buttons"
+import { useI18nContext } from "@app/i18n/i18n-react"
+import { useHomeAuthedQuery } from "@app/graphql/generated"
+import { useIsAuthed } from "@app/graphql/is-authed-context"
+
+type Props = StackScreenProps
+
+const CardPayment: React.FC = ({ navigation, route }) => {
+ const isAuthed = useIsAuthed()
+ const styles = useStyles()
+ const { colors } = useTheme().theme
+ const { LL } = useI18nContext()
+ const webViewRef = useRef(null)
+ 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 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",
+ "paypal-activation.com",
+ "paypal.com",
+ "paypal-cdn.com",
+ "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()}`, // 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 || // Exact match
+ domain.endsWith(`.${allowed}`) || // Subdomain match
+ allowed.endsWith(`.${domain}`), // Parent domain match
+ )
+ } catch {
+ // Invalid URL - deny by default
+ return false
+ }
+ }
+
+ const handleRetry = () => {
+ setError(false)
+ setIsLoading(true)
+ webViewRef.current?.reload()
+ }
+
+ if (error) {
+ return (
+
+
+
+ {LL.FygaroWebViewScreen.error()}
+
+
+
+
+ )
+ }
+
+ return (
+
+
+ {isLoading && (
+
+
+
+ {LL.FygaroWebViewScreen.loading()}
+
+
+ )}
+ {
+ setIsLoading(false)
+ setError(false)
+ }}
+ onError={() => {
+ 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:")) {
+ 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
+ // Less restrictive due to PayPal's complex domain requirements
+ if (Platform.OS === "ios") {
+ return url.startsWith("https://") || !url.startsWith("http")
+ }
+
+ // Android: Use domain whitelist for stricter security
+ return isAllowedDomain(url)
+ }}
+ style={styles.webView}
+ javaScriptEnabled
+ domStorageEnabled
+ startInLoadingState
+ scalesPageToFit={false}
+ bounces={false}
+ 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
+ /**
+ * 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());
+ 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
+ })();`}
+ />
+
+
+ )
+}
+
+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 CardPayment
diff --git a/app/screens/buy-bitcoin-flow/index.ts b/app/screens/buy-bitcoin-flow/index.ts
new file mode 100644
index 000000000..8c16ca65b
--- /dev/null
+++ b/app/screens/buy-bitcoin-flow/index.ts
@@ -0,0 +1,15 @@
+import BuySellBitcoin from "./BuySellBitcoin"
+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,
+ BuyBitcoinSuccess,
+}
diff --git a/app/screens/buy-bitcoin-flow/payment-success-screen.tsx b/app/screens/buy-bitcoin-flow/payment-success-screen.tsx
new file mode 100644
index 000000000..d6f1cb23e
--- /dev/null
+++ b/app/screens/buy-bitcoin-flow/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/cashout-screen/CashoutConfirmation.tsx b/app/screens/cashout-screen/CashoutConfirmation.tsx
index 526f77f8c..3b3ecedbd 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),
})
@@ -83,11 +85,16 @@ const CashoutConfirmation: React.FC = ({ navigation, route }) => {
{LL.Cashout.valid({ time: moment(expiresAt).fromNow(true) })}
-
+
{!!errorMsg && (
@@ -98,7 +105,7 @@ const CashoutConfirmation: React.FC = ({ navigation, route }) => {
@@ -107,13 +114,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,
- },
}))
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,
- },
}))