mcpbeat Sign in

Add Native Feature Agent Skill

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.

4k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
1429
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/stripe/stripe-react-native --skill add-native-feature

The instruction itself

38 sections, as written by the author

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; // New parameter
};

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() {
  // 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
  )
}
iOS Implementation

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
)

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 ->
    // Store the continuation to resume later
    provideSetupIntentClientSecretCallback = continuation

    // Emit the event to JavaScript
    stripeSdkModule?.eventEmitter?.emitOnCustomerSessionProviderSetupIntentClientSecret()
  }
}
iOS Implementation

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()
    }
  }
)

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 = {
  // ... existing events ...
  onCustomerSessionProviderSetupIntentClientSecret: EventEmitter<void>; // No parameters
  // OR if you need to pass data:
  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)
}

// For events with no parameters:
fun emitOnCustomerSessionProviderSetupIntentClientSecret() {
  invoke("onCustomerSessionProviderSetupIntentClientSecret")
}
2c. Implement iOS Emitter

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()
}

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 {
  // ... existing methods ...

  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)

// 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.

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 {
    // 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"
    )
  }
}
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 {
  // Resume the continuation with the result from JavaScript
  self.clientSecretProviderSetupIntentClientSecretCallback?(setupIntentClientSecret)

  // Clear the callback
  self.clientSecretProviderSetupIntentClientSecretCallback = nil

  resolve([])
}

Implementation Checklist

Part 1: Simple Data Passing

  • [ ] TypeScript types updated in src/types/
  • [ ] Android parameter parsing implemented
  • [ ] iOS parameter parsing implemented

Part 2: Bidirectional Communication

  • [ ] Event emission added in Android native code
  • [ ] Event emission added in iOS native code
  • [ ] Event type defined in src/events.ts
  • [ ] Android emitter implemented in EventEmitterCompat.kt
  • [ ] iOS emitter declared in StripeSdkEmitter.swift
  • [ ] TypeScript callback spec added to NativeStripeSdkModule.ts
  • [ ] Android callback spec added to NativeStripeSdkModuleSpec.java
  • [ ] iOS bridge method added to StripeSdk.mm
  • [ ] JavaScript event listener implemented in component
  • [ ] Android callback completion implemented in StripeSdkModule.kt
  • [ ] iOS callback completion implemented in Swift

Testing & Documentation

  • [ ] Unit tests written for TypeScript code
  • [ ] Native tests written (iOS XCTest / Android)
  • [ ] Example app updated to demonstrate feature
  • [ ] E2E tests written using Maestro
  • [ ] Code runs without linter errors (yarn lint)
  • [ ] TypeScript compiles without errors (yarn typescript)
  • [ ] Tested on both iOS and Android
  • [ ] Tested with both Old and New Architecture

Common Pitfalls

Memory Leaks

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();
  };
}, []);

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);
    // Consider how to communicate errors back to native
  }
}

Thread Safety (iOS)

Problem: Updating UI from background threads.

Solution: Ensure UI updates happen on the main thread:

DispatchQueue.main.async {
  // UI updates here
}

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

  • React Native TurboModules: https://reactnative.dev/docs/the-new-architecture/pillars-turbomodules
  • Stripe iOS SDK: https://stripe.dev/stripe-ios
  • Stripe Android SDK: https://stripe.dev/stripe-android

Other skills for the same job

different authors, same section of the catalogue
MCP Builder
by anthropics
vendor ×13

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).

30k tokens scripts
Changelog Generator
by frostant
×9

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.

774 tokens
Finishing A Development Branch
by ZhanlinCui
×7

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

1k tokens
MCP Builder
by JayZeeDesign
×7

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).

37k tokens scripts
Vercel React Native Skills
by vercel-labs
vendor ×6

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.

39k tokens
Vercel React Best Practices
by ratacat
×5

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.

34k tokens
Next Best Practices
by vercel-labs
vendor ×4

Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling

20k tokens
Using Git Worktrees
by ZhanlinCui
×4

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

1k tokens

How to use it

Copy the folder

Take stripe/add-native-feature from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

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.