diff --git a/CHANGELOG.md b/CHANGELOG.md index 1df6a914c..3868bc260 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # CHANGELOG +## Unreleased + +**Features** + +- Added ability to pass an `onEvent` listener to Financial Connections methods via a `params` argument. This includes the following methods, both when used directly or via `useStripe` or `useFinancialConnectionsSheet`: + - `collectBankAccountForPayment` + - `collectBankAccountForSetup` + - `collectBankAccountToken` + - `collectFinancialConnectionsAccounts` + ## 0.41.0 - 2024-12-19 **Fixes** diff --git a/android/src/main/java/com/reactnativestripesdk/CollectBankAccountLauncherFragment.kt b/android/src/main/java/com/reactnativestripesdk/CollectBankAccountLauncherFragment.kt index bc5427907..0139f3b62 100644 --- a/android/src/main/java/com/reactnativestripesdk/CollectBankAccountLauncherFragment.kt +++ b/android/src/main/java/com/reactnativestripesdk/CollectBankAccountLauncherFragment.kt @@ -7,6 +7,7 @@ import android.view.ViewGroup import android.widget.FrameLayout import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment +import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.reactnativestripesdk.utils.* @@ -14,6 +15,7 @@ import com.reactnativestripesdk.utils.createError import com.reactnativestripesdk.utils.createResult import com.reactnativestripesdk.utils.mapFromPaymentIntentResult import com.reactnativestripesdk.utils.mapFromSetupIntentResult +import com.stripe.android.financialconnections.FinancialConnections import com.stripe.android.model.PaymentIntent import com.stripe.android.model.SetupIntent import com.stripe.android.model.StripeIntent @@ -32,6 +34,18 @@ class CollectBankAccountLauncherFragment( ) : Fragment() { private lateinit var collectBankAccountLauncher: CollectBankAccountLauncher + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val stripeSdkModule: StripeSdkModule? = context.getNativeModule(StripeSdkModule::class.java) + if (stripeSdkModule != null && stripeSdkModule.eventListenerCount > 0) { + FinancialConnections.setEventListener { event -> + val params = mapFromFinancialConnectionsEvent(event) + stripeSdkModule.sendEvent(context, "onFinancialConnectionsEvent", params) + } + } + } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { collectBankAccountLauncher = createBankAccountLauncher() @@ -63,6 +77,13 @@ class CollectBankAccountLauncherFragment( } } + override fun onDestroy() { + super.onDestroy() + + // Remove any event listener that might be set + FinancialConnections.clearEventListener() + } + private fun createBankAccountLauncher(): CollectBankAccountLauncher { return CollectBankAccountLauncher.create(this) { result -> when (result) { diff --git a/android/src/main/java/com/reactnativestripesdk/FinancialConnectionsSheetFragment.kt b/android/src/main/java/com/reactnativestripesdk/FinancialConnectionsSheetFragment.kt index bb41e4363..3beae8c8e 100644 --- a/android/src/main/java/com/reactnativestripesdk/FinancialConnectionsSheetFragment.kt +++ b/android/src/main/java/com/reactnativestripesdk/FinancialConnectionsSheetFragment.kt @@ -12,6 +12,7 @@ import com.reactnativestripesdk.utils.* import com.reactnativestripesdk.utils.createError import com.reactnativestripesdk.utils.createMissingActivityError import com.reactnativestripesdk.utils.mapFromToken +import com.stripe.android.financialconnections.FinancialConnections import com.stripe.android.financialconnections.FinancialConnectionsSheet import com.stripe.android.financialconnections.FinancialConnectionsSheetForTokenResult import com.stripe.android.financialconnections.FinancialConnectionsSheetResult @@ -27,6 +28,18 @@ class FinancialConnectionsSheetFragment : Fragment() { private lateinit var configuration: FinancialConnectionsSheet.Configuration private lateinit var mode: Mode + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val stripeSdkModule: StripeSdkModule? = context.getNativeModule(StripeSdkModule::class.java) + if (stripeSdkModule != null && stripeSdkModule.eventListenerCount > 0) { + FinancialConnections.setEventListener { event -> + val params = mapFromFinancialConnectionsEvent(event) + stripeSdkModule.sendEvent(context, "onFinancialConnectionsEvent", params) + } + } + } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return FrameLayout(requireActivity()).also { @@ -55,6 +68,13 @@ class FinancialConnectionsSheetFragment : Fragment() { } } + override fun onDestroy() { + super.onDestroy() + + // Remove any event listener that might be set + FinancialConnections.clearEventListener() + } + private fun onFinancialConnectionsSheetForTokenResult(result: FinancialConnectionsSheetForTokenResult) { when(result) { is FinancialConnectionsSheetForTokenResult.Canceled -> { diff --git a/android/src/main/java/com/reactnativestripesdk/utils/Mappers.kt b/android/src/main/java/com/reactnativestripesdk/utils/Mappers.kt index ca66b44a7..d8d2142a2 100644 --- a/android/src/main/java/com/reactnativestripesdk/utils/Mappers.kt +++ b/android/src/main/java/com/reactnativestripesdk/utils/Mappers.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.os.Bundle import android.util.Log import com.facebook.react.bridge.* +import com.stripe.android.financialconnections.analytics.FinancialConnectionsEvent import com.stripe.android.PaymentAuthConfig import com.stripe.android.model.* import com.stripe.android.model.StripeIntent.NextActionType @@ -957,3 +958,48 @@ internal fun mapToPreferredNetworks(networksAsInts: ArrayList?): List.toWritableArray(): WritableArray { + val writableArray = Arguments.createArray() + + forEach { value -> + when (value) { + null -> writableArray.pushNull() + is Boolean -> writableArray.pushBoolean(value) + is Int -> writableArray.pushInt(value) + is Double -> writableArray.pushDouble(value) + is String -> writableArray.pushString(value) + is Map<*, *> -> writableArray.pushMap((value as Map).toReadableMap()) + is List<*> -> writableArray.pushArray((value as List).toWritableArray()) + else -> writableArray.pushString(value.toString()) + } + } + + return writableArray +} + +private fun Map.toReadableMap(): ReadableMap { + val writableMap = Arguments.createMap() + + forEach { (key, value) -> + when (value) { + null -> writableMap.putNull(key) + is Boolean -> writableMap.putBoolean(key, value) + is Int -> writableMap.putInt(key, value) + is Double -> writableMap.putDouble(key, value) + is String -> writableMap.putString(key, value) + is Map<*, *> -> writableMap.putMap(key, (value as Map).toReadableMap()) + is List<*> -> writableMap.putArray(key, (value as List).toWritableArray()) + else -> writableMap.putString(key, value.toString()) + } + } + + return writableMap +} diff --git a/example/src/screens/ACHPaymentScreen.tsx b/example/src/screens/ACHPaymentScreen.tsx index c4216da74..87a973966 100644 --- a/example/src/screens/ACHPaymentScreen.tsx +++ b/example/src/screens/ACHPaymentScreen.tsx @@ -6,6 +6,7 @@ import { verifyMicrodepositsForPayment, VerifyMicrodepositsParams, collectBankAccountForPayment, + FinancialConnections, } from '@stripe/stripe-react-native'; import Button from '../components/Button'; import PaymentScreen from '../components/PaymentScreen'; @@ -136,6 +137,11 @@ export default function ACHPaymentScreen() { setSecret(clientSecret); + const onEvent = (event: FinancialConnections.FinancialConnectionsEvent) => { + let value = JSON.stringify(event, null, 2); + console.log(`Received Financial Connections event: ${value}`); + }; + const { paymentIntent, error } = await collectBankAccountForPayment( clientSecret, { @@ -146,6 +152,7 @@ export default function ACHPaymentScreen() { email, }, }, + onEvent: onEvent, } ); diff --git a/example/src/screens/CollectBankAccountScreen.tsx b/example/src/screens/CollectBankAccountScreen.tsx index ff037b960..01600d8e6 100644 --- a/example/src/screens/CollectBankAccountScreen.tsx +++ b/example/src/screens/CollectBankAccountScreen.tsx @@ -4,6 +4,7 @@ import { useFinancialConnectionsSheet } from '@stripe/stripe-react-native'; import Button from '../components/Button'; import PaymentScreen from '../components/PaymentScreen'; import { API_URL } from '../Config'; +import type { FinancialConnectionsEvent } from 'src/types/FinancialConnections'; export default function CollectBankAccountScreen() { const [clientSecret, setClientSecret] = React.useState(''); @@ -34,7 +35,12 @@ export default function CollectBankAccountScreen() { const handleCollectTokenPress = async () => { const { session, token, error } = await collectBankAccountToken( - clientSecret + clientSecret, + { + onEvent: (event: FinancialConnectionsEvent) => { + console.log('Event received:', event); + }, + } ); if (error) { @@ -55,7 +61,12 @@ export default function CollectBankAccountScreen() { const handleCollectSessionPress = async () => { const { session, error } = await collectFinancialConnectionsAccounts( - clientSecret + clientSecret, + { + onEvent: (event: FinancialConnectionsEvent) => { + console.log('Event received:', event); + }, + } ); if (error) { diff --git a/ios/FinancialConnections.swift b/ios/FinancialConnections.swift index 260baaf85..5e4791f15 100644 --- a/ios/FinancialConnections.swift +++ b/ios/FinancialConnections.swift @@ -14,10 +14,16 @@ class FinancialConnections { internal static func present( withClientSecret: String, returnURL: String? = nil, + onEvent: ((FinancialConnectionsEvent) -> Void)? = nil, resolve: @escaping RCTPromiseResolveBlock ) -> Void { DispatchQueue.main.async { - FinancialConnectionsSheet(financialConnectionsSessionClientSecret: withClientSecret, returnURL: returnURL).present( + let financialConnectionsSheet = FinancialConnectionsSheet( + financialConnectionsSessionClientSecret: withClientSecret, + returnURL: returnURL + ) + financialConnectionsSheet.onEvent = onEvent + financialConnectionsSheet.present( from: findViewControllerPresenter(from: UIApplication.shared.delegate?.window??.rootViewController ?? UIViewController()), completion: { result in switch result { @@ -35,10 +41,16 @@ class FinancialConnections { internal static func presentForToken( withClientSecret: String, returnURL: String? = nil, + onEvent: ((FinancialConnectionsEvent) -> Void)? = nil, resolve: @escaping RCTPromiseResolveBlock ) -> Void { DispatchQueue.main.async { - FinancialConnectionsSheet(financialConnectionsSessionClientSecret: withClientSecret, returnURL: returnURL).presentForToken( + let financialConnectionsSheet = FinancialConnectionsSheet( + financialConnectionsSessionClientSecret: withClientSecret, + returnURL: returnURL + ) + financialConnectionsSheet.onEvent = onEvent + financialConnectionsSheet.presentForToken( from: findViewControllerPresenter(from: UIApplication.shared.delegate?.window??.rootViewController ?? UIViewController()), completion: { result in switch result { diff --git a/ios/Mappers.swift b/ios/Mappers.swift index 1909f776c..01aa4dc79 100644 --- a/ios/Mappers.swift +++ b/ios/Mappers.swift @@ -1050,4 +1050,27 @@ class Mappers { return nil } } + + class func financialConnectionsEventToMap(_ event: FinancialConnectionsEvent) -> [String: Any] { + var metadata: [String: Any] = [:] + + if let manualEntry = event.metadata.manualEntry { + metadata["manualEntry"] = manualEntry + } + + if let institutionName = event.metadata.institutionName { + metadata["institutionName"] = institutionName + } + + if let errorCode = event.metadata.errorCode { + metadata["errorCode"] = errorCode.rawValue + } + + let mappedEvent: [String: Any] = [ + "name": event.name.rawValue, + "metadata": metadata + ] + + return mappedEvent + } } diff --git a/ios/StripeSdk.swift b/ios/StripeSdk.swift index 97fa68ae6..123b858ac 100644 --- a/ios/StripeSdk.swift +++ b/ios/StripeSdk.swift @@ -71,7 +71,7 @@ class StripeSdk: RCTEventEmitter, UIAdaptivePresentationControllerDelegate { override func supportedEvents() -> [String]! { return ["onOrderTrackingCallback", "onConfirmHandlerCallback", "onCustomerAdapterFetchPaymentMethodsCallback", "onCustomerAdapterAttachPaymentMethodCallback", "onCustomerAdapterDetachPaymentMethodCallback", "onCustomerAdapterSetSelectedPaymentOptionCallback", "onCustomerAdapterFetchSelectedPaymentOptionCallback", - "onCustomerAdapterSetupIntentClientSecretForCustomerAttachCallback"] + "onCustomerAdapterSetupIntentClientSecretForCustomerAttachCallback", "onFinancialConnectionsEvent"] } @objc override static func requiresMainQueueSetup() -> Bool { @@ -733,13 +733,23 @@ class StripeSdk: RCTEventEmitter, UIAdaptivePresentationControllerDelegate { connectionsReturnURL = nil } + var onEvent: ((FinancialConnectionsEvent) -> Void)? = nil + + if hasEventListeners { + onEvent = { [weak self] event in + let mappedEvent = Mappers.financialConnectionsEventToMap(event) + self?.sendEvent(withName: "onFinancialConnectionsEvent", body: mappedEvent) + } + } + if (isPaymentIntent) { DispatchQueue.main.async { STPBankAccountCollector().collectBankAccountForPayment( clientSecret: clientSecret as String, returnURL: connectionsReturnURL, params: collectParams, - from: findViewControllerPresenter(from: UIApplication.shared.delegate?.window??.rootViewController ?? UIViewController()) + from: findViewControllerPresenter(from: UIApplication.shared.delegate?.window??.rootViewController ?? UIViewController()), + onEvent: onEvent ) { intent, error in if let error = error { resolve(Errors.createError(ErrorType.Failed, error as NSError)) @@ -765,7 +775,8 @@ class StripeSdk: RCTEventEmitter, UIAdaptivePresentationControllerDelegate { clientSecret: clientSecret as String, returnURL: connectionsReturnURL, params: collectParams, - from: findViewControllerPresenter(from: UIApplication.shared.delegate?.window??.rootViewController ?? UIViewController()) + from: findViewControllerPresenter(from: UIApplication.shared.delegate?.window??.rootViewController ?? UIViewController()), + onEvent: onEvent ) { intent, error in if let error = error { resolve(Errors.createError(ErrorType.Failed, error as NSError)) @@ -1040,9 +1051,19 @@ class StripeSdk: RCTEventEmitter, UIAdaptivePresentationControllerDelegate { if let urlScheme = urlScheme { returnURL = Mappers.mapToFinancialConnectionsReturnURL(urlScheme: urlScheme) } else { - returnURL = nil + returnURL = nil + } + + var onEvent: ((FinancialConnectionsEvent) -> Void)? = nil + + if hasEventListeners { + onEvent = { [weak self] event in + let mappedEvent = Mappers.financialConnectionsEventToMap(event) + self?.sendEvent(withName: "onFinancialConnectionsEvent", body: mappedEvent) + } } - FinancialConnections.presentForToken(withClientSecret: clientSecret, returnURL: returnURL, resolve: resolve) + + FinancialConnections.presentForToken(withClientSecret: clientSecret, returnURL: returnURL, onEvent: onEvent, resolve: resolve) } @objc(collectFinancialConnectionsAccounts:resolver:rejecter:) @@ -1059,9 +1080,19 @@ class StripeSdk: RCTEventEmitter, UIAdaptivePresentationControllerDelegate { if let urlScheme = urlScheme { returnURL = Mappers.mapToFinancialConnectionsReturnURL(urlScheme: urlScheme) } else { - returnURL = nil + returnURL = nil } - FinancialConnections.present(withClientSecret: clientSecret, returnURL: returnURL, resolve: resolve) + + var onEvent: ((FinancialConnectionsEvent) -> Void)? = nil + + if hasEventListeners { + onEvent = { [weak self] event in + let mappedEvent = Mappers.financialConnectionsEventToMap(event) + self?.sendEvent(withName: "onFinancialConnectionsEvent", body: mappedEvent) + } + } + + FinancialConnections.present(withClientSecret: clientSecret, returnURL: returnURL, onEvent: onEvent, resolve: resolve) } @objc(configureOrderTracking:orderIdentifier:webServiceUrl:authenticationToken:resolver:rejecter:) diff --git a/src/NativeStripeSdk.tsx b/src/NativeStripeSdk.tsx index f1eedd590..dbcc19dd3 100644 --- a/src/NativeStripeSdk.tsx +++ b/src/NativeStripeSdk.tsx @@ -84,7 +84,7 @@ type NativeStripeSdkType = { collectBankAccount( isPaymentIntent: boolean, clientSecret: string, - params: PaymentMethod.CollectBankAccountParams + params: Omit ): Promise; getConstants(): { API_VERSIONS: { CORE: string; ISSUING: string } }; canAddCardToWallet( diff --git a/src/functions.ts b/src/functions.ts index b39da179a..a7fafa64f 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -38,6 +38,8 @@ import { NativeModules, EmitterSubscription, } from 'react-native'; +import type { CollectFinancialConnectionsAccountsParams } from './types/FinancialConnections'; +import type { CollectBankAccountTokenParams } from './types/PaymentMethod'; export const createPaymentMethod = async ( params: PaymentMethod.CreateParams, @@ -356,6 +358,7 @@ export const verifyMicrodepositsForSetup = async ( const eventEmitter = new NativeEventEmitter(NativeModules.StripeSdk); let confirmHandlerCallback: EmitterSubscription | null = null; let orderTrackingCallbackListener: EmitterSubscription | null = null; +let financialConnectionsEventListener: EmitterSubscription | null = null; export const initPaymentSheet = async ( params: PaymentSheet.SetupParams @@ -468,6 +471,15 @@ export const collectBankAccountForPayment = async ( clientSecret: string, params: PaymentMethod.CollectBankAccountParams ): Promise => { + financialConnectionsEventListener?.remove(); + + if (params.onEvent) { + financialConnectionsEventListener = eventEmitter.addListener( + 'onFinancialConnectionsEvent', + params.onEvent + ); + } + try { const { paymentIntent, error } = (await NativeStripeSdk.collectBankAccount( true, @@ -475,6 +487,8 @@ export const collectBankAccountForPayment = async ( params )) as CollectBankAccountForPaymentResult; + financialConnectionsEventListener?.remove(); + if (error) { return { error, @@ -484,6 +498,7 @@ export const collectBankAccountForPayment = async ( paymentIntent: paymentIntent!, }; } catch (error: any) { + financialConnectionsEventListener?.remove(); return { error: createError(error), }; @@ -494,6 +509,15 @@ export const collectBankAccountForSetup = async ( clientSecret: string, params: PaymentMethod.CollectBankAccountParams ): Promise => { + financialConnectionsEventListener?.remove(); + + if (params.onEvent) { + financialConnectionsEventListener = eventEmitter.addListener( + 'onFinancialConnectionsEvent', + params.onEvent + ); + } + try { const { setupIntent, error } = (await NativeStripeSdk.collectBankAccount( false, @@ -501,6 +525,8 @@ export const collectBankAccountForSetup = async ( params )) as CollectBankAccountForSetupResult; + financialConnectionsEventListener?.remove(); + if (error) { return { error, @@ -510,6 +536,7 @@ export const collectBankAccountForSetup = async ( setupIntent: setupIntent!, }; } catch (error: any) { + financialConnectionsEventListener?.remove(); return { error: createError(error), }; @@ -520,15 +547,28 @@ export const collectBankAccountForSetup = async ( * Use collectBankAccountToken in the [Add a Financial Connections Account to a US Custom Connect](https://stripe.com/docs/financial-connections/connect-payouts) account flow. * When called, it will load the Authentication Flow, an on-page modal UI which allows your user to securely link their external financial account for payouts. * @param {string} clientSecret The client_secret of the [Financial Connections Session](https://stripe.com/docs/api/financial_connections/session). + * @param {CollectBankAccountTokenParams} params Optional parameters. * @returns A promise that resolves to an object containing either `session` and `token` fields, or an error field. */ export const collectBankAccountToken = async ( - clientSecret: string + clientSecret: string, + params: CollectBankAccountTokenParams = {} ): Promise => { + financialConnectionsEventListener?.remove(); + + if (params.onEvent) { + financialConnectionsEventListener = eventEmitter.addListener( + 'onFinancialConnectionsEvent', + params.onEvent + ); + } + try { const { session, token, error } = await NativeStripeSdk.collectBankAccountToken(clientSecret); + financialConnectionsEventListener?.remove(); + if (error) { return { error, @@ -539,6 +579,7 @@ export const collectBankAccountToken = async ( token: token!, }; } catch (error: any) { + financialConnectionsEventListener?.remove(); return { error: createError(error), }; @@ -549,15 +590,28 @@ export const collectBankAccountToken = async ( * Use collectFinancialConnectionsAccounts in the [Collect an account to build data-powered products](https://stripe.com/docs/financial-connections/other-data-powered-products) flow. * When called, it will load the Authentication Flow, an on-page modal UI which allows your user to securely link their external financial account. * @param {string} clientSecret The client_secret of the [Financial Connections Session](https://stripe.com/docs/api/financial_connections/session). + * @param {CollectFinancialConnectionsAccountsParams} params Optional parameters. * @returns A promise that resolves to an object containing either a `session` field, or an error field. */ export const collectFinancialConnectionsAccounts = async ( - clientSecret: string + clientSecret: string, + params: CollectFinancialConnectionsAccountsParams = {} ): Promise => { + financialConnectionsEventListener?.remove(); + + if (params.onEvent) { + financialConnectionsEventListener = eventEmitter.addListener( + 'onFinancialConnectionsEvent', + params.onEvent + ); + } + try { const { session, error } = await NativeStripeSdk.collectFinancialConnectionsAccounts(clientSecret); + financialConnectionsEventListener?.remove(); + if (error) { return { error, @@ -567,6 +621,7 @@ export const collectFinancialConnectionsAccounts = async ( session: session!, }; } catch (error: any) { + financialConnectionsEventListener?.remove(); return { error: createError(error), }; diff --git a/src/hooks/useFinancialConnectionsSheet.tsx b/src/hooks/useFinancialConnectionsSheet.tsx index 2236ae485..1ed929227 100644 --- a/src/hooks/useFinancialConnectionsSheet.tsx +++ b/src/hooks/useFinancialConnectionsSheet.tsx @@ -1,5 +1,7 @@ import { useState, useCallback } from 'react'; import { useStripe } from './useStripe'; +import type { CollectFinancialConnectionsAccountsParams } from 'src/types/FinancialConnections'; +import type { CollectBankAccountTokenParams } from 'src/types/PaymentMethod'; /** * React hook for accessing functions on the Financial Connections sheet. @@ -13,9 +15,9 @@ export function useFinancialConnectionsSheet() { useStripe(); const _collectBankAccountToken = useCallback( - async (clientSecret: string) => { + async (clientSecret: string, params?: CollectBankAccountTokenParams) => { setLoading(true); - const result = await collectBankAccountToken(clientSecret); + const result = await collectBankAccountToken(clientSecret, params); setLoading(false); return result; }, @@ -23,9 +25,15 @@ export function useFinancialConnectionsSheet() { ); const _collectFinancialConnectionsAccounts = useCallback( - async (clientSecret: string) => { + async ( + clientSecret: string, + params?: CollectFinancialConnectionsAccountsParams + ) => { setLoading(true); - const result = await collectFinancialConnectionsAccounts(clientSecret); + const result = await collectFinancialConnectionsAccounts( + clientSecret, + params + ); setLoading(false); return result; }, diff --git a/src/hooks/useStripe.tsx b/src/hooks/useStripe.tsx index 4530fd8c4..e486d96cb 100644 --- a/src/hooks/useStripe.tsx +++ b/src/hooks/useStripe.tsx @@ -60,6 +60,8 @@ import { updatePlatformPaySheet, openPlatformPaySetup, } from '../functions'; +import type { CollectBankAccountTokenParams } from 'src/types/PaymentMethod'; +import type { CollectFinancialConnectionsAccountsParams } from 'src/types/FinancialConnections'; /** * useStripe hook @@ -225,17 +227,21 @@ export function useStripe() { ); const _collectBankAccountToken = useCallback( - async (clientSecret: string): Promise => { - return collectBankAccountToken(clientSecret); + async ( + clientSecret: string, + params?: CollectBankAccountTokenParams + ): Promise => { + return collectBankAccountToken(clientSecret, params); }, [] ); const _collectFinancialConnectionsAccounts = useCallback( async ( - clientSecret: string + clientSecret: string, + params?: CollectFinancialConnectionsAccountsParams ): Promise => { - return collectFinancialConnectionsAccounts(clientSecret); + return collectFinancialConnectionsAccounts(clientSecret, params); }, [] ); diff --git a/src/types/FinancialConnections.ts b/src/types/FinancialConnections.ts index 225a8e6db..9c44c6852 100644 --- a/src/types/FinancialConnections.ts +++ b/src/types/FinancialConnections.ts @@ -1,6 +1,11 @@ import type { BankAccount } from './Token'; import type { StripeError } from './Errors'; +export type CollectFinancialConnectionsAccountsParams = { + /** An optional event listener to receive @type {FinancialConnectionEvent} for specific events during the process of a user connecting their financial accounts. */ + onEvent?: (event: FinancialConnectionsEvent) => void; +}; + export type SessionResult = | { /** The updated Financial Connections Session object. */ @@ -124,3 +129,67 @@ export enum FinancialConnectionsSheetError { Failed = 'Failed', Canceled = 'Canceled', } + +export type FinancialConnectionsEvent = { + /** The event's name. Represents the type of event that has occurred during the Financial Connections process. */ + name: FinancialConnectionsEventName; + /** Event-associated metadata. Provides further detail related to the occurred event. */ + metadata: FinancialConnectionsEventMetadata; +}; + +export enum FinancialConnectionsEventName { + /** Invoked when the sheet successfully opens. */ + Open = 'open', + /** Invoked when the manual entry flow is initiated. */ + ManualEntryInitiated = 'manual_entry_initiated', + /** Invoked when "Agree and continue" is selected on the consent pane. */ + ConsentAcquired = 'consent_acquired', + /** Invoked when the search bar is selected, the user inputs search terms, and receives an API response. */ + SearchInitiated = 'search_initiated', + /** Invoked when an institution is selected, either from featured institutions or search results. */ + InstitutionSelected = 'institution_selected', + /** Invoked when the authorization is successfully completed. */ + InstitutionAuthorized = 'institution_authorized', + /** Invoked when accounts are selected and "confirm" is selected. */ + AccountsSelected = 'accounts_selected', + /** Invoked when the flow is completed and selected accounts are correctly connected to the payment instrument. */ + Success = 'success', + /** Invoked when an error is encountered. Refer to error codes for more details. */ + Error = 'error', + /** Invoked when the flow is cancelled, typically by the user pressing the "X" button. */ + Cancel = 'cancel', + /** Invoked when the modal is launched in an external browser. After this event, no other events will be sent until the completion of the browser session. */ + FlowLaunchedInBrowser = 'flow_launched_in_browser', +} + +export type FinancialConnectionsEventMetadata = { + /** A Boolean value that indicates if the user completed the process through the manual entry flow. */ + manualEntry?: boolean; + /** A String value containing the name of the institution that the user selected. */ + institutionName?: string; + /** An ErrorCode value representing the type of error that occurred. */ + errorCode?: FinancialConnectionsEventErrorCode; +}; + +export enum FinancialConnectionsEventErrorCode { + /** The system could not retrieve account numbers for selected accounts. */ + AccountNumbersUnavailable = 'account_numbers_unavailable', + /** The system could not retrieve accounts for the selected institution. */ + AccountsUnavailable = 'accounts_unavailable', + /** For payment flows, no debitable account was available at the selected institution. */ + NoDebitableAccount = 'no_debitable_account', + /** Authorization with the selected institution has failed. */ + AuthorizationFailed = 'authorization_failed', + /** The selected institution is down for expected maintenance. */ + InstitutionUnavailablePlanned = 'institution_unavailable_planned', + /** The selected institution is unexpectedly down. */ + InstitutionUnavailableUnplanned = 'institution_unavailable_unplanned', + /** A timeout occurred while communicating with our partner or downstream institutions. */ + InstitutionTimeout = 'institution_timeout', + /** An unexpected error occurred, either in an API call or on the client-side. */ + UnexpectedError = 'unexpected_error', + /** The client secret that powers the session has expired. */ + SessionExpired = 'session_expired', + /** The hCaptcha challenge failed. */ + FailedBotDetection = 'failed_bot_detection', +} diff --git a/src/types/PaymentMethod.ts b/src/types/PaymentMethod.ts index 625d866b7..5c571c330 100644 --- a/src/types/PaymentMethod.ts +++ b/src/types/PaymentMethod.ts @@ -6,6 +6,7 @@ import type { } from './Token'; import type { FutureUsage } from './PaymentIntent'; import type { Address, BillingDetails } from './Common'; +import type { FinancialConnectionsEvent } from './FinancialConnections'; export interface Result { id: string; @@ -313,4 +314,11 @@ export type CollectBankAccountParams = { email?: string; }; }; + /** An optional event listener to receive @type {FinancialConnectionEvent} for specific events during the process of a user connecting their financial accounts. */ + onEvent?: (event: FinancialConnectionsEvent) => void; +}; + +export type CollectBankAccountTokenParams = { + /** An optional event listener to receive @type {FinancialConnectionEvent} for specific events during the process of a user connecting their financial accounts. */ + onEvent?: (event: FinancialConnectionsEvent) => void; };