Documentation Index Fetch the complete documentation index at: https://mintlify.com/Expensify/App/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The Expensify Wallet enables users to send and receive money directly within Expensify. This guide covers wallet activation, balance management, and transfer operations.
Wallet Page
The wallet page is the central hub for managing bank accounts, cards, and wallet balance.
src/pages/settings/Wallet/WalletPage/index.tsx
function WalletPage () {
const [ bankAccountList = getEmptyObject < OnyxTypes . BankAccountList >()] = useOnyx ( ONYXKEYS . BANK_ACCOUNT_LIST );
const [ cardList = getEmptyObject < OnyxTypes . CardList >()] = useOnyx ( ONYXKEYS . CARD_LIST );
const [ fundList = getEmptyObject < OnyxTypes . FundList >()] = useOnyx ( ONYXKEYS . FUND_LIST , {
selector: fundListSelector ,
});
const [ userWallet ] = useOnyx ( ONYXKEYS . USER_WALLET );
const [ isLoadingPaymentMethods = true ] = useOnyx ( ONYXKEYS . IS_LOADING_PAYMENT_METHODS );
const hasWallet = ! isEmpty ( userWallet );
const hasActivatedWallet = ([ CONST . WALLET . TIER_NAME . GOLD , CONST . WALLET . TIER_NAME . PLATINUM ] as string [])
. includes ( userWallet ?. tierName ?? '' );
useEffect (() => {
if ( network . isOffline ) {
return ;
}
getPaymentMethods ( true );
}, [ network . isOffline ]);
// ... UI rendering
}
Wallet Tiers
Expensify Wallet has multiple tiers with different capabilities:
Silver - Basic wallet, view balance only
Gold - Can send and receive money
Platinum - Enhanced limits and features
Enabling Payments
Activate the wallet to enable sending and receiving payments.
src/pages/EnablePayments/EnablePaymentsPage.tsx
function EnablePaymentsPage () {
const { translate } = useLocalize ();
const { isOffline } = useNetwork ();
const [ userWallet ] = useOnyx ( ONYXKEYS . USER_WALLET , {
// We want to refresh the wallet each time the user attempts to activate the wallet
initWithStoredValues: false ,
});
const { isPendingOnfidoResult , hasFailedOnfido } = userWallet ?? {};
useEffect (() => {
if ( isOffline ) {
return ;
}
if ( isPendingOnfidoResult || hasFailedOnfido ) {
Navigation . navigate ( ROUTES . SETTINGS_WALLET , { forceReplace: true });
return ;
}
openEnablePaymentsPage ();
}, [ isOffline , isPendingOnfidoResult , hasFailedOnfido ]);
if ( isEmptyObject ( userWallet )) {
return < FullScreenLoadingIndicator />;
}
return (
< ScreenWrapper
shouldShowOfflineIndicator = {userWallet?.currentStep !== CONST.WALLET.STEP. ONFIDO }
includeSafeAreaPaddingBottom
testID = "EnablePaymentsPage"
>
{() => {
if ( userWallet ? .errorCode === CONST.WALLET.ERROR.KYC) {
return (
<>
< HeaderWithBackButton
title = { translate ( 'additionalDetailsStep.headerTitle' )}
onBackButtonPress = {() => Navigation.goBack(ROUTES.SETTINGS_WALLET)}
/>
< FailedKYC />
</>
);
}
const currentStep = userWallet ?. currentStep || CONST . WALLET . STEP . ADDITIONAL_DETAILS ;
switch ( currentStep ) {
case CONST . WALLET . STEP . ADDITIONAL_DETAILS :
case CONST . WALLET . STEP . ADDITIONAL_DETAILS_KBA :
return < AdditionalDetailsStep />;
case CONST . WALLET . STEP . ONFIDO :
return < OnfidoStep />;
case CONST . WALLET . STEP . TERMS :
return < TermsStep userWallet ={ userWallet } />;
case CONST . WALLET . STEP . ACTIVATE :
return < ActivateStep userWallet ={ userWallet } />;
default :
return null ;
}
}}
</ ScreenWrapper >
);
}
Wallet Steps
The wallet activation process includes multiple steps:
Additional Details - Provide personal information
Onfido - Identity verification
Terms - Accept wallet terms and conditions
Activate - Final activation
Transferring Balance
Transfer funds from your wallet to a connected payment method.
src/pages/settings/Wallet/TransferBalancePage.tsx
function TransferBalancePage () {
const [ userWallet ] = useOnyx ( ONYXKEYS . USER_WALLET );
const [ walletTransfer ] = useOnyx ( ONYXKEYS . WALLET_TRANSFER );
const [ bankAccountList ] = useOnyx ( ONYXKEYS . BANK_ACCOUNT_LIST );
const [ fundList ] = useOnyx ( ONYXKEYS . FUND_LIST );
const paymentCardList = fundList ?? {};
const paymentTypes = [
{
key: CONST . WALLET . TRANSFER_METHOD_TYPE . ACH ,
title: translate ( 'transferAmountPage.ach' ),
description: translate ( 'transferAmountPage.achSummary' ),
icon: icons . Bank ,
type: CONST . PAYMENT_METHODS . PERSONAL_BANK_ACCOUNT ,
},
];
function getSelectedPaymentMethodAccount () : PaymentMethod | undefined {
const paymentMethods = formatPaymentMethods ( bankAccountList ?? {}, paymentCardList , styles , translate );
const defaultAccount = paymentMethods . find (( method ) => method . isDefault );
const selectedAccount = paymentMethods . find (
( method ) => method . accountType === walletTransfer ?. selectedAccountType &&
method . methodID ?. toString () === walletTransfer ?. selectedAccountID ?. toString (),
);
return selectedAccount ?? defaultAccount ;
}
function navigateToChooseTransferAccount ( filterPaymentMethodType : FilterMethodPaymentType ) {
saveWalletTransferMethodType ( filterPaymentMethodType );
// If we only have a single option, auto-select it
const combinedPaymentMethods = formatPaymentMethods ( bankAccountList ?? {}, paymentCardList , styles , translate );
const filteredMethods = combinedPaymentMethods . filter (
( paymentMethod ) => paymentMethod . accountType === filterPaymentMethodType
);
if ( filteredMethods . length === 1 ) {
const account = filteredMethods . at ( 0 );
saveWalletTransferAccountTypeAndID ( filterPaymentMethodType , account ?. methodID ?. toString ());
return ;
}
Navigation . navigate ( ROUTES . SETTINGS_WALLET_CHOOSE_TRANSFER_ACCOUNT );
}
const selectedAccount = getSelectedPaymentMethodAccount ();
const selectedPaymentType = selectedAccount &&
selectedAccount . accountType === CONST . PAYMENT_METHODS . PERSONAL_BANK_ACCOUNT
? CONST . WALLET . TRANSFER_METHOD_TYPE . ACH
: CONST . WALLET . TRANSFER_METHOD_TYPE . INSTANT ;
const calculatedFee = calculateWalletTransferBalanceFee ( userWallet ?. currentBalance ?? 0 , selectedPaymentType );
const transferAmount = userWallet ?. currentBalance ?? 0 - calculatedFee ;
const isTransferable = transferAmount > 0 ;
const isButtonDisabled = ! isTransferable || ! selectedAccount ;
const shouldShowTransferView = hasExpensifyPaymentMethod ( paymentCardList , bankAccountList ?? {}) &&
TRANSFER_TIER_NAMES . has ( userWallet ?. tierName ?? '' );
if ( walletTransfer ?. shouldShowSuccess && ! walletTransfer ?. loading ) {
return (
< ConfirmationPage
heading = { translate ( 'transferAmountPage.transferSuccess' )}
description = {
walletTransfer. paymentMethodType === CONST . PAYMENT_METHODS . PERSONAL_BANK_ACCOUNT
? translate ( 'transferAmountPage.transferDetailBankAccount' )
: translate ( 'transferAmountPage.transferDetailDebitCard' )
}
shouldShowButton
buttonText = { translate ( 'common.done' )}
onButtonPress = { dismissSuccessfulTransferBalancePage }
/>
);
}
return (
< ScreenWrapper testID = "TransferBalancePage" >
< View style = { [styles.flexGrow1, styles.flexShrink1, styles.flexBasisAuto, styles.justifyContentCenter]}>
<CurrentWalletBalance balanceStyles={[styles.transferBalanceBalance]} />
</View>
<ScrollView>
<View style={styles.ph5}>
{paymentTypes.map((paymentType) => (
<MenuItem
key={paymentType.key}
title={paymentType.title}
description={paymentType.description}
icon={paymentType.icon}
success={selectedPaymentType === paymentType.key}
onPress={() => navigateToChooseTransferAccount(paymentType.type)}
/>
))}
</View>
{!!selectedAccount && (
<MenuItem
title={selectedAccount?.title}
description={selectedAccount?.description}
shouldShowRightIcon
icon={selectedAccount?.icon}
onPress={() => navigateToChooseTransferAccount(
selectedAccount?.accountType ?? CONST.PAYMENT_METHODS.DEBIT_CARD
)}
/>
)}
<View style={styles.ph5}>
<Text style={[styles.mt5, styles.mb3]}>{translate('transferAmountPage.fee')}</Text>
<Text>{convertToDisplayString(calculatedFee)}</Text>
</View>
</ScrollView>
<FormAlertWithSubmitButton
buttonText={translate('transferAmountPage.transfer',
isTransferable ? convertToDisplayString(transferAmount) : '' )}
isLoading = {walletTransfer?. loading }
onSubmit = {() => selectedAccount && transferWalletBalance ( selectedAccount )}
isDisabled = {isButtonDisabled || isOffline }
/>
</ ScreenWrapper >
);
}
Transfer Fees
Transfer fees are calculated based on the transfer method:
ACH - Free (3-5 business days)
Instant - 1.5% fee with $0.25 minimum (typically disabled)
Wallet Balance Display
Show the current wallet balance prominently.
{ ! shouldShowLoadingSpinner && hasActivatedWallet && (
< OfflineWithFeedback
pendingAction = {CONST.RED_BRICK_ROAD_PENDING_ACTION. ADD }
errors = {walletTerms?. errors }
onClose = { clearWalletTermsError }
>
< MenuItemWithTopDescription
description = { translate ( 'walletPage.balance' )}
title = { convertToDisplayString ( userWallet ? .currentBalance ?? 0)}
titleStyle = {styles. textHeadlineH2 }
interactive = { false }
copyValue = { convertToDisplayString ( userWallet ? .currentBalance ?? 0)}
copyable
/>
</ OfflineWithFeedback >
)}
KYC Wall
The KYC (Know Your Customer) wall ensures users complete necessary verification before accessing payment features.
< KYCWall
ref = { kycWallRef }
onSuccessfulKYC = {(_iouPaymentType? : PaymentMethodType , source? : Source ) =>
navigateToWalletOrTransferBalancePage ( source )
}
onSelectPaymentMethod = {( selectedPaymentMethod: string ) => {
if ( hasActivatedWallet || selectedPaymentMethod !== CONST . PAYMENT_METHODS . PERSONAL_BANK_ACCOUNT ) {
return ;
}
// To allow upgrading to a gold wallet, continue with the KYC flow after adding a bank account
setPersonalBankAccountContinueKYCOnSuccess ( ROUTES . SETTINGS_WALLET );
}}
enablePaymentsRoute = {ROUTES. SETTINGS_ENABLE_PAYMENTS }
addDebitCardRoute = {ROUTES. SETTINGS_ADD_DEBIT_CARD }
source = {hasActivatedWallet ? CONST.KYC_WALL_SOURCE. TRANSFER_BALANCE : CONST . KYC_WALL_SOURCE . ENABLE_WALLET }
shouldIncludeDebitCard = { hasActivatedWallet }
>
{( triggerKYCFlow , buttonRef : RefObject < View | null >) => {
if ( hasActivatedWallet ) {
return (
< MenuItem
ref = {buttonRef as ForwardedRef < View >}
title = { translate ( 'common.transferBalance' )}
icon = {icons. Transfer }
onPress = {(event) => {
triggerKYCFlow ({ event });
}}
shouldShowRightIcon
/>
);
}
return (
< MenuItem
title = { translate ( 'walletPage.enableWallet' )}
icon = {icons. Wallet }
ref = {buttonRef as ForwardedRef < View >}
onPress = {() => {
if ( ! isUserValidated ) {
Navigation . navigate ( createDynamicRoute ( DYNAMIC_ROUTES . VERIFY_ACCOUNT . path ));
return ;
}
Navigation . navigate ( ROUTES . SETTINGS_ENABLE_PAYMENTS );
}}
/>
);
}}
</ KYCWall >
Wallet States
Pending Onfido
When identity verification is in progress:
if ( isPendingOnfidoResult ) {
return (
< View style = { alertViewStyle } >
< Icon
src = {icons. Hourglass }
fill = {theme. icon }
/>
< Text style = { alertTextStyle } >
{ translate (' walletPage . walletActivationPending ')}
</ Text >
</ View >
);
}
Failed Onfido
When identity verification fails:
if ( hasFailedOnfido ) {
return (
< View style = { alertViewStyle } >
< Icon
src = {icons. Exclamation }
fill = {theme. icon }
/>
< Text style = { alertTextStyle } >
{ translate (' walletPage . walletActivationFailed ')}
</ Text >
</ View >
);
}
International Deposit Accounts
For users outside the US, international bank accounts can be configured.
International deposit accounts use Corpay for processing and may have different requirements and processing times compared to US bank accounts.
Connecting Accounts Learn about connecting bank accounts
Payment Methods Managing payment methods and cards
Best Practices
Complete KYC verification promptly
Keep personal information up to date
Monitor wallet activity regularly
Use secure payment methods
Show clear loading states during transfers
Display wallet balance prominently
Provide helpful error messages
Guide users through the KYC process
Handle offline scenarios gracefully
Provide retry options for failed operations
Clear errors after successful resolution
Log important events for debugging