From de1df5758e5c50410731b48000746001ab29a3a4 Mon Sep 17 00:00:00 2001 From: David Estes Date: Wed, 23 Jul 2025 14:19:25 -0700 Subject: [PATCH 1/3] Add CLAUDE.md --- CLAUDE.md | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..6927a5548 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,134 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +### Building and Linting +- `yarn typescript` - Type-check TypeScript files +- `yarn lint` - Lint code with ESLint +- `yarn lint --fix` - Fix linting issues automatically +- `yarn test` - Run Jest unit tests +- `yarn prepare` - Build the library (runs `bob build && husky`) + +### Development Setup +- `yarn bootstrap` - Setup project by installing dependencies and CocoaPods +- `yarn bootstrap-no-pods` - Setup without installing CocoaPods +- `yarn pods` - Install CocoaPods for iOS (example app) +- `yarn update-pods` - Update specific Stripe pod dependencies + +### Example App Development Workflow +1. **Setup**: `yarn bootstrap` +2. **Configure**: `cp example/.env.example example/.env` and set values +3. **Run Example App** (requires 3 terminals): + - Terminal 1: `yarn example start:server` + - Terminal 2: `yarn example start` + - Terminal 3: `yarn example ios` OR `yarn example android` + +### Example App Commands +- `yarn example start` - Start Metro server +- `yarn example start:server` - Start backend server for examples +- `yarn run-example-ios` - Run iOS example app on iPhone 16 simulator +- `yarn run-example-android` - Run Android example app +- `yarn run-example-ios:release` - Build and run iOS app in release mode +- `yarn run-example-android:release` - Build and run Android app in release mode + +### Testing +- `yarn test:e2e:ios` - Run all iOS E2E tests using Maestro +- `yarn test:e2e:android` - Run all Android E2E tests using Maestro +- `yarn test-ios ./path/to/test.yml` - Run single iOS E2E test +- `yarn test-android ./path/to/test.yml` - Run single Android E2E test +- `yarn test:unit:ios` - Run iOS native unit tests via Xcode +- `yarn test:unit:android` - Run Android unit tests + +### Documentation +- `yarn docs` - Generate API documentation using TypeDoc + +### Commit Convention +Follow [conventional commits](https://www.conventionalcommits.org/en): +- `fix:` - Bug fixes +- `feat:` - New features +- `refactor:` - Code refactoring +- `docs:` - Documentation changes +- `test:` - Test additions/updates +- `chore:` - Tooling changes + +## Architecture Overview + +This is the official Stripe React Native SDK, providing payment processing capabilities for mobile apps. + +### Key Directories + +- **`src/`** - Main TypeScript source code + - `components/` - React Native components (CardField, PaymentSheet, etc.) + - `hooks/` - React hooks for payment functionality + - `types/` - TypeScript type definitions + - `specs/` - Native module specifications for code generation + - `functions.ts` - Core payment functions + - `index.tsx` - Main exports + +- **`ios/`** - Native iOS implementation in Swift/Objective-C + - Uses Stripe iOS SDK (~24.16.1) + - Supports both Old and New Architecture (Fabric) + - Test files in `ios/Tests/` + +- **`android/`** - Native Android implementation in Kotlin/Java + - Uses Stripe Android SDK + - Gradle build configuration + +- **`example/`** - Example React Native app demonstrating SDK usage + - Contains test server in `server/` directory + - Configured for both iOS and Android development + +- **`e2e-tests/`** - End-to-end tests using Maestro framework + - Platform-specific tests in `ios-only/` and `android-only/` + - Tests payment flows, UI components, and integrations + +### Code Generation + +The SDK uses React Native's TurboModules/Fabric for native communication: +- Specs defined in `src/specs/` generate native interfaces +- Both Old and New Architecture supported +- Special patch for Old Architecture compatibility in `patches/old-arch-codegen-fix.patch` + +### Build System + +- **React Native Builder Bob** - Builds CommonJS, ES modules, and TypeScript declarations +- **CocoaPods** - iOS dependency management +- **Gradle** - Android build system +- **Expo Plugin** - `src/plugin/withStripe.ts` for Expo integration + +### Key Components + +- **StripeProvider** - Context provider for SDK initialization +- **PaymentSheet** - Pre-built payment UI +- **CardField/CardForm** - Card input components +- **PlatformPayButton** - Apple Pay/Google Pay integration +- **CustomerSheet** - Customer payment method management +- **AddressSheet** - Address collection component + +### Testing Strategy + +- **Jest** - Unit tests for TypeScript code +- **Maestro** - E2E testing framework for mobile flows +- **Native Tests** - iOS XCTest and Android instrumentation tests +- Mock implementation provided in `jest/mock.js` + +### Platform-Specific Notes + +- **iOS**: Requires Xcode 14.1+, iOS 13+ deployment target +- **Android**: Requires API 21+, compileSdkVersion 34, Kotlin 2.x +- **React Native**: Compatible with 0.78+, TypeScript 5.7+ +- **Expo**: Supported via plugin configuration + +### Development File Locations + +- **iOS Native**: Open `example/ios/StripeSdkExample.xcworkspace` in Xcode + - Source files: `Pods > Development Pods > stripe-react-native` +- **Android Native**: Open `example/android` in Android Studio + - Source files: `reactnativestripesdk` under `Android` +- **TypeScript**: Edit files in `src/` and `example/` + +### Old Architecture Compatibility + +The SDK maintains compatibility with React Native's Old Architecture via `patches/old-arch-codegen-fix.patch`. This patch converts EventEmitter properties to callback functions for code generation compatibility with RN ≥ 0.74. From c783b40b3e5186f71ac1ca7278b27074613810ea Mon Sep 17 00:00:00 2001 From: David Estes Date: Wed, 23 Jul 2025 17:27:14 -0700 Subject: [PATCH 2/3] Protect against double clicks in the example app --- example/src/screens/ApplePayScreen.tsx | 70 +++++++++++++++----------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/example/src/screens/ApplePayScreen.tsx b/example/src/screens/ApplePayScreen.tsx index 7765a17ed..a08b3a3a6 100644 --- a/example/src/screens/ApplePayScreen.tsx +++ b/example/src/screens/ApplePayScreen.tsx @@ -21,6 +21,7 @@ export default function ApplePayScreen() { const [cardDetails, setCardDetails] = useState(null); const [isApplePaySupported, setIsApplePaySupported] = useState(false); const [clientSecret, setClientSecret] = useState(null); + const [isPaymentInProgress, setIsPaymentInProgress] = useState(false); const { createPlatformPayPaymentMethod, createPlatformPayToken, @@ -213,34 +214,47 @@ export default function ApplePayScreen() { Alert.alert('No client secret is set.'); return; } - const { paymentIntent, error } = await confirmPlatformPayPayment( - clientSecret as string, - { - applePay: { - cartItems: cart, - merchantCountryCode: 'US', - currencyCode: 'USD', - shippingMethods, - requiredShippingAddressFields: [ - PlatformPay.ContactField.EmailAddress, - PlatformPay.ContactField.PhoneNumber, - PlatformPay.ContactField.PostalAddress, - PlatformPay.ContactField.Name, - ], - requiredBillingContactFields: [ - PlatformPay.ContactField.PostalAddress, - ], - shippingType: PlatformPay.ApplePayShippingType.StorePickup, - additionalEnabledNetworks: ['JCB'], - }, + + // Prevent multiple simultaneous payment attempts + if (isPaymentInProgress) { + console.log('Payment already in progress, ignoring duplicate request'); + return; + } + + setIsPaymentInProgress(true); + + try { + const { paymentIntent, error } = await confirmPlatformPayPayment( + clientSecret as string, + { + applePay: { + cartItems: cart, + merchantCountryCode: 'US', + currencyCode: 'USD', + shippingMethods, + requiredShippingAddressFields: [ + PlatformPay.ContactField.EmailAddress, + PlatformPay.ContactField.PhoneNumber, + PlatformPay.ContactField.PostalAddress, + PlatformPay.ContactField.Name, + ], + requiredBillingContactFields: [ + PlatformPay.ContactField.PostalAddress, + ], + shippingType: PlatformPay.ApplePayShippingType.StorePickup, + additionalEnabledNetworks: ['JCB'], + }, + } + ); + if (error) { + Alert.alert(error.code, error.localizedMessage); + } else { + Alert.alert('Success', 'Check the logs for payment intent details.'); + console.log(JSON.stringify(paymentIntent, null, 2)); + setClientSecret(null); } - ); - if (error) { - Alert.alert(error.code, error.localizedMessage); - } else { - Alert.alert('Success', 'Check the logs for payment intent details.'); - console.log(JSON.stringify(paymentIntent, null, 2)); - setClientSecret(null); + } finally { + setIsPaymentInProgress(false); } }; @@ -316,7 +330,7 @@ export default function ApplePayScreen() { onPress={pay} appearance={PlatformPay.ButtonStyle.White} borderRadius={4} - disabled={!isApplePaySupported} + disabled={!isApplePaySupported || isPaymentInProgress} style={styles.payButton} onShippingContactSelected={({ shippingContact }) => { console.log(JSON.stringify(shippingContact, null, 2)); From cd8b188cef6d32a3169d4c0b6524676a68dcdf11 Mon Sep 17 00:00:00 2001 From: David Estes Date: Wed, 23 Jul 2025 17:34:57 -0700 Subject: [PATCH 3/3] Add better error --- ios/StripeSdkImpl.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ios/StripeSdkImpl.swift b/ios/StripeSdkImpl.swift index b73efa020..bdfbb948b 100644 --- a/ios/StripeSdkImpl.swift +++ b/ios/StripeSdkImpl.swift @@ -467,6 +467,12 @@ public class StripeSdkImpl: NSObject, UIAdaptivePresentationControllerDelegate { return } + // Prevent multiple simultaneous Apple Pay presentations + if self.confirmApplePayResolver != nil { + resolve(Errors.createError(ErrorType.Failed, "Apple Pay is already in progress")) + return + } + self.applePaySummaryItems = paymentRequest.paymentSummaryItems self.applePayShippingMethods = paymentRequest.shippingMethods ?? [] self.applePayShippingAddressErrors = nil