Instant Transfer

Instant Transfer moves a supplier’s balance to a debit card immediately, outside the normal payout schedule, for a fee. Card capture is handled by TabaPay in an embedded iframe, so raw card data never touches the application.

Eligibility

Two independent gates:

user.ppmAvailable

Grants the INSTANT_TRANSFER_METHOD permission — the feature is offered.

user.canRequestInstantTransfer

Server-side gate on actually requesting a transfer.

Holding INSTANT_TRANSFER_METHOD is not sufficient. A supplier can see the Instant Transfer surface while canRequestInstantTransfer is false, in which case the request CTA is disabled.

Setup flow

InstantTransferService.openInstantTransferSetupModal()

openInstantTransferSetupModal(): Observable<any> {
    this.twoFactorAuthenticationService.checkTwoFactorAuthenticationState().then(isTFAConfirmed => {
        if (isTFAConfirmed) {                                       (1)
            this.dialogService.open<InstantTransferSetupModal>(InstantTransferSetupModal)
                .beforeClosed()
                .pipe(withLatestFrom(this.store.select(selectBusinessInformation)))
                .subscribe(([res, bizInfo]) => this.nextStep(res, bizInfo));
        } else {
            this.onSetupFinish.error('twoFactorAuthenticationFail');  (2)
        }
    });

    return this.onSetupFinish.asObservable();
}
1 Setting up a payout card requires a confirmed second factor. This is the only product flow in the application that gates on 2FA directly rather than through a route guard.
2 A failed challenge errors the onSetupFinish subject, so callers can react.
supplier clicks "Set up Instant Transfer"
  → 2FA challenge (TwoFactorAuthenticationService)
  → InstantTransferSetupModal opens with the TabaPay iframe
  → supplier enters card details inside the iframe
  → iframe posts a TabapayIframeEvent
        success → updatePaymentConfigs(configs) → dispatch loadPaymentMethodsSuccess
                → InstantTransferSetupSuccessModal
                → optional makePaymentMethodAsDefault(paymentConfigKey)
                → dispatch fetchDashboardNotifications()
                → onSetupFinish.next('done')
        error   → openFailureModal(errorData, businessInformation)

The TabaPay iframe

views/partials/content/tabapay-iframe/ hosts the embedded form and translates its postMessage traffic into typed events:

TabapayIframeEvent

{ type: TabapayIframeEventType, data }

TabapaySuccessData

Carries the updated PaymentMethodModel[] in configs.

TabapayErrorData

Carries ctx.code (TabapayErrorCode) and ctx.reason (TabapayErrorReason).

The iframe origin comes from environment.tabapayUrl.

On success the service takes the returned configs and pushes them straight into the store, bypassing a refetch:

updatePaymentConfigs(configs: PaymentMethodModel[]): void {
    this.store.dispatch(loadPaymentMethodsSuccess({ paymentMethods: configs }));
}

Error handling

openFailureModal() maps TabaPay’s error taxonomy onto specific, actionable dialogs — each of which offers a "Try again" button that reopens the setup modal.

TabapayErrorCode TabapayErrorReason Modal

CARD_ADDRESS_ERROR

WRONG_ZIP_CODE, ZIP_MISSMATCH, ADDRESS_MISSMATCH

configAddressErrorModal — shows the business address on file

CARD_ADDRESS_ERROR

WRONG_PHONE_COUNTRY

configPhoneCountryNumberErrorModal

CARD_ADDRESS_ERROR

WRONG_PHONE_NUMBER

configPhoneNumberErrorModal

CARD_ADDRESS_ERROR

NOT_SUPPORTED

configAvsCardNotSupportedErrorModal

CARD_NUMBER_ERROR

—

configCardNumberErrorModal

CARD_NOT_SUPPORTED / CARD_AVS_DECLINED

ZIP_MISSMATCH, ADDRESS_MISSMATCH

configAddressErrorModal

CARD_NOT_SUPPORTED / CARD_AVS_DECLINED

TEMPORARY_FAIL

configCommonErrorModal

CARD_NOT_SUPPORTED / CARD_AVS_DECLINED

anything else

configCardNotSupportedErrorModal

anything else

—

configCommonErrorModal

All configurations are built in instant-transfer.config.ts from a shared baseConfig that supplies a Close button, a Try-again button wired to openInstantTransferSetupModal, and a danger-coloured exclamation icon. Every message interpolates supportEmail and supportPhone from constants/contact.const.ts.

The address error modal interpolates the supplier’s business address from the businessInformation slice, so the supplier can see exactly which address failed AVS without leaving the dialog.

Fees

getFeesData(transferAmount: number): Observable<InstantTransferFeesData> {
    return this.feesScheduleHelperService.getInstantTransferFees().pipe(
        map(fees => this.feesScheduleHelperService.crateInstantTransferFeesData(transferAmount, fees)),
    );
}

isFreeAccessEnabled(): Observable<boolean> {
    return this.feesScheduleHelperService.hasFreeAccessPaymentFee();
}

FeesScheduleHelperService reads the fees slice, which is populated once by fetchFees() from GlobalSubscriptionsService.init().

hasFreeAccessPaymentFee() also drives the success modal’s freeTrialAvailable flag — suppliers without a free-access fee are told a free trial is available.

Default payout method

The success modal offers to make Instant Transfer the default:

if (res.setAsDefaultMethod) {
    this.setInstantTransferAsDefaultMethod(instantTransferMethod.paymentConfigKey);
}

which dispatches makePaymentMethodAsDefault({ paymentConfigKey }) against the paymentMethods slice. The relevant method is located by configs.find(m ⇒ m.paymentType === PaymentMethodType.InstantTransfer).

UI surface

instant-access-widget

Dashboard CTA.

modals/instant-transfer-setup/

InstantTransferSetupModal and InstantTransferSetupSuccessModal.

modals/instant-transfer-details-modal/

Fee and limit details.

modals/transfer-funds-modal/

The transfer request itself.

views/onboarding/product-offering/instant-access/

Offer presentation during onboarding.

Analytics

Constant Segment event

CrossSellEventType.ClickedInstantTransferSignUpOnProductOfferings

Clicked Instant Transfer Sign Up on Product Offerings