Thank you @Vu_Dung_Pham! I managed to changed my api version but the problem still persist.
I deployed to google cloud run using the following varibles and code:
STRIPE_SECRET_KEY (Live Stripe Secret Key)
STRIPE_WEBHOOK_SECRET (Live Stripe Webhook Signing Secret)
NEXT_PUBLIC_APP_URL (Your deployed app's URL: https://newsassist.journotech.org)
GEMINI_API_KEY
But it automatcially has the FIREBASE_CONFIG variables which is number 1
Here’s my code use:
/ src/app/api/stripe-webhooks/route.ts
import { NextResponse, type NextRequest } from 'next/server';
import { headers } from 'next/headers';
import Stripe from 'stripe';
import { handleStripeWebhook } from '@/app/actions/stripeWebhookHandler';
// This is the correct way to disable the default body parser in App Router.
// It allows us to get the raw body for Stripe's signature verification.
export const config = {
api: {
bodyParser: false,
},
};
// Re-initialize Stripe here as this is a separate serverless function environment,
// ensuring the correct API version is used.
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-06-20',
});
const stripeWebhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
export async function POST(request: NextRequest) {
// We need the raw body as a string for constructEvent.
const body = await request.text();
const signature = headers().get('stripe-signature');
if (!stripeWebhookSecret) {
console.error('[Webhook] CRITICAL: Stripe webhook secret (STRIPE_WEBHOOK_SECRET) is not set.');
return NextResponse.json({ error: 'Webhook secret not configured.' }, { status: 500 });
}
if (!body || !signature) {
console.error("[Webhook] Missing request body or signature.");
return NextResponse.json({ error: 'Missing request body or signature.' }, { status: 400 });
}
let event: Stripe.Event;
try {
// Use the raw text body to construct the event. This is the critical step.
event = stripe.webhooks.constructEvent(body, signature, stripeWebhookSecret);
} catch (err: any) {
console.error(`[Webhook] SIGNATURE VERIFICATION FAILED. This is a critical error.`);
console.error(`[Webhook] Error Message: ${err.message}`);
return NextResponse.json({ error: `Webhook signature verification failed: ${err.message}` }, { status: 400 });
}
// Pass the verified event to the handler function that contains the business logic
// (e.g., updating the Firestore database).
const result = await handleStripeWebhook(event);
if (result.error) {
return NextResponse.json({ received: true, error: result.error }, { status: result.status });
}
return NextResponse.json({ received: true, message: result.message }, { status: result.status });
}
See my the other code handler
// src/app/actions/stripeWebhookHandler.ts
'use server';
import Stripe from 'stripe';
import { adminDb, adminAuth } from '@/firebase/firebaseAdmin';
import { PLANS, getPlanById, getResetQuota } from '@/config/plans';
import type { PlanId, UserProfile } from '@/lib/types';
import * as admin from 'firebase-admin';
/**
* Main handler for processing verified Stripe webhook events.
* Accepts a Stripe.Event object that has already been verified.
*/
export async function handleStripeWebhook(
event: Stripe.Event
): Promise<{ error?: string; status: number; message?: string }> {
try {
switch (event.type) {
case "checkout.session.completed":
const session = event.data.object as Stripe.Checkout.Session;
console.log(`[Webhook] Handling checkout.session.completed for session: ${session.id}`);
if (session.payment_status !== "paid") {
console.log(`[Webhook] Session ${session.id} not paid (Status: ${session.payment_status}). No action taken.`);
return { status: 200, message: "Session not paid, no action." };
}
const planIdFromMeta = session.metadata?.planId as PlanId | undefined;
const stripeCustomerId = typeof session.customer === "string" ? session.customer : session.customer?.id;
const stripeSubscriptionId = typeof session.subscription === "string" ? session.subscription : session.subscription?.id;
const stripePriceId = session.metadata?.priceId;
let firebaseUIDFromMeta = session.metadata?.firebaseUID;
const customerEmail = session.customer_details?.email;
if (!stripeCustomerId) {
console.error(`[Webhook] CRITICAL: Missing Stripe Customer ID from checkout session: ${session.id}.`);
return { status: 200, message: 'Acknowledged: Missing Stripe Customer ID. Logged for review.' };
}
if (!planIdFromMeta) {
console.error(`[Webhook] CRITICAL: Missing planId from checkout session metadata: ${session.id}.`);
return { status: 200, message: 'Acknowledged: Missing planId in metadata. Logged for review.' };
}
if (planIdFromMeta === 'donation') {
console.log(`[Webhook] Donation checkout session ${session.id} completed. No user plan update needed.`);
return { status: 200, message: "Donation processed." };
}
let userRecord: admin.auth.UserRecord | null = null;
if (firebaseUIDFromMeta) {
try {
userRecord = await adminAuth.getUser(firebaseUIDFromMeta);
console.log(`[Webhook] Found user by UID from metadata: ${firebaseUIDFromMeta}`);
} catch (e: any) {
console.warn(`[Webhook] Firebase UID ${firebaseUIDFromMeta} from metadata not found. Will try finding user by email. Error: ${e.message}`);
firebaseUIDFromMeta = undefined;
}
}
if (!userRecord && customerEmail) {
try {
userRecord = await adminAuth.getUserByEmail(customerEmail);
firebaseUIDFromMeta = userRecord.uid;
console.log(`[Webhook] Found existing Firebase user by email: ${customerEmail}, UID: ${firebaseUIDFromMeta}`);
} catch (e: any) {
if (e.code === 'auth/user-not-found') {
console.log(`[Webhook] No Firebase user found for email ${customerEmail}. This is expected for new signups.`);
} else {
throw e;
}
}
}
if (!firebaseUIDFromMeta && customerEmail) {
console.log(`[Webhook] Could not find existing Firebase user for ${customerEmail}. Creating a new user record.`);
const newUserRecord = await adminAuth.createUser({
email: customerEmail,
emailVerified: true, // Assuming payment implies email is valid
});
firebaseUIDFromMeta = newUserRecord.uid;
console.log(`[Webhook] Created new Firebase user with UID: ${firebaseUIDFromMeta}`);
}
if (!firebaseUIDFromMeta) {
console.error("[Webhook] CRITICAL: Could not determine or create Firebase UID for customer:", stripeCustomerId);
return { status: 200, message: "Acknowledged: Could not determine Firebase UID. Logged for manual review." };
}
const plan = getPlanById(planIdFromMeta);
if (!plan || plan.id === 'enterprise') {
console.error(`[Webhook] Invalid planId '${planIdFromMeta}' in metadata.`);
return { status: 200, message: `Acknowledged: Invalid planId ${planIdFromMeta}.` };
}
if (!stripeSubscriptionId && plan.priceMonthly !== undefined && plan.priceMonthly > 0) {
console.error(`[Webhook] Missing stripeSubscriptionId for paid plan '${planIdFromMeta}'.`);
return { status: 200, message: `Acknowledged: Missing stripeSubscriptionId for plan ${planIdFromMeta}.` };
}
const userRef = adminDb.collection("users").doc(firebaseUIDFromMeta);
console.log(`[Webhook] Preparing to write to Firestore for user ${firebaseUIDFromMeta} with plan ${plan.id}`);
await userRef.set({
stripeCustomerId,
stripeSubscriptionId: stripeSubscriptionId || null,
stripePriceId: stripePriceId || null,
planId: plan.id,
quota: getResetQuota(plan.id),
quotaLastReset: admin.firestore.FieldValue.serverTimestamp(),
currentPeriodStart: admin.firestore.FieldValue.serverTimestamp(),
email: customerEmail,
uid: firebaseUIDFromMeta,
}, { merge: true });
console.log(`[Webhook] SUCCESS: User ${firebaseUIDFromMeta} plan updated to ${plan.id} in Firestore.`);
break;
case "customer.subscription.updated":
case "customer.subscription.deleted":
const subscription = event.data.object as Stripe.Subscription;
console.log(`[Webhook] Handling ${event.type} for subscription: ${subscription.id}`);
const stripeCustomerIdForSub = typeof subscription.customer === "string" ? subscription.customer : subscription.customer.id;
if (!stripeCustomerIdForSub) {
console.error(`[Webhook] CRITICAL (${event.type}): Missing customer ID for subscription ${subscription.id}.`);
return { status: 200, message: `Acknowledged: Missing customer ID for ${event.type}.` };
}
const usersToUpdateSnapshot = await adminDb.collection("users").where("stripeCustomerId", "==", stripeCustomerIdForSub).get();
if (usersToUpdateSnapshot.empty) {
console.warn(`[Webhook] No user found with Stripe Customer ID: ${stripeCustomerIdForSub} for ${event.type}.`);
break;
}
for (const userDoc of usersToUpdateSnapshot.docs) {
const firebaseUIDToUpdate = userDoc.id;
console.log(`[Webhook] Found user ${firebaseUIDToUpdate} to update for subscription ${subscription.id}`);
let newPlanKey: PlanId | undefined;
let newStripePriceId: string | null = null;
let newStripeSubscriptionId: string | null = subscription.id;
if (event.type === "customer.subscription.deleted" || subscription.status === "canceled" || subscription.status === "unpaid" || subscription.status === "past_due") {
newPlanKey = "free";
newStripeSubscriptionId = null;
} else if (subscription.items.data.length > 0) {
newStripePriceId = subscription.items.data[0].price.id;
newPlanKey = Object.values(PLANS).find(p => p.stripePriceId === newStripePriceId)?.id;
if (!newPlanKey) {
console.warn(`[Webhook] Subscription ${subscription.id} updated with unknown price ID ${newStripePriceId}. Reverting user ${firebaseUIDToUpdate} to free plan.`);
newPlanKey = "free";
newStripeSubscriptionId = null;
}
} else {
console.warn(`[Webhook] Subscription ${subscription.id} has no items. Reverting user ${firebaseUIDToUpdate} to free plan.`);
newPlanKey = "free";
newStripeSubscriptionId = null;
}
const targetPlan = getPlanById(newPlanKey);
const updatesForSubChange: Partial<UserProfile> = {
planId: targetPlan.id,
stripeSubscriptionId: newStripeSubscriptionId,
stripePriceId: targetPlan.id === 'free' ? null : newStripePriceId,
quota: getResetQuota(targetPlan.id),
quotaLastReset: admin.firestore.FieldValue.serverTimestamp(),
};
await userDoc.ref.update(updatesForSubChange);
console.log(`[Webhook] SUCCESS: User ${firebaseUIDToUpdate} plan updated to ${targetPlan.id} via ${event.type}.`);
}
break;
default:
console.log(`[Webhook] Unhandled event type received: ${event.type}`);
}
return { status: 200, message: "Webhook processed successfully." };
} catch (error: any) {
console.error(`[Webhook] CRITICAL ERROR while processing event ${event.id} (Type: ${event.type}):`, error.message, error.stack);
// Return 200 to Stripe even if our internal logic fails, to prevent retries for a bug we need to fix.
return { error: `Webhook processing error: ${error.message}`, status: 200 };
}
}