@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
Maintainers
Readme
@fredyk/react-native-stripe-checkout-webview
💰 A fully configurable React Native library for Stripe Checkout integration via WebView
🌟 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-webviewrequired - 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-webviewPrerequisites:
npm install react-native-webviewFor Expo apps:
npx expo install react-native-webviewBasic 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:
- Register your custom URL scheme in Xcode
- Configure deep linking in your app
- 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:watchTest 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.mdAdvanced 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.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
📄 License
MIT License - See LICENSE file for details
🙏 Acknowledgments
- Inspired by 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
Made with ❤️ for the React Native community
