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.
npx skills add https://github.com/stripe/stripe-react-native --skill add-native-feature
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.
The SDK uses a bidirectional communication pattern:
Use this when adding new configuration parameters that flow one-way from JavaScript to native code.
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; // New parameter
};
Extract the parameter from the bridge arguments and pass it to the native SDK.
File: android/src/main/java/com/reactnativestripesdk/PaymentSheetManager.kt (or similar)
override fun onCreate() {
// Parse the parameter from arguments
val onBehalfOf = arguments?.getString("onBehalfOf")
// Pass it to the native SDK
val intentConfiguration = PaymentSheet.IntentConfiguration(
mode = mode,
// ... other existing parameters ...
onBehalfOf = onBehalfOf
)
}
File: ios/StripeSdkImpl+PaymentSheet.swift
guard let intentConfiguration = params["intentConfiguration"] as? NSDictionary else {
// handle error
return
}
// Extract the parameter
let onBehalfOf = intentConfiguration["onBehalfOf"] as? String
// Build the configuration
let intentConfig = PaymentSheet.IntentConfiguration(
mode: mode,
// ... other existing parameters ...
onBehalfOf: onBehalfOf
)
Use this when native code needs to request data from JavaScript (e.g., fetching client secrets, custom validation).
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
Create the native code that will request data from JavaScript.
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 ->
// Store the continuation to resume later
provideSetupIntentClientSecretCallback = continuation
// Emit the event to JavaScript
stripeSdkModule?.eventEmitter?.emitOnCustomerSessionProviderSetupIntentClientSecret()
}
}
File: ios/StripeSdkImpl.swift
// Store the continuation as a property
var clientSecretProviderSetupIntentClientSecretCallback: ((String) -> Void)? = nil
File: ios/StripeSdkImpl+CustomerSheet.swift
let intentConfiguration = CustomerSheet.IntentConfiguration(
// ... other parameters ...
setupIntentClientSecretProvider: {
return try await withCheckedThrowingContinuation { continuation in
// Store the continuation to be resumed later
self.clientSecretProviderSetupIntentClientSecretCallback = { clientSecret in
continuation.resume(returning: clientSecret)
}
// Emit the event to JavaScript
self.emitter?.emitOnCustomerSessionProviderSetupIntentClientSecret()
}
}
)
File: src/events.ts
Add your event to the Events type:
type Events = {
// ... existing events ...
onCustomerSessionProviderSetupIntentClientSecret: EventEmitter<void>; // No parameters
// OR if you need to pass data:
onCustomerSessionProviderSetupIntentClientSecret: EventEmitter<{
customerId: string;
}>;
};
Guidelines:
EventEmitter<void> if no data is passed from native to JSEventEmitter<{ param: type }> for simple parametersEventEmitter<UnsafeObject<any>> for complex objects (use sparingly)File: android/src/main/java/com/reactnativestripesdk/EventEmitterCompat.kt
fun emitOnCustomerSessionProviderSetupIntentClientSecret(value: ReadableMap? = null) {
invoke("onCustomerSessionProviderSetupIntentClientSecret", value)
}
// For events with no parameters:
fun emitOnCustomerSessionProviderSetupIntentClientSecret() {
invoke("onCustomerSessionProviderSetupIntentClientSecret")
}
File: ios/StripeSdkEmitter.swift
@objc public protocol StripeSdkEmitter {
// ... existing methods ...
// For events with parameters:
func emitOnCustomerSessionProviderSetupIntentClientSecret(_ value: [String: Any])
// For events without parameters:
func emitOnCustomerSessionProviderSetupIntentClientSecret()
}
These are the methods JavaScript will call to return data to native code.
File: src/specs/NativeStripeSdkModule.ts
export interface Spec extends TurboModule {
// ... existing methods ...
clientSecretProviderSetupIntentClientSecretCallback(
setupIntentClientSecret: string
): Promise<void>;
}
File: android/src/oldarch/java/com/reactnativestripesdk/NativeStripeSdkModuleSpec.java
@ReactMethod
@DoNotStrip
public abstract void clientSecretProviderSetupIntentClientSecretCallback(
String setupIntentClientSecret,
Promise promise
);
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];
}
Listen for the native event and invoke the callback with the result.
File: src/components/CustomerSheet.tsx (or relevant component)
// Declare the EventSubscription at the top of the file
let setupIntentClientSecretProviderCallback: EventSubscription | null = null;
const configureClientSecretProviderEventListeners = (
clientSecretProvider: ClientSecretProvider
): void => {
// Remove existing listener to prevent duplicates
setupIntentClientSecretProviderCallback?.remove();
// Register the event listener
setupIntentClientSecretProviderCallback = addListener(
'onCustomerSessionProviderSetupIntentClientSecret',
async () => {
try {
// Execute the user-provided function (e.g., API call)
const setupIntentClientSecret =
await clientSecretProvider.provideSetupIntentClientSecret();
// Return the result to native code
await NativeStripeSdk.clientSecretProviderSetupIntentClientSecretCallback(
setupIntentClientSecret
);
} catch (error) {
// Handle errors appropriately
console.error('Failed to provide setup intent client secret:', error);
}
}
);
};
If the event includes parameters from native:
setupIntentClientSecretProviderCallback = addListener(
'onCustomerSessionProviderSetupIntentClientSecret',
async ({ customerId }) => { // Destructure parameters
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.
Resume the async operation started in Step 1 with the data from JavaScript.
File: android/src/main/java/com/reactnativestripesdk/StripeSdkModule.kt
override fun clientSecretProviderSetupIntentClientSecretCallback(
setupIntentClientSecret: String,
promise: Promise
) {
customerSheetFragment?.let {
// Resume the coroutine with the result from JavaScript
it.customerSessionProvider?.provideSetupIntentClientSecretCallback?.resume(
Result.success(setupIntentClientSecret)
)
promise.resolve(null)
} ?: run {
promise.reject(
"CustomerSheetNotInitialized",
"Customer Sheet must be initialized before calling this callback"
)
}
}
File: ios/StripeSdkImpl+CustomerSheet.swift
@objc(clientSecretProviderSetupIntentClientSecretCallback:resolver:rejecter:)
public func clientSecretProviderSetupIntentClientSecretCallback(
setupIntentClientSecret: String,
resolver resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock
) -> Void {
// Resume the continuation with the result from JavaScript
self.clientSecretProviderSetupIntentClientSecretCallback?(setupIntentClientSecret)
// Clear the callback
self.clientSecretProviderSetupIntentClientSecretCallback = nil
resolve([])
}
src/types/src/events.tsEventEmitterCompat.ktStripeSdkEmitter.swiftNativeStripeSdkModule.tsNativeStripeSdkModuleSpec.javaStripeSdk.mmStripeSdkModule.ktyarn lint)yarn typescript)Problem: Forgetting to remove event listeners.
Solution: Always call .remove() on subscriptions before creating new ones or when unmounting.
useEffect(() => {
// Setup listener
const subscription = addListener('myEvent', handler);
return () => {
// Cleanup on unmount
subscription?.remove();
};
}, []);
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);
// Consider how to communicate errors back to native
}
}
Problem: Updating UI from background threads.
Solution: Ensure UI updates happen on the main thread:
DispatchQueue.main.async {
// UI updates here
}
Problem: Not calling promise.resolve() or promise.reject() in native code.
Solution: Always resolve or reject promises, even in error cases.
Problem: TypeScript types don't match native expectations.
Solution: Use UnsafeObject<T> for complex types and validate in native code.
withCheckedThrowingContinuation)((String) -> Void)?)[weak self] when neededsuspendCancellableCoroutineCancellableContinuation or CompletableDeferredGuide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
Automatically creates user-facing changelogs from git commits by analyzing commit history, categorizing changes, and transforming technical commits into clear, customer-friendly release notes. Turns hours of manual changelog writing into minutes of automated generation.
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
React Native and Expo best practices for building performant mobile apps. Use when building React Native components, optimizing list performance, implementing animations, or working with native modules. Triggers on tasks involving React Native, Expo, mobile performance, or native platform APIs.
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling
Use when starting feature work that needs isolation from current workspace or before executing implementation plans - creates isolated git worktrees with smart directory selection and safety verification
Take stripe/add-native-feature from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.