Skip to content

Commit

Permalink
Support events in Financial Connections (#1828)
Browse files Browse the repository at this point in the history
* Support events in Financial Connections

* Address code review feedback

Use `financialConnectionsEventListener` instead of local subscriptions

* Allow `onEvent` with `useFinancialConnectionsSheet`

* Only set `onEvent` if SDK has event listeners

* Wrap `onEvent` in `params` construct

* Add release notes

* Update docs

* Mention all methods in the release notes
  • Loading branch information
tillh-stripe authored Feb 21, 2025
1 parent 24f3990 commit c318f98
Show file tree
Hide file tree
Showing 15 changed files with 349 additions and 22 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ 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.*
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
Expand All @@ -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()
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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 -> {
Expand Down
46 changes: 46 additions & 0 deletions android/src/main/java/com/reactnativestripesdk/utils/Mappers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -957,3 +958,48 @@ internal fun mapToPreferredNetworks(networksAsInts: ArrayList<Int>?): List<CardB
intToCardBrand[it]
}
}

internal fun mapFromFinancialConnectionsEvent(event: FinancialConnectionsEvent): WritableMap {
return Arguments.createMap().apply {
putString("name", event.name.value)
putMap("metadata", event.metadata.toMap().toReadableMap())
}
}

private fun List<Any?>.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<String, Any?>).toReadableMap())
is List<*> -> writableArray.pushArray((value as List<Any?>).toWritableArray())
else -> writableArray.pushString(value.toString())
}
}

return writableArray
}

private fun Map<String, Any?>.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<String, Any?>).toReadableMap())
is List<*> -> writableMap.putArray(key, (value as List<Any?>).toWritableArray())
else -> writableMap.putString(key, value.toString())
}
}

return writableMap
}
7 changes: 7 additions & 0 deletions example/src/screens/ACHPaymentScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
verifyMicrodepositsForPayment,
VerifyMicrodepositsParams,
collectBankAccountForPayment,
FinancialConnections,
} from '@stripe/stripe-react-native';
import Button from '../components/Button';
import PaymentScreen from '../components/PaymentScreen';
Expand Down Expand Up @@ -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,
{
Expand All @@ -146,6 +152,7 @@ export default function ACHPaymentScreen() {
email,
},
},
onEvent: onEvent,
}
);

Expand Down
15 changes: 13 additions & 2 deletions example/src/screens/CollectBankAccountScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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('');
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
16 changes: 14 additions & 2 deletions ios/FinancialConnections.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
23 changes: 23 additions & 0 deletions ios/Mappers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
45 changes: 38 additions & 7 deletions ios/StripeSdk.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
Expand All @@ -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))
Expand Down Expand Up @@ -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:)
Expand All @@ -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:)
Expand Down
2 changes: 1 addition & 1 deletion src/NativeStripeSdk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ type NativeStripeSdkType = {
collectBankAccount(
isPaymentIntent: boolean,
clientSecret: string,
params: PaymentMethod.CollectBankAccountParams
params: Omit<PaymentMethod.CollectBankAccountParams, 'onEvent'>
): Promise<ConfirmSetupIntentResult | ConfirmPaymentResult>;
getConstants(): { API_VERSIONS: { CORE: string; ISSUING: string } };
canAddCardToWallet(
Expand Down
Loading

0 comments on commit c318f98

Please sign in to comment.