
Here's the step-by-step process that actually works in production, including the shit that will break and how to fix it when (not if) it happens.
Phase 1: Firebase Setup (This Will Take 3 Hours Minimum)
Set Up Firebase Auth (And Watch It Break)
Go to the Firebase Console and enable Email/Password authentication. Don't bother with anonymous auth - it breaks subscription continuity when users upgrade accounts. Check out the Firebase Auth best practices and security guidelines while you're at it.
The Firestore database needs these security rules, but they're more fragile than they look:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /customers/{uid} {
// This breaks when users sign out mid-payment
allow read, write: if request.auth != null && request.auth.uid == uid;
}
match /customers/{uid}/checkout_sessions/{id} {
// Add this or webhook updates fail silently
allow read, write: if request.auth != null &&
(request.auth.uid == uid || resource == null);
}
match /customers/{uid}/subscriptions/{id} {
// Read-only because Stripe manages these via webhooks
allow read: if request.auth != null && request.auth.uid == uid;
}
}
}
Install the Firebase Extension (Prepare for Debugging Hell)
The Stripe Firebase Extension works great when it works. When it doesn't, good luck debugging Cloud Functions you can't see. Check the extension documentation and troubleshooting guide before you start.
Go to Extensions → Browse → Install "Run Payments with Stripe". You'll need:
- Your Stripe restricted API key with these exact permissions:
- Customers: Read, Write
- Checkout Sessions: Read, Write
- Customer Portal: Read, Write
- Subscriptions: Read, Write
- Prices: Read, Write
DO NOT use your full access key - the extension will work in development but fail security reviews. I learned this when our security audit flagged it as a critical vulnerability and I had to explain to legal why we were storing unrestricted API keys. That was a fun Monday morning meeting. Read the API key security guide and best practices before you make the same mistake.
The extension creates these Cloud Functions that will randomly timeout:
ext-firestore-stripe-payments-createCustomer
ext-firestore-stripe-payments-createCheckoutSession
ext-firestore-stripe-payments-handleWebhookEvents
Set memory to 512MB minimum or they'll timeout under load. I spent 3 hours one Thursday night debugging payment failures that turned out to be memory limits - the function was just running out of RAM during checkout. Took me way too long to figure out because Firebase doesn't tell you it's a memory issue, just "function execution failed". Check the Cloud Functions performance guide so you don't waste your evening like I did.
Phase 2: React Native Setup (Where Everything Breaks)

Install Dependencies (And Pray They Work Together)
## Install these versions - seriously, don't get creative
npm install @stripe/stripe-react-native@0.34.0
npm install @react-native-firebase/app@18.5.0
npm install @react-native-firebase/auth@18.5.0
npm install @react-native-firebase/firestore@18.5.0
## iOS setup (probably breaks on Xcode 15+)
cd ios && pod install
If pod install
fails with dependency conflicts, delete Podfile.lock
and your ios/build
folder, then run it again. You'll do this dance at least 3 times.
Android Build Issues (Because Of Course)
Your Android build will fail with "ECONNREFUSED" errors. Add this to android/app/src/debug/res/xml/network_security_config.xml
:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">10.0.3.2</domain>
</domain-config>
</network-security-config>
Firebase Initialization (The Part That Randomly Fails)
Follow the setup guide, but ignore the part about automatic linking - it breaks half the time. Manual linking is more work but actually functions.
Your google-services.json
and GoogleService-Info.plist
files need to be in the exact right locations or Firebase will fail silently. Check the file paths 3 times.
Stripe Provider Setup (Simple But Fragile)
import { StripeProvider } from '@stripe/stripe-react-native';
export default function App() {
return (
<StripeProvider
publishableKey="pk_test_your_key_here"
// Add these or PaymentSheet breaks on Android
threeDSParams={{
timeout: 5,
uiCustomization: {}
}}
>
<YourAppContent />
</StripeProvider>
);
}
DO NOT put your live publishable key in the code. Use environment variables or React Native Config. I've seen too many apps accidentally process real payments in development.
Phase 3: Auth Integration (Where Users Get Stuck in Loops)
Customer Creation That Actually Works
The Firebase Extension automatically creates Stripe customers when users authenticate - except when it doesn't. Here's what actually happens:
import auth from '@react-native-firebase/auth';
import firestore from '@react-native-firebase/firestore';
useEffect(() => {
// This listener fires like 3 or 4 times on startup for some reason
const unsubscribe = auth().onAuthStateChanged(async (user) => {
if (user) {
try {
// Check if customer already exists to prevent duplicates
const customerDoc = await firestore()
.collection('customers')
.doc(user.uid)
.get();
if (!customerDoc.exists) {
// Trigger customer creation - this fails silently sometimes
await firestore()
.collection('customers')
.doc(user.uid)
.set({
email: user.email,
// Add this or the extension breaks
created: firestore.FieldValue.serverTimestamp()
});
}
} catch (error) {
// This error happens when Firestore security rules are wrong
console.log('Customer creation failed:', error);
}
}
});
return unsubscribe;
}, []);
The Debouncing You'll Need
Without debouncing, the auth state change fires multiple times and your UI flickers:
import { debounce } from 'lodash';
const debouncedAuthHandler = debounce(async (user) => {
if (user) {
await initializeCustomerData(user.uid);
}
}, 500);
useEffect(() => {
const unsubscribe = auth().onAuthStateChanged(debouncedAuthHandler);
return unsubscribe;
}, []);
Phase 4: Payment Processing (The Part That Randomly Fails)

PaymentSheet Setup (It Will Break on Android)
import { useStripe } from '@stripe/stripe-react-native';
const { presentPaymentSheet } = useStripe();
const handlePayment = async () => {
try {
// Create checkout session in Firestore
const checkoutSessionRef = await firestore()
.collection('customers')
.doc(user.uid)
.collection('checkout_sessions')
.add({
price: 'price_your_price_id',
// Add success_url or PaymentSheet breaks
success_url: 'https://yourapp.com/success',
cancel_url: 'https://yourapp.com/cancel'
});
// Wait for the Cloud Function to process it
// This takes like 2-5 seconds because of cold starts
const unsubscribe = checkoutSessionRef.onSnapshot((snap) => {
const { sessionId } = snap.data() || {};
if (sessionId) {
unsubscribe();
presentPaymentSheet({
clientSecret: sessionId,
// Add these or Android crashes
googlePay: { merchantCountryCode: 'US' },
applePay: { merchantCountryCode: 'US' }
});
}
});
} catch (error) {
// Common errors:
// - "Price not found" = wrong price ID
// - "Customer not found" = auth token expired
// - "Payment failed" = everything else
console.log('Payment failed:', error);
}
};
Webhook Debugging (When Payments Get Lost)
Stripe webhooks randomly fail with 500 errors when Cloud Functions timeout. Use the Stripe CLI to debug:
## Forward webhooks to your local dev server
stripe listen --forward-to localhost:3000/webhook
## Test webhook processing
stripe trigger payment_intent.succeeded
If webhooks keep failing, increase Cloud Function memory and add retry logic:
// In your Cloud Function (if you're writing custom ones)
exports.handleWebhook = functions
.runWith({ memory: '512MB', timeoutSeconds: 60 })
.https.onRequest(async (req, res) => {
// Your webhook handling code here
});
Phase 5: The Stuff That Breaks in Production
Subscription Access Control (Custom Claims Hell)
Firebase custom claims are great until they take 5 minutes to propagate. Users pay for subscriptions but can't access premium features:
// Check subscription status with fallback
const checkSubscriptionAccess = async () => {
try {
// Check custom claims first (cached)
const idTokenResult = await auth().currentUser.getIdTokenResult(true);
const subscriptionActive = idTokenResult.claims.subscription_status === 'active';
if (!subscriptionActive) {
// Fallback: Check Firestore directly
const subscription = await firestore()
.collection('customers')
.doc(user.uid)
.collection('subscriptions')
.where('status', '==', 'active')
.get();
return !subscription.empty;
}
return subscriptionActive;
} catch (error) {
// Always fail open for better UX
return false;
}
};
Customer Portal Integration (WebView Nightmare)
The Customer Portal opens in WebView but redirects don't work:
import { Linking } from 'react-native';
import InAppBrowser from 'react-native-inappbrowser-reborn';
const openCustomerPortal = async () => {
try {
const portalRef = await firestore()
.collection('customers')
.doc(user.uid)
.collection('checkout_sessions')
.add({
// Trigger customer portal creation
mode: 'customer_portal',
return_url: 'yourapp://billing'
});
// Use InAppBrowser instead of WebView
await InAppBrowser.open(portal.url, {
dismissButtonStyle: 'cancel',
readerMode: false,
modalPresentationStyle: 'fullScreen'
});
} catch (error) {
console.log('Portal failed:', error);
}
};
Production Monitoring (Or You'll Be Blind)
Set up actual monitoring because Stripe's dashboard doesn't show you when Firebase Extensions break:
- Monitor Cloud Function errors in Firebase Console
- Track payment success rates in Stripe Dashboard
- Set up alerts for webhook failures
- Log authentication issues to crash reporting
The integration works great when everything goes right. When it breaks, you'll need these debugging tools to figure out why payments disappear into the void.