diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eb93a3e458..10d754acfe4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased (develop) +- fixed: Infinite buy transfer payload and confirmation error handling + ## 4.46.0 (staging) - added: Xgram swap exchange plugin support diff --git a/src/components/scenes/DevTestScene.tsx b/src/components/scenes/DevTestScene.tsx index 49083116471..c2bc24736bc 100644 --- a/src/components/scenes/DevTestScene.tsx +++ b/src/components/scenes/DevTestScene.tsx @@ -366,8 +366,17 @@ export const DevTestScene: React.FC = props => { navigation.navigate('rampBankRoutingDetails', { bank: { name: 'Test Bank', + beneficiaryName: 'Test Business', + address: { + addressLine1: '123 Main St', + city: 'New York', + state: 'NY', + postalCode: '10001', + country: 'US' + }, accountNumber: '1234567890', - routingNumber: '987654321' + routingNumber: '987654321', + depositMessage: 'REF-12345' }, fiatCurrencyCode: 'USD', fiatAmount: '1,000.00', diff --git a/src/components/scenes/RampBankRoutingDetailsScene.tsx b/src/components/scenes/RampBankRoutingDetailsScene.tsx index 75afdae0a0c..1c04542ffb4 100644 --- a/src/components/scenes/RampBankRoutingDetailsScene.tsx +++ b/src/components/scenes/RampBankRoutingDetailsScene.tsx @@ -16,12 +16,24 @@ import { SceneContainer } from '../layout/SceneContainer' import { EdgeRow } from '../rows/EdgeRow' import { showToast } from '../services/AirshipInstance' import { cacheStyles, type Theme, useTheme } from '../services/ThemeContext' -import { EdgeText, Paragraph } from '../themed/EdgeText' +import { EdgeText } from '../themed/EdgeText' + +export interface BankAddress { + addressLine1: string + city: string + state: string + postalCode: string + country: string +} export interface BankInfo { name: string + beneficiaryName: string + address?: BankAddress + addressLine?: string accountNumber: string routingNumber: string + depositMessage: string } export interface RampBankRoutingDetailsParams { @@ -31,6 +43,24 @@ export interface RampBankRoutingDetailsParams { onDone: () => void } +/** Try to parse a single-line US address like "1800 North Pole St., Orlando, FL 32801" */ +const parseAddressLine = (line: string): BankAddress | undefined => { + const parts = line.split(',').map(s => s.trim()) + if (parts.length < 3) return undefined + + const stateZip = parts[parts.length - 1] + const match = /^([A-Z]{2})\s+(\S+)$/.exec(stateZip) + if (match == null) return undefined + + return { + addressLine1: parts.slice(0, -2).join(', '), + city: parts[parts.length - 2], + state: match[1], + postalCode: match[2], + country: 'US' + } +} + interface Props extends EdgeAppSceneProps<'rampBankRoutingDetails'> {} export const RampBankRoutingDetailsScene: React.FC = props => { @@ -40,6 +70,10 @@ export const RampBankRoutingDetailsScene: React.FC = props => { const theme = useTheme() const styles = getStyles(theme) + const resolvedAddress: BankAddress | undefined = + bank.address ?? + (bank.addressLine != null ? parseAddressLine(bank.addressLine) : undefined) + const amountToSendText = `${fiatAmount} ${fiatCurrencyCode}` const handleCopyAmount = useHandler(() => { @@ -57,9 +91,9 @@ export const RampBankRoutingDetailsScene: React.FC = props => { size={theme.rem(2.5)} color={theme.primaryText} /> - - {lstrings.ramp_bank_routing_instructions} - + + {lstrings.ramp_bank_routing_instructions_1} + @@ -87,6 +121,13 @@ export const RampBankRoutingDetailsScene: React.FC = props => { body={bank.name} rightButtonType="copy" /> + {bank.beneficiaryName !== '' && ( + + )} = props => { body={bank.routingNumber} rightButtonType="copy" /> + {bank.depositMessage !== '' && ( + + )} + {resolvedAddress != null ? ( + <> + + + + + + + + + + ) : bank.addressLine != null && bank.addressLine !== '' ? ( + <> + + + + + + ) : null} + - - {lstrings.ramp_bank_routing_warning} + + {lstrings.ramp_bank_routing_instructions_2} @@ -127,7 +223,8 @@ const getStyles = cacheStyles((theme: Theme) => ({ color: theme.iconTappable }, instructionText: { - flexShrink: 1 + flexShrink: 1, + marginLeft: theme.rem(0.5) }, cardContent: { padding: theme.rem(0.5) diff --git a/src/components/scenes/RampConfirmationScene.tsx b/src/components/scenes/RampConfirmationScene.tsx index 5ad1680ec05..cee5f66f3e1 100644 --- a/src/components/scenes/RampConfirmationScene.tsx +++ b/src/components/scenes/RampConfirmationScene.tsx @@ -48,10 +48,10 @@ export const RampConfirmationScene: React.FC = props => { setIsConfirming(true) try { await onConfirm() - } catch (err) { + } catch (err: unknown) { setError(err) - reset() // Reset the slider on error } finally { + reset() setIsConfirming(false) } }) diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 2c96e30367a..f101ab0168f 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -2541,15 +2541,23 @@ const strings = { // Ramp Bank Routing Details ramp_bank_routing_title: 'Send Bank Transfer', - ramp_bank_routing_instructions: - 'Please send the exact amount shown below to the following bank account.', + ramp_bank_routing_instructions_1: + 'Please send the exact amount shown below to the following bank account using ACH transfer.', + ramp_bank_routing_instructions_2: + 'Verify all details before sending. Be sure to include the reference/memo exactly as shown. Transfers must be completed within 7 days of the quote.', ramp_send_amount_label: 'Amount to Send', ramp_bank_details_section_title: 'Bank Details', ramp_bank_name_label: 'Bank Name', + ramp_business_name_label: 'Business Name', + ramp_bank_address_label: 'Recipient Address', + ramp_address_line_label: 'Address', + ramp_city_label: 'City', + ramp_state_label: 'State', + ramp_postal_code_label: 'ZIP / Postal Code', + ramp_country_label: 'Country', ramp_account_number_label: 'Account Number', ramp_routing_number_label: 'Routing Number', - ramp_bank_routing_warning: - 'Please ensure all details are correct before making the transfer.', + ramp_deposit_message_label: 'Reference / Memo', // #endregion unknown_error_message: 'An unknown error occurred.' diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index 3272013a8e0..50d5ac90c97 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -1984,12 +1984,20 @@ "ramp_routing_number_error_length_1s": "Routing number must be exactly %1$s digits", "string_submit": "Submit", "ramp_bank_routing_title": "Send Bank Transfer", - "ramp_bank_routing_instructions": "Please send the exact amount shown below to the following bank account.", + "ramp_bank_routing_instructions_1": "Please send the exact amount shown below to the following bank account using ACH transfer.", + "ramp_bank_routing_instructions_2": "Verify all details before sending. Be sure to include the reference/memo exactly as shown. Transfers must be completed within 7 days of the quote.", "ramp_send_amount_label": "Amount to Send", "ramp_bank_details_section_title": "Bank Details", "ramp_bank_name_label": "Bank Name", + "ramp_business_name_label": "Business Name", + "ramp_bank_address_label": "Recipient Address", + "ramp_address_line_label": "Address", + "ramp_city_label": "City", + "ramp_state_label": "State", + "ramp_postal_code_label": "ZIP / Postal Code", + "ramp_country_label": "Country", "ramp_account_number_label": "Account Number", "ramp_routing_number_label": "Routing Number", - "ramp_bank_routing_warning": "Please ensure all details are correct before making the transfer.", + "ramp_deposit_message_label": "Reference / Memo", "unknown_error_message": "An unknown error occurred." } diff --git a/src/plugins/ramps/infinite/infiniteApiTypes.ts b/src/plugins/ramps/infinite/infiniteApiTypes.ts index 2d4afa93e6a..dfe4dfc68d9 100644 --- a/src/plugins/ramps/infinite/infiniteApiTypes.ts +++ b/src/plugins/ramps/infinite/infiniteApiTypes.ts @@ -81,15 +81,26 @@ export const asInfiniteTransferResponse = asJSON( sourceDepositInstructions: asOptional( asObject({ amount: asNumber, + depositMessage: asOptional(asString, null), bankAccountNumber: asOptional(asString, null), bankRoutingNumber: asOptional(asString, null), + bankBeneficiaryName: asOptional(asString, null), bankName: asOptional(asString, null), + bankAddress: asOptional( + asObject({ + addressLine1: asString, + city: asString, + state: asString, + postalCode: asString, + country: asString + }), + null + ), + bankAddressLine: asOptional(asString, null), toAddress: asOptional(asString, null) // UNUSED fields: // network: asString, // currency: asString, - // depositMessage: asOptional(asString, null), - // bankBeneficiaryName: asOptional(asString, null), // fromAddress: asOptional(asString, null) }) ) @@ -360,6 +371,41 @@ export type InfiniteQuoteResponse = ReturnType export type InfiniteTransferResponse = ReturnType< typeof asInfiniteTransferResponse > +export interface InfiniteOnrampTransferRequest { + type: 'ONRAMP' + amount: number + source: { + currency: string + } + destination: { + currency: string + network: string + toAddress: string + } + clientReferenceId?: string + developerFee?: string +} + +export interface InfiniteOfframpTransferRequest { + type: 'OFFRAMP' + amount: number + source: { + currency: string + network: string + fromAddress: string + } + destination: { + currency: string + network: string + accountId: string + } + clientReferenceId?: string + developerFee?: string +} + +export type InfiniteTransferRequest = + | InfiniteOnrampTransferRequest + | InfiniteOfframpTransferRequest export type InfiniteCustomerRequest = ReturnType< typeof asInfiniteCustomerRequest > @@ -443,24 +489,9 @@ export interface InfiniteApi { }) => Promise // Transfer methods - createTransfer: (params: { - type: InfiniteQuoteFlow - amount: number - source: { - currency: string - network: string - accountId?: string - fromAddress?: string - } - destination: { - currency: string - network: string - accountId?: string - toAddress?: string - } - clientReferenceId?: string - developerFee?: string - }) => Promise + createTransfer: ( + params: InfiniteTransferRequest + ) => Promise getTransferStatus: (transferId: string) => Promise diff --git a/src/plugins/ramps/infinite/infiniteRampPlugin.ts b/src/plugins/ramps/infinite/infiniteRampPlugin.ts index a1de64e86a2..a8a699a933d 100644 --- a/src/plugins/ramps/infinite/infiniteRampPlugin.ts +++ b/src/plugins/ramps/infinite/infiniteRampPlugin.ts @@ -703,7 +703,6 @@ export const infiniteRampPlugin: RampPluginFactory = ( freshQuote, coreWallet, bankAccountId: bankAccountResult.bankAccountId, - flow, infiniteNetwork, cleanFiatCode } diff --git a/src/plugins/ramps/infinite/utils/navigationFlow.ts b/src/plugins/ramps/infinite/utils/navigationFlow.ts index 074cabbe797..ebb41a8d270 100644 --- a/src/plugins/ramps/infinite/utils/navigationFlow.ts +++ b/src/plugins/ramps/infinite/utils/navigationFlow.ts @@ -3,6 +3,7 @@ import type { NavigationBase } from '../../../../types/routerTypes' export interface NavigationFlow { navigate: NavigationBase['navigate'] goBack: () => void + popToTop: () => void } export const makeNavigationFlow = ( @@ -26,5 +27,10 @@ export const makeNavigationFlow = ( hasNavigated = false } - return { navigate, goBack } + const popToTop = (): void => { + navigation.popToTop() + hasNavigated = false + } + + return { navigate, goBack, popToTop } } diff --git a/src/plugins/ramps/infinite/workflows/confirmationWorkflow.ts b/src/plugins/ramps/infinite/workflows/confirmationWorkflow.ts index 12062eaeaa3..275c93d98f2 100644 --- a/src/plugins/ramps/infinite/workflows/confirmationWorkflow.ts +++ b/src/plugins/ramps/infinite/workflows/confirmationWorkflow.ts @@ -4,7 +4,6 @@ import { showToast } from '../../../../components/services/AirshipInstance' import type { RampQuoteRequest } from '../../rampPluginTypes' import type { InfiniteApi, - InfiniteQuoteFlow, InfiniteQuoteResponse, InfiniteTransferResponse } from '../infiniteApiTypes' @@ -29,7 +28,6 @@ export interface ConfirmationParams { freshQuote: InfiniteQuoteResponse coreWallet: EdgeCurrencyWallet bankAccountId: string - flow: InfiniteQuoteFlow infiniteNetwork: string cleanFiatCode: string } @@ -51,7 +49,6 @@ export const confirmationWorkflow = async ( freshQuote, coreWallet, bankAccountId, - flow, infiniteNetwork, cleanFiatCode } = confirmationParams @@ -61,21 +58,17 @@ export const confirmationWorkflow = async ( source, target, direction: request.direction, - onConfirm: async () => { - // Create the transfer here - let errors bubble up + onConfirm: async (): Promise => { if (request.direction === 'buy') { - // For buy (onramp), source is bank account const [receiveAddress] = await coreWallet.getAddresses({ tokenId: request.tokenId }) const transferParams = { - type: flow, + type: 'ONRAMP' as const, amount: freshQuote.source.amount, source: { - currency: cleanFiatCode.toLowerCase(), - network: 'wire', // Default to wire for bank transfers - accountId: bankAccountId + currency: cleanFiatCode.toLowerCase() }, destination: { currency: request.displayCurrencyCode.toLowerCase(), @@ -87,60 +80,66 @@ export const confirmationWorkflow = async ( const transfer = await infiniteApi.createTransfer(transferParams) - // Show deposit instructions for bank transfer with replace const instructions = transfer.sourceDepositInstructions - if (instructions?.bankName != null && instructions.amount != null) { - navigationFlow.navigate('rampBankRoutingDetails', { - bank: { - name: instructions.bankName, - accountNumber: instructions.bankAccountNumber ?? '', - routingNumber: instructions.bankRoutingNumber ?? '' - }, - fiatCurrencyCode: cleanFiatCode, - fiatAmount: instructions.amount.toString(), - onDone: () => { - navigationFlow.goBack() - } - }) + if (instructions?.bankName == null || instructions.amount == null) { + throw new Error( + `Transfer ${transfer.id} created but deposit instructions are missing` + ) } + navigationFlow.navigate('rampBankRoutingDetails', { + bank: { + name: instructions.bankName, + beneficiaryName: instructions.bankBeneficiaryName ?? '', + address: instructions.bankAddress ?? undefined, + addressLine: instructions.bankAddressLine ?? undefined, + accountNumber: instructions.bankAccountNumber ?? '', + routingNumber: instructions.bankRoutingNumber ?? '', + depositMessage: instructions.depositMessage ?? '' + }, + fiatCurrencyCode: cleanFiatCode, + fiatAmount: instructions.amount.toString(), + onDone: () => { + navigationFlow.popToTop() + } + }) + resolve({ confirmed: true, transfer }) - } else { - // TODO: This whole else block is a WIP implementation! + return + } - // For sell (offramp), destination is bank account - const [receiveAddress] = await coreWallet.getAddresses({ - tokenId: request.tokenId - }) + // TODO: This whole else block is a WIP implementation! - const transferParams = { - type: flow, - amount: freshQuote.source.amount, - source: { - currency: request.displayCurrencyCode.toLowerCase(), - network: infiniteNetwork, - fromAddress: receiveAddress.publicAddress - }, - destination: { - currency: cleanFiatCode.toLowerCase(), - network: 'ach', // Default to ACH for bank transfers - accountId: bankAccountId - }, - clientReferenceId: `edge_${Date.now()}` - } + const [receiveAddress] = await coreWallet.getAddresses({ + tokenId: request.tokenId + }) - const transfer = await infiniteApi.createTransfer(transferParams) + const transferParams = { + type: 'OFFRAMP' as const, + amount: freshQuote.source.amount, + source: { + currency: request.displayCurrencyCode.toLowerCase(), + network: infiniteNetwork, + fromAddress: receiveAddress.publicAddress + }, + destination: { + currency: cleanFiatCode.toLowerCase(), + network: 'ach', // Default to ACH for bank transfers + accountId: bankAccountId + }, + clientReferenceId: `edge_${Date.now()}` + } - // Show deposit instructions - if (transfer.sourceDepositInstructions?.toAddress != null) { - // TODO: Show deposit address to user - showToast( - `Send ${request.displayCurrencyCode} to: ${transfer.sourceDepositInstructions.toAddress}` - ) - } + const transfer = await infiniteApi.createTransfer(transferParams) - resolve({ confirmed: true, transfer }) + if (transfer.sourceDepositInstructions?.toAddress != null) { + // TODO: Show deposit address to user + showToast( + `Send ${request.displayCurrencyCode} to: ${transfer.sourceDepositInstructions.toAddress}` + ) } + + resolve({ confirmed: true, transfer }) }, onCancel: () => { resolve({ confirmed: false })