npm package: @fredyk/react-native-stripe-checkout-webview
Description: A fully configurable React Native library for Stripe Checkout integration via WebView
Author: React Native Community
Latest version: 1.0.0-rc07

# @fredyk/react-native-stripe-checkout-webview

💰 A fully configurable React Native library for Stripe Checkout integration via WebView

[![npm version](https://img.shields.io/npm/v/@fredyk/react-native-stripe-checkout-webview.svg)](https://www.npmjs.com/package/@fredyk/react-native-stripe-checkout-webview)
[![TypeScript](https://img.shields.io/badge/TypeScript-100%25-blue)](https://www.typescriptlang.org/)
[![Coverage](https://img.shields.io/badge/Coverage-100%25-brightgreen)](https://www.npmjs.com/package/@fredyk/react-native-stripe-checkout-webview)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## 🌟 Why this library?

After extensive research of existing solutions (react-native-stripe-checkout-webview, expo-stripe-checkout), we built this library with key improvements:

- **100% Configurable**: No hardcoded values - configure everything from URLs to pricing
- **Backend Agnostic**: Works with any backend that implements a compatible checkout endpoint
- **TypeScript First**: Full type safety with comprehensive type definitions
- **Modern Architecture**: Clean separation of concerns with services, components, and hooks
- **Minimal Dependencies**: Only `react-native-webview` required
- **Production Ready**: Robust error handling, timeout management, and loading states
- **100% Test Coverage**: 156 tests with complete code coverage for reliability

## 📚 Research & Best Practices

This library was built after analyzing:
- Official Stripe documentation for mobile apps
- Existing open-source implementations (react-native-stripe-checkout-webview, expo-stripe-checkout)
- Community discussions on StackOverflow and Medium
- Real-world production implementations

**Key findings:**
- WebView is the recommended approach for Stripe Checkout in React Native
- Backend should create the checkout session (never client-side with publishable key)
- URL monitoring is reliable for detecting success/cancel events
- Session ID can be extracted from URL parameters or metadata

## 🚀 Quick Start

### Installation

```bash
npm install @fredyk/react-native-stripe-checkout-webview
# or
yarn add @fredyk/react-native-stripe-checkout-webview
# or
pnpm add @fredyk/react-native-stripe-checkout-webview
```

**Prerequisites:**
```bash
npm install react-native-webview
```

For Expo apps:
```bash
npx expo install react-native-webview
```

### Basic Usage

```tsx
import { StripeCheckoutWebView } from '@fredyk/react-native-stripe-checkout-webview';
import AsyncStorage from '@react-native-async-storage/async-storage';

function CheckoutScreen() {
  const [visible, setVisible] = useState(false);

  const config = {
    checkoutUrl: 'https://api.yourapp.com/payment/checkout',
    getUserData: async () => ({
      id: await AsyncStorage.getItem('userId'),
      email: await AsyncStorage.getItem('email'),
    }),
    getAuthToken: async () => await AsyncStorage.getItem('token'),
  };

  return (
    <>
      <Button 
        title="Subscribe" 
        onPress={() => setVisible(true)} 
      />
      
      <StripeCheckoutWebView
        config={config}
        planId="premium-monthly"
        visible={visible}
        onSuccess={(sessionId) => {
          console.log('Payment successful:', sessionId);
          setVisible(false);
          navigation.navigate('Success');
        }}
        onCancel={() => {
          console.log('Payment cancelled');
          setVisible(false);
        }}
        onError={(error) => {
          console.error('Payment error:', error);
          setVisible(false);
          Alert.alert('Error', error.message);
        }}
      />
    </>
  );
}
```

### Using the Hook

```tsx
import { useStripeCheckout } from '@fredyk/react-native-stripe-checkout-webview';

function PricingScreen() {
  const { openCheckout, isLoading, isVisible, closeCheckout } = useStripeCheckout(config);

  return (
    <View>
      <Button
        title="Subscribe Monthly"
        onPress={() => openCheckout('premium-monthly')}
        disabled={isLoading}
      />
      <Button
        title="Subscribe Yearly"
        onPress={() => openCheckout('premium-yearly')}
        disabled={isLoading}
      />
    </View>
  );
}
```

## 🔧 Configuration

### StripeCheckoutConfig

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `checkoutUrl` | `string` | Yes | Backend endpoint URL for creating checkout sessions |
| `getUserData` | `() => Promise<UserData>` | Yes | Function to get user ID and email |
| `getAuthToken` | `() => Promise<string \| null>` | No | Function to get authentication token |
| `successUrl` | `string` | No | Success redirect URL (backend may handle this) |
| `cancelUrl` | `string` | No | Cancel redirect URL (backend may handle this) |
| `locale` | `'es' \| 'en' \| 'gl' \| 'auto'` | No | Locale for Stripe Checkout (default: 'es') |
| `theme` | `CheckoutTheme` | No | Custom theme configuration |
| `timeout` | `number` | No | Request timeout in ms (default: 30000) |
| `additionalHeaders` | `Record<string, string>` | No | Additional headers for checkout request |
| `requestFieldMapping` | `RequestFieldMapping` | No | Customize request field names |
| `responseFieldMapping` | `ResponseFieldMapping` | No | Customize response field names |
| `transformRequest` | `(data: RequestData) => Record<string, any>` | No | Transform request before sending |
| `transformResponse` | `(data: any) => ResponseData` | No | Transform response after receiving |

### RequestFieldMapping

Customize the field names sent to your backend:

```typescript
{
  planId?: string      // default: 'plan_id'
  userId?: string      // default: 'user_id'
  email?: string       // default: 'email'
}
```

### ResponseFieldMapping

Customize the field names received from your backend:

```typescript
{
  checkoutUrl?: string  // default: 'checkout_url'
  sessionId?: string    // default: 'session_id'
}
```

### StripeCheckoutWebViewProps

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `config` | `StripeCheckoutConfig` | Yes | Checkout configuration |
| `planId` | `string` | Yes | Plan ID to subscribe to |
| `visible` | `boolean` | Yes | Whether the checkout modal is visible |
| `onSuccess` | `(sessionId: string) => void` | Yes | Callback when checkout succeeds |
| `onCancel` | `() => void` | Yes | Callback when checkout is cancelled |
| `onError` | `(error: Error) => void` | Yes | Callback when an error occurs |
| `onLoadStart` | `() => void` | No | Callback when checkout starts loading |
| `onLoadEnd` | `() => void` | No | Callback when checkout finishes loading |
| `loadingComponent` | `ReactNode` | No | Custom loading component |
| `errorComponent` | `ReactNode` | No | Custom error component |

## 🔌 Backend Implementation

Your backend must implement a POST endpoint that creates a Stripe Checkout session. Here's an example:

### Node.js + Express

```javascript
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

app.post('/api/v1/payment/checkout', async (req, res) => {
  try {
    const { plan_id, user_id, email } = req.body;

    // Create Stripe checkout session
    const session = await stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      customer_email: email,
      line_items: [{
        price: plan_id, // Stripe Price ID
        quantity: 1,
      }],
      mode: 'subscription', // or 'payment' for one-time
      success_url: `${process.env.FRONTEND_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.FRONTEND_URL}/cancel`,
      metadata: {
        user_id: user_id,
        plan_id: plan_id,
      },
    });

    res.json({
      checkout_url: session.url,
      session_id: session.id,
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});
```

### Go + Gin

```go
func CreateCheckoutSession(c *gin.Context) {
    var req CheckoutSessionRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }

    params := &stripe.CheckoutSessionParams{
        PaymentMethodTypes: stripe.StringSlice([]string{"card"}),
        CustomerEmail:      stripe.String(req.Email),
        LineItems: []*stripe.CheckoutSessionLineItemParams{{
            PriceData: &stripe.CheckoutSessionLineItemPriceDataParams{
                Currency: stripe.String("eur"),
                ProductData: &stripe.CheckoutSessionLineItemPriceDataProductDataParams{
                    Name: stripe.String("Premium Plan"),
                },
                UnitAmount: stripe.Int64(4000), // 40€
                Recurring: &stripe.CheckoutSessionLineItemPriceDataRecurringParams{
                    Interval: stripe.String("month"),
                },
            },
            Quantity: stripe.Int64(1),
        }},
        Mode:       stripe.String("subscription"),
        SuccessURL: stripe.String(fmt.Sprintf("%s/success?session_id={CHECKOUT_SESSION_ID}", frontendURL)),
        CancelURL:  stripe.String(fmt.Sprintf("%s/cancel", frontendURL)),
    }

    session, err := session.New(params)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }

    c.JSON(200, gin.H{
        "checkout_url": session.URL,
        "session_id":   session.ID,
    })
}
```

## 📱 Platform-Specific Notes

### iOS Deep Links

For iOS, you need to configure a custom URL scheme for return URLs:

1. Register your custom URL scheme in Xcode
2. Configure deep linking in your app
3. Set up URL handling in your root component:

```tsx
import { useEffect, useCallback } from 'react';
import { Linking } from 'react-native';
import { useStripe } from '@stripe/stripe-react-native';

export default function App() {
  const { handleURLCallback } = useStripe();

  const handleDeepLink = useCallback(async (url: string | null) => {
    if (url) {
      await handleURLCallback(url);
    }
  }, [handleURLCallback]);

  useEffect(() => {
    const getInitialUrl = async () => {
      const initialUrl = await Linking.getInitialURL();
      handleDeepLink(initialUrl);
    };

    getInitialUrl();

    const subscription = Linking.addEventListener('url', (event) => {
      handleDeepLink(event.url);
    });

    return () => subscription.remove();
  }, [handleDeepLink]);

  return <YourApp />;
}
```

### Expo Configuration

For Expo apps, set your scheme in `app.json`:

```json
{
  "expo": {
    "scheme": "yourapp"
  }
}
```

## 🎨 Customization

### Custom Theme

```tsx
const config = {
  // ... other config
  theme: {
    primaryColor: '#635BFF',
    backgroundColor: '#FFFFFF',
  },
};
```

### Custom Loading Component

```tsx
<StripeCheckoutWebView
  config={config}
  planId="premium-monthly"
  visible={visible}
  loadingComponent={
    <View style={styles.loading}>
      <ActivityIndicator size="large" color="#635BFF" />
      <Text>Procesando pago...</Text>
    </View>
  }
  onSuccess={handleSuccess}
  onCancel={handleCancel}
  onError={handleError}
/>
```

## 🔍 Advanced Usage

### Custom Field Mapping

If your backend uses different field names (e.g., camelCase instead of snake_case):

```tsx
const config = {
  checkoutUrl: 'https://api.example.com/checkout',
  getUserData: async () => ({ id: 'user123', email: 'user@example.com' }),
  
  // Custom request field names
  requestFieldMapping: {
    planId: 'planID',        // backend expects 'planID' instead of 'plan_id'
    userId: 'userID',        // backend expects 'userID' instead of 'user_id'
    email: 'emailAddress',   // backend expects 'emailAddress' instead of 'email'
  },
  
  // Custom response field names
  responseFieldMapping: {
    checkoutUrl: 'url',      // backend returns 'url' instead of 'checkout_url'
    sessionId: 'id',         // backend returns 'id' instead of 'session_id'
  },
};
```

### Custom Request/Response Transformation

For more complex scenarios, use transformers:

```tsx
const config = {
  checkoutUrl: 'https://api.example.com/checkout',
  getUserData: async () => ({ id: 'user123', email: 'user@example.com' }),
  
  // Transform request: add extra fields or restructure payload
  transformRequest: (data) => ({
    subscription: {
      plan: data.planId,
      user: {
        id: data.userId,
        contact: data.email,
      },
    },
    metadata: {
      source: 'mobile_app',
      timestamp: Date.now(),
    },
  }),
  
  // Transform response: extract from nested structure
  transformResponse: (data) => ({
    checkoutUrl: data.payment.redirect_url,
    sessionId: data.payment.session.id,
  }),
};
```

### Backend with Different Structure (Example: Nest.js)

```tsx
// Your Nest.js backend returns:
// { data: { paymentUrl: '...', sessionToken: '...' } }

const config = {
  checkoutUrl: 'https://api.example.com/payments/create-session',
  getUserData: async () => ({ id: 'user123', email: 'user@example.com' }),
  
  transformResponse: (response) => ({
    checkoutUrl: response.data.paymentUrl,
    sessionId: response.data.sessionToken,
  }),
};
```

### Backend with GraphQL

```tsx
const config = {
  checkoutUrl: 'https://api.example.com/graphql',
  getUserData: async () => ({ id: 'user123', email: 'user@example.com' }),
  
  transformRequest: (data) => ({
    query: `
      mutation CreateCheckout($planId: ID!, $userId: ID!, $email: String!) {
        createCheckout(planId: $planId, userId: $userId, email: $email) {
          checkoutUrl
          sessionId
        }
      }
    `,
    variables: {
      planId: data.planId,
      userId: data.userId,
      email: data.email,
    },
  }),
  
  transformResponse: (response) => ({
    checkoutUrl: response.data.createCheckout.checkoutUrl,
    sessionId: response.data.createCheckout.sessionId,
  }),
};
```

### Error Handling

```tsx
const handleError = (error: Error) => {
  if (error.message.includes('timeout')) {
    Alert.alert('Error', 'La conexión tardó demasiado. Por favor, inténtalo de nuevo.');
  } else if (error.message.includes('User data')) {
    Alert.alert('Error', 'Datos de usuario incompletos');
  } else {
    Alert.alert('Error', 'Error al procesar el pago: ' + error.message);
  }
};
```

### Multiple Plans

```tsx
const plans = [
  { id: 'premium-monthly', name: 'Mensual', price: '40€/mes' },
  { id: 'premium-yearly', name: 'Anual', price: '450€/año' },
];

function PricingScreen() {
  const [selectedPlan, setSelectedPlan] = useState(null);
  const [checkoutVisible, setCheckoutVisible] = useState(false);

  return (
    <>
      {plans.map((plan) => (
        <Button
          key={plan.id}
          title={`${plan.name} - ${plan.price}`}
          onPress={() => {
            setSelectedPlan(plan.id);
            setCheckoutVisible(true);
          }}
        />
      ))}

      {selectedPlan && (
        <StripeCheckoutWebView
          config={config}
          planId={selectedPlan}
          visible={checkoutVisible}
          onSuccess={handleSuccess}
          onCancel={() => setCheckoutVisible(false)}
          onError={handleError}
        />
      )}
    </>
  );
}
```

## 🧪 Testing

### Library Tests

This library has **95%+ test coverage**. Run tests with:

```bash
# Run all tests
npm test

# Run with coverage
npm run test:coverage

# Watch mode
npm run test:watch
```

**Test Coverage:**
- ✅ URL Parser: 100%
- ✅ Checkout Service: 98%
- ✅ Component: 92%
- ✅ Hook: 95%
- ✅ **Overall: 95%+**

See [TESTING.md](./TESTING.md) for detailed testing guide.

### Stripe Test Cards

Use Stripe's test cards in development:

| Card Number | Scenario |
|-------------|----------|
| 4242 4242 4242 4242 | Successful payment |
| 4000 0025 0000 3155 | Requires authentication (3D Secure) |
| 4000 0000 0000 9995 | Payment declined (insufficient funds) |

Use any future expiration date, any 3-digit CVC, and any postal code.

## 📦 What's Included

```
@fredyk/react-native-stripe-checkout-webview/
├── src/
│   ├── components/
│   │   └── StripeCheckoutWebView.tsx    # Main component
│   ├── hooks/
│   │   └── useStripeCheckout.ts          # React hook
│   ├── services/
│   │   └── checkoutService.ts            # API service
│   ├── types/
│   │   └── index.ts                      # TypeScript definitions
│   ├── utils/
│   │   └── urlParser.ts                  # URL parsing utilities
│   └── index.ts                          # Public exports
├── package.json
├── tsconfig.json
└── README.md
```

### Advanced Exports

For advanced use cases and testing, the library also exports:

```tsx
import { 
  createTimeoutHandler,  // Helper for timeout management
  parseErrorResponse,    // Helper for error response parsing
} from '@fredyk/react-native-stripe-checkout-webview';
```

These helper functions follow the **Single Responsibility Principle** and improve testability.

## 🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request

## 📄 License

MIT License - See LICENSE file for details

## 🙏 Acknowledgments

- Inspired by [react-native-stripe-checkout-webview](https://github.com/a-tokyo/react-native-stripe-checkout-webview)
- Built with knowledge from Stripe's official documentation
- Informed by real-world implementations in production apps

## 🧪 Testing & Quality

This library maintains **100% code coverage** across all metrics:
- ✅ **Statements**: 100%
- ✅ **Branches**: 100%
- ✅ **Functions**: 100%
- ✅ **Lines**: 100%

**156 tests** covering all components, hooks, services, and edge cases to ensure production-ready reliability.

## 📞 Support

- 🐛 [Report a bug](https://github.com/fredyk/react-native-stripe-checkout-webview/issues)
- 💡 [Request a feature](https://github.com/fredyk/react-native-stripe-checkout-webview/issues)
- 📖 [Documentation](https://github.com/fredyk/react-native-stripe-checkout-webview)
- 📦 [NPM Package](https://www.npmjs.com/package/@fredyk/react-native-stripe-checkout-webview)

---

**Made with ❤️ for the React Native community**