I have a React app integrated with Stripe payments, where users can view, add, edit, and remove their payment methods under the Payments tab in settings. To enable this, I call a Firebase Cloud Function named createSetupIntent using the httpsCallable method from the Firebase SDK. However, when I click the button to create a setup intent, I encounter the following CORS error in my browser console:
Access to fetch at 'https://us-central1-lurk-.cloudfunctions.net/createSetupIntent' from origin 'http://localhost:5173' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
and the request fails with:
POST https://us-central1-lurk-<id>.cloudfunctions.net/createSetupIntent net::ERR_FAILED
I suspect that somewhere in my code, a direct fetch (or similar) is still being made, bypassing the intended httpsCallable pipeline. I want to ensure that all calls to createSetupIntent use httpsCallable without any leftover direct fetch calls or misconfigurations causing CORS issues. I have made sure to:
- Replace any custom fetch or postJSON calls with httpsCallable in PaymentMethodManager.jsx.
- Export my Firebase Function as functions.https.onCall, which should handle CORS automatically with httpsCallable.
- Redeploy my functions and verify that my imports are correct:
import { httpsCallable } from 'firebase/functions';
const createSetupIntentFn = httpsCallable(functions, 'createSetupIntent');
Below is the relevant part of my PaymentMethodManager.jsx:
import React, { useState } from 'react';
import { functions } from '../firebase';
import { httpsCallable } from 'firebase/functions';
import { useToast } from '@chakra-ui/react';
export const PaymentMethodManager = () => {
const [showAddCard, setShowAddCard] = useState(false);
const toast = useToast();
const handleAddPaymentMethod = async () => {
try {
// Attempt to create a setup intent via httpsCallable
const createSetupIntentFn = httpsCallable(functions, 'createSetupIntent');
const { data } = await createSetupIntentFn();
if (!data || !data.clientSecret) {
throw new Error('Missing client secret from createSetupIntent response');
}
// Use data.clientSecret with Stripe.js to confirm a card setup
console.log('Setup Intent created:', data.clientSecret);
} catch (error) {
console.error('Error creating setup intent:', error);
toast({
title: 'Error',
description: error.message,
status: 'error',
duration: 3000,
});
}
};
return (
<div>
<button onClick={handleAddPaymentMethod}>
Add Payment Method
</button>
{showAddCard && <StripeCardForm />}
</div>
);
};
And here is the createSetupIntent function from my Stripe.js:
exports.createSetupIntent = functions.https.onCall(async (data, context) => {
if (!context.auth) {
throw new functions.https.HttpsError('unauthenticated', 'Must be logged in');
}
try {
console.log('Creating setup intent for user:', context.auth.uid);
// Get user's Stripe customer ID from Firestore
const userDoc = await admin.firestore().collection('userInfo').doc(context.auth.uid).get();
const userData = userDoc.exists ? userDoc.data() : {};
let customerId = userData.stripeCustomerId;
// If no customer ID exists, create a new customer
if (!customerId) {
console.log('No customer ID found, creating new customer');
const customer = await stripe.customers.create({
email: context.auth.token.email,
metadata: {
firebaseUID: context.auth.uid
}
});
customerId = customer.id;
console.log('Created new customer:', customerId);
// Save the customer ID to Firestore
await admin.firestore().collection('userInfo').doc(context.auth.uid).set({
stripeCustomerId: customerId,
email: context.auth.token.email,
updatedAt: admin.firestore.FieldValue.serverTimestamp()
}, { merge: true });
} else {
console.log('Found existing customer:', customerId);
}
// Create a setup intent for the customer
const setupIntent = await stripe.setupIntents.create({
customer: customerId,
payment_method_types: ['card'],
usage: 'off_session',
metadata: {
firebaseUID: context.auth.uid,
customerId: customerId
}
});
console.log('Created setup intent:', setupIntent.id);
return {
clientSecret: setupIntent.client_secret,
customerId: customerId
};
} catch (error) {
console.error('Error in createSetupIntent:', error);
throw new functions.https.HttpsError('internal', error.message);
}
});
I also reviewed the getPaymentMethods function to ensure proper CORS handling in express-style calls (if needed), but for onCall functions, additional CORS middleware isn’t necessary. Despite using httpsCallable, the network tab still shows a direct POST request to …cloudfunctions.net/createSetupIntent that fails its preflight OPTIONS request due to the missing Access-Control-Allow-Origin header.
I’m looking for guidance on how to identify and remove any stray direct calls or reconfigure my Firebase Functions setup so that the httpsCallable method handles the call properly and eliminates the CORS error.
Any advice to pinpoint the issue or correct my configuration would be greatly appreciated.