Created
May 30, 2025 18:59
-
-
Save Josiassejod1/70d2f10a5c348dd160972c17fa172aec to your computer and use it in GitHub Desktop.
Working Example of Stripe Subscription React Native
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState, useEffect } from 'react'; | |
import { StyleSheet, View, TouchableOpacity, ActivityIndicator, Alert, Linking } from 'react-native'; | |
import { ThemedView } from '@/components/ThemedView'; | |
import { ThemedText } from '@/components/ThemedText'; | |
import { useSubscription } from '@/context/SubscriptionContext'; | |
import { useStripe } from '@stripe/stripe-react-native'; | |
import { getAuth } from 'firebase/auth'; | |
import { STRIPE_CONFIG } from '@/config/stripe'; | |
import Stripe from 'stripe'; | |
export default function SubscriptionPlans() { | |
const { isSubscribed, subscriptionTier, loading, error, handleSubscribe, handleCancelSubscription } = useSubscription(); | |
const { initPaymentSheet, presentPaymentSheet } = useStripe(); | |
const [processing, setProcessing] = useState(false); | |
const [subscriptionInfo, setSubscriptionInfo] = useState<any>(null); | |
const [subscriptionLoading, setSubscriptionLoading] = useState(false); | |
const auth = getAuth(); | |
const VERCEL_URL = __DEV__ ? 'http://localhost:3000' : 'https://example.com'; | |
useEffect(() => { | |
const handleDeepLink = (event: { url: any; }) => { | |
const url = event.url; | |
if (url && url.startsWith('example://stripe-redirect')) { | |
// Parse the result, or just react to success | |
// You may want to confirm the PaymentIntent status on your backend | |
Alert.alert('Payment Complete', 'You were redirected back after payment.'); | |
} | |
}; | |
const subscription = Linking.addEventListener('url', handleDeepLink); | |
// For the case where the app was cold-started from the link | |
Linking.getInitialURL().then((url) => { | |
if (url && url.startsWith('example')) { | |
handleDeepLink({ url }); | |
} | |
}); | |
return () => { | |
subscription.remove(); | |
}; | |
}, []); | |
// Fetch subscription info from backend | |
const fetchSubscriptionInfo = async () => { | |
setSubscriptionLoading(true); | |
try { | |
const user = auth.currentUser; | |
if (!user) return; | |
const response = await fetch(`${VERCEL_URL}/api/get-subscription`, { | |
method: 'GET', | |
headers: { | |
'Authorization': `Bearer ${await user.getIdToken()}` | |
} | |
}); | |
if (response.ok) { | |
const data = await response.json(); | |
setSubscriptionInfo(data); | |
} else { | |
setSubscriptionInfo(null); | |
} | |
} catch (err) { | |
setSubscriptionInfo(null); | |
} finally { | |
setSubscriptionLoading(false); | |
} | |
}; | |
useEffect(() => { | |
fetchSubscriptionInfo(); | |
}, [isSubscribed]); | |
const handleSubscribePress = async () => { | |
try { | |
setProcessing(true); | |
const user = auth.currentUser; | |
if (!user) { | |
Alert.alert('Error', 'You must be logged in to subscribe'); | |
return; | |
} | |
console.log(user.getIdToken()) | |
// Create payment intent on your backend | |
const response = await fetch(`${VERCEL_URL}/api/create-payment-intent`, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
'Authorization': `Bearer ${await user.getIdToken()}` | |
}, | |
body: JSON.stringify({ | |
priceId: STRIPE_CONFIG.plans.premium.id, | |
userId: user.uid, | |
email: user.email | |
}), | |
}); | |
const { clientSecret, subscriptionId } = await response.json(); | |
// Initialize the Payment sheet | |
const { error } = await initPaymentSheet({ | |
paymentIntentClientSecret: clientSecret, | |
merchantDisplayName: 'Jorge\'s Journey', | |
returnURL: 'jorgejourney://stripe-redirect', | |
}); | |
if (error) { | |
console.log(error,"error"); | |
throw error; | |
} | |
// Present the Payment Sheet | |
const { error: presentError } = await presentPaymentSheet(); | |
if (presentError) { | |
console.log(presentError,"presentError"); | |
throw presentError; | |
} | |
// If successful, update subscription status | |
await handleSubscribe(clientSecret, subscriptionId); | |
Alert.alert('Success', 'Welcome to Premium! 🎉'); | |
} catch (err) { | |
Alert.alert('Error', err instanceof Error ? err.message : 'Failed to process subscription'); | |
} finally { | |
setProcessing(false); | |
} | |
}; | |
const handleCancelPress = async () => { | |
try { | |
setProcessing(true); | |
const user = auth.currentUser; | |
if (!user) { | |
Alert.alert('Error', 'You must be logged in to cancel subscription'); | |
setProcessing(false); | |
return; | |
} | |
await handleCancelSubscription(); | |
Alert.alert('Success', 'Subscription cancellation request processed.'); | |
await fetchSubscriptionInfo(); | |
} catch (err: any) { | |
const errorMessage = err?.message || (typeof err === 'string' ? err : 'Failed to process cancellation.'); | |
Alert.alert('Error', errorMessage); | |
} finally { | |
setProcessing(false); | |
} | |
}; | |
if (loading || subscriptionLoading) { | |
return ( | |
<ThemedView style={styles.container}> | |
<ActivityIndicator size="large" color="#FFD700" /> | |
</ThemedView> | |
); | |
} | |
if (error) { | |
return ( | |
<ThemedView style={styles.container}> | |
<ThemedText style={styles.errorText}>{error}</ThemedText> | |
</ThemedView> | |
); | |
} | |
return ( | |
<ThemedView style={styles.container}> | |
<ThemedText style={styles.title}>Choose Your Plan</ThemedText> | |
<View style={styles.planContainer}> | |
<View style={styles.planCard}> | |
<ThemedText style={styles.planTitle}>Free</ThemedText> | |
<ThemedText style={styles.planPrice}>$0</ThemedText> | |
<ThemedText style={styles.planDescription}> | |
• Basic lessons{'\n'} | |
• Limited practice exercises{'\n'} | |
• Community access | |
</ThemedText> | |
{subscriptionTier === 'free' && ( | |
<ThemedText style={styles.currentPlan}>Current Plan</ThemedText> | |
)} | |
</View> | |
<View style={[styles.planCard, styles.premiumCard]}> | |
<ThemedText style={styles.planTitle}>Premium</ThemedText> | |
<ThemedText style={styles.planPrice}>$4.99/month</ThemedText> | |
<ThemedText style={styles.planDescription}> | |
• All Free features{'\n'} | |
• Unlimited practice{'\n'} | |
• Premium lessons{'\n'} | |
• Offline access{'\n'} | |
• Priority support | |
</ThemedText> | |
{isSubscribed ? ( | |
<TouchableOpacity | |
style={[styles.button, styles.cancelButton]} | |
onPress={handleCancelPress} | |
disabled={processing} | |
> | |
<ThemedText style={styles.buttonText}> | |
{processing ? 'Processing...' : 'Cancel Subscription'} | |
</ThemedText> | |
</TouchableOpacity> | |
) : ( | |
<TouchableOpacity | |
style={[styles.button, styles.subscribeButton]} | |
onPress={handleSubscribePress} | |
disabled={processing} | |
> | |
<ThemedText style={styles.buttonText}> | |
{processing ? 'Processing...' : 'Subscribe Now'} | |
</ThemedText> | |
</TouchableOpacity> | |
)} | |
</View> | |
</View> | |
</ThemedView> | |
); | |
} | |
const styles = StyleSheet.create({ | |
container: { | |
flex: 1, | |
padding: 20, | |
alignItems: 'center', | |
justifyContent: 'center', | |
}, | |
title: { | |
fontSize: 32, | |
fontWeight: 'bold', | |
marginBottom: 40, | |
textAlign: 'center', | |
}, | |
planContainer: { | |
width: '100%', | |
maxWidth: 600, | |
gap: 20, | |
}, | |
planCard: { | |
padding: 20, | |
borderRadius: 15, | |
backgroundColor: '#FFF', | |
shadowColor: '#000', | |
shadowOffset: { width: 0, height: 2 }, | |
shadowOpacity: 0.1, | |
shadowRadius: 4, | |
elevation: 3, | |
}, | |
premiumCard: { | |
backgroundColor: '#FFF5E1', | |
borderWidth: 2, | |
borderColor: '#FFD700', | |
}, | |
planTitle: { | |
fontSize: 24, | |
fontWeight: 'bold', | |
marginBottom: 10, | |
}, | |
planPrice: { | |
fontSize: 36, | |
fontWeight: 'bold', | |
color: '#E65100', | |
marginBottom: 20, | |
}, | |
planDescription: { | |
fontSize: 16, | |
lineHeight: 24, | |
marginBottom: 20, | |
}, | |
button: { | |
padding: 15, | |
borderRadius: 10, | |
alignItems: 'center', | |
}, | |
subscribeButton: { | |
backgroundColor: '#FFD700', | |
}, | |
cancelButton: { | |
backgroundColor: '#FF6B6B', | |
}, | |
buttonText: { | |
fontSize: 18, | |
fontWeight: 'bold', | |
color: '#333', | |
}, | |
currentPlan: { | |
fontSize: 16, | |
color: '#666', | |
textAlign: 'center', | |
marginTop: 10, | |
}, | |
errorText: { | |
color: '#FF6B6B', | |
fontSize: 16, | |
textAlign: 'center', | |
}, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment