| name | add-native-feature |
| description | Step-by-step guide for adding features requiring JS-to-native bridge communication in the Stripe React Native SDK. Covers TypeScript types, Android Kotlin, iOS Swift, event emitters, bidirectional callbacks, and native module specs. |
| when_to_use | Use when adding new native functionality, payment methods, extending components with platform-specific capabilities, implementing native-to-JS event communication, or adding new parameters that flow from JavaScript to native iOS/Android code. |
How to add features to React Native SDK
This guide explains how to add new features that require communication between React Native (JavaScript) and native code (iOS/Android). Use this when adding new native functionality, payment methods, or extending existing components with platform-specific capabilities.
Overview
The SDK uses a bidirectional communication pattern:
- Native -> JavaScript: Native code emits events that JavaScript listens for
- JavaScript -> Native: JavaScript invokes callbacks to return data to native code
Part 1: Passing Simple Data to Native SDKs
Use this when adding new configuration parameters that flow one-way from JavaScript to native code.
Step 1: Update TypeScript Types
Add your new parameter to the relevant type definition in src/types/.
Example: Adding onBehalfOf to PaymentSheet.IntentConfiguration
File: src/types/PaymentSheet.ts
export type IntentConfiguration = {
mode: Mode;
paymentMethodTypes?: PaymentMethod.Type[];
onBehalfOf?: string;
};
Step 2: Parse Parameters in Native Code
Extract the parameter from the bridge arguments and pass it to the native SDK.
Android Implementation
File: android/src/main/java/com/reactnativestripesdk/PaymentSheetManager.kt (or similar)
override fun onCreate() {
val onBehalfOf = arguments?.getString("onBehalfOf")
val intentConfiguration = PaymentSheet.IntentConfiguration(
mode = mode,
onBehalfOf = onBehalfOf
)
}
iOS Implementation
File: ios/StripeSdkImpl+PaymentSheet.swift
guard let intentConfiguration = params["intentConfiguration"] as? NSDictionary else {
return
}
let onBehalfOf = intentConfiguration["onBehalfOf"] as? String
let intentConfig = PaymentSheet.IntentConfiguration(
mode: mode,
onBehalfOf: onBehalfOf
)
Part 2: Implementing Bidirectional Communication
Use this when native code needs to request data from JavaScript (e.g., fetching client secrets, custom validation).
Communication Flow
React Native (JS) -> Registers Event Listener
|
Native Code (iOS/Android) -> Emits Event -> JS Listener Triggered
|
JS Executes Logic (API call, user input, etc.)
|
JS Invokes Native Callback -> Native Code Receives Result
|
Native Code Continues Execution
Step 1: Emit an Event from Native Code
Create the native code that will request data from JavaScript.
Android Implementation
File: android/src/main/java/com/reactnativestripesdk/ReactNativeCustomerSessionProvider.kt (or similar)
internal var provideSetupIntentClientSecretCallback: CompletableDeferred<String>? = null
override suspend fun provideSetupIntentClientSecret(customerId: String): Result<String> {
return suspendCancellableCoroutine { continuation ->
provideSetupIntentClientSecretCallback = continuation
stripeSdkModule?.eventEmitter?.emitOnCustomerSessionProviderSetupIntentClientSecret()
}
}
iOS Implementation
File: ios/StripeSdkImpl.swift
var clientSecretProviderSetupIntentClientSecretCallback: ((String) -> Void)? = nil
File: ios/StripeSdkImpl+CustomerSheet.swift
let intentConfiguration = CustomerSheet.IntentConfiguration(
setupIntentClientSecretProvider: {
return try await withCheckedThrowingContinuation { continuation in
self.clientSecretProviderSetupIntentClientSecretCallback = { clientSecret in
continuation.resume(returning: clientSecret)
}
self.emitter?.emitOnCustomerSessionProviderSetupIntentClientSecret()
}
}
)
Step 2: Define and Implement the Event Emitter
2a. Define the Event Type
File: src/events.ts
Add your event to the Events type:
type Events = {
onCustomerSessionProviderSetupIntentClientSecret: EventEmitter<void>;
onCustomerSessionProviderSetupIntentClientSecret: EventEmitter<{
customerId: string;
}>;
};
Guidelines:
- Use
EventEmitter<void> if no data is passed from native to JS
- Use
EventEmitter<{ param: type }> for simple parameters
- Use
EventEmitter<UnsafeObject<any>> for complex objects (use sparingly)
2b. Implement Android Emitter
File: android/src/main/java/com/reactnativestripesdk/EventEmitterCompat.kt
fun emitOnCustomerSessionProviderSetupIntentClientSecret(value: ReadableMap? = null) {
invoke("onCustomerSessionProviderSetupIntentClientSecret", value)
}
fun emitOnCustomerSessionProviderSetupIntentClientSecret() {
invoke("onCustomerSessionProviderSetupIntentClientSecret")
}
2c. Implement iOS Emitter
File: ios/StripeSdkEmitter.swift
@objc public protocol StripeSdkEmitter {
func emitOnCustomerSessionProviderSetupIntentClientSecret(_ value: [String: Any])
func emitOnCustomerSessionProviderSetupIntentClientSecret()
}
Step 3: Define Native Callback Signatures
These are the methods JavaScript will call to return data to native code.
3a. TypeScript Spec
File: src/specs/NativeStripeSdkModule.ts
export interface Spec extends TurboModule {
clientSecretProviderSetupIntentClientSecretCallback(
setupIntentClientSecret: string
): Promise<void>;
}
3b. Android Spec
File: android/src/oldarch/java/com/reactnativestripesdk/NativeStripeSdkModuleSpec.java
@ReactMethod
@DoNotStrip
public abstract void clientSecretProviderSetupIntentClientSecretCallback(
String setupIntentClientSecret,
Promise promise
);
3c. iOS Bridge Declaration
File: ios/StripeSdk.mm
RCT_EXPORT_METHOD(clientSecretProviderSetupIntentClientSecretCallback:(nonnull NSString *)setupIntentClientSecret
resolve:(nonnull RCTPromiseResolveBlock)resolve
reject:(nonnull RCTPromiseRejectBlock)reject)
{
[StripeSdkImpl.shared clientSecretProviderSetupIntentClientSecretCallback:setupIntentClientSecret
resolver:resolve
rejecter:reject];
}
Step 4: Implement JavaScript Event Listener
Listen for the native event and invoke the callback with the result.
File: src/components/CustomerSheet.tsx (or relevant component)
let setupIntentClientSecretProviderCallback: EventSubscription | null = null;
const configureClientSecretProviderEventListeners = (
clientSecretProvider: ClientSecretProvider
): void => {
setupIntentClientSecretProviderCallback?.remove();
setupIntentClientSecretProviderCallback = addListener(
'onCustomerSessionProviderSetupIntentClientSecret',
async () => {
try {
const setupIntentClientSecret =
await clientSecretProvider.provideSetupIntentClientSecret();
await NativeStripeSdk.clientSecretProviderSetupIntentClientSecretCallback(
setupIntentClientSecret
);
} catch (error) {
console.error('Failed to provide setup intent client secret:', error);
}
}
);
};
If the event includes parameters from native:
setupIntentClientSecretProviderCallback = addListener(
'onCustomerSessionProviderSetupIntentClientSecret',
async ({ customerId }) => {
const setupIntentClientSecret =
await clientSecretProvider.provideSetupIntentClientSecret(customerId);
await NativeStripeSdk.clientSecretProviderSetupIntentClientSecretCallback(
setupIntentClientSecret
);
}
);
Important: Don't forget to clean up listeners when the component unmounts or is reconfigured.
Step 5: Complete the Native Callback Implementation
Resume the async operation started in Step 1 with the data from JavaScript.
Android Implementation
File: android/src/main/java/com/reactnativestripesdk/StripeSdkModule.kt
override fun clientSecretProviderSetupIntentClientSecretCallback(
setupIntentClientSecret: String,
promise: Promise
) {
customerSheetFragment?.let {
it.customerSessionProvider?.provideSetupIntentClientSecretCallback?.resume(
Result.success(setupIntentClientSecret)
)
promise.resolve(null)
} ?: run {
promise.reject(
"CustomerSheetNotInitialized",
"Customer Sheet must be initialized before calling this callback"
)
}
}
iOS Implementation
File: ios/StripeSdkImpl+CustomerSheet.swift
@objc(clientSecretProviderSetupIntentClientSecretCallback:resolver:rejecter:)
public func clientSecretProviderSetupIntentClientSecretCallback(
setupIntentClientSecret: String,
resolver resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock
) -> Void {
self.clientSecretProviderSetupIntentClientSecretCallback?(setupIntentClientSecret)
self.clientSecretProviderSetupIntentClientSecretCallback = nil
resolve([])
}
Implementation Checklist
Part 1: Simple Data Passing
Part 2: Bidirectional Communication
Testing & Documentation
Common Pitfalls
Memory Leaks
Problem: Forgetting to remove event listeners.
Solution: Always call .remove() on subscriptions before creating new ones or when unmounting.
useEffect(() => {
const subscription = addListener('myEvent', handler);
return () => {
subscription?.remove();
};
}, []);
Missing Error Handling
Problem: Not handling errors in async callbacks.
Solution: Wrap callback logic in try-catch blocks and handle failures gracefully.
async () => {
try {
const result = await userProvidedFunction();
await NativeStripeSdk.callback(result);
} catch (error) {
console.error('Error:', error);
}
}
Thread Safety (iOS)
Problem: Updating UI from background threads.
Solution: Ensure UI updates happen on the main thread:
DispatchQueue.main.async {
}
Incomplete Callback Resolution
Problem: Not calling promise.resolve() or promise.reject() in native code.
Solution: Always resolve or reject promises, even in error cases.
Type Mismatches
Problem: TypeScript types don't match native expectations.
Solution: Use UnsafeObject<T> for complex types and validate in native code.
Platform-Specific Considerations
iOS
- Async/Await: Uses Swift continuations (
withCheckedThrowingContinuation)
- Callbacks: Stored as optional closures (
((String) -> Void)?)
- Threading: UI operations must run on main thread
- Memory: Be careful with retain cycles; use
[weak self] when needed
Android
- Async/Await: Uses Kotlin coroutines and
suspendCancellableCoroutine
- Callbacks: Uses
CancellableContinuation or CompletableDeferred
- Threading: React Native bridge handles threading automatically
- Lifecycle: Be aware of Activity/Fragment lifecycle when storing callbacks
Additional Resources