npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

v1.0.0-rc07

Published

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

Downloads

11

Readme

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

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

npm version TypeScript Coverage License: 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

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:

npm install react-native-webview

For Expo apps:

npx expo install react-native-webview

Basic Usage

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

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:

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

ResponseFieldMapping

Customize the field names received from your backend:

{
  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

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

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:
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:

{
  "expo": {
    "scheme": "yourapp"
  }
}

🎨 Customization

Custom Theme

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

Custom Loading Component

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

const config = {
  checkoutUrl: 'https://api.example.com/checkout',
  getUserData: async () => ({ id: 'user123', email: '[email protected]' }),
  
  // 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:

const config = {
  checkoutUrl: 'https://api.example.com/checkout',
  getUserData: async () => ({ id: 'user123', email: '[email protected]' }),
  
  // 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)

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

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

Backend with GraphQL

const config = {
  checkoutUrl: 'https://api.example.com/graphql',
  getUserData: async () => ({ id: 'user123', email: '[email protected]' }),
  
  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

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

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:

# 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 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:

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

🧪 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


Made with ❤️ for the React Native community