Skip to main content

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.
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.
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:
  1. Additional Details - Provide personal information
  2. Onfido - Identity verification
  3. Terms - Accept wallet terms and conditions
  4. Activate - Final activation

Transferring Balance

Transfer funds from your wallet to a connected payment method.
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

Build docs developers (and LLMs) love