Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions functions/.env.fitapp-ns
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Loaded automatically by `firebase deploy --project fitapp-ns` (Firebase Functions v2).
# Safe to commit: Stripe price IDs are not secrets. Actual secrets
# (STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET) are managed via defineSecret.
#
# Stripe mode is auto-detected from the deployed secret key prefix
# (sk_test_ vs sk_live_). Both sets below must be accurate for the modes
# this project can run in.

# Test mode — FitApp sandbox (acct_1TKU0MPtbwp1t4mS), verified 2026-04-19.
STRIPE_PRO_PRICE_ID_TEST=price_1TKcyWPtbwp1t4mSEwScBDRT
STRIPE_COACH_PRICE_ID_TEST=price_1TKcyWPtbwp1t4mScl7d3fyz

# Live mode — FitApp (acct_1TKU0BQ3zUu0EIKv), verified 2026-04-19.
STRIPE_PRO_PRICE_ID_LIVE=price_1TLYZ2Q3zUu0EIKvJYP1vuei
STRIPE_COACH_PRICE_ID_LIVE=price_1TLYZYQ3zUu0EIKvS79urawK
37 changes: 25 additions & 12 deletions functions/src/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,23 @@ type SubscriptionStatus = 'active' | 'canceled' | 'past_due' | 'trialing';
// SubscriptionStatusRaw matches the Stripe SDK's Subscription.Status literal union
type StripeSubscriptionStatus = 'active' | 'canceled' | 'incomplete' | 'incomplete_expired' | 'past_due' | 'paused' | 'trialing' | 'unpaid';

// Stripe mode is determined by the secret key prefix (sk_test_ vs sk_live_).
// Price IDs differ between test and live mode — configure both sets.
const isTestMode = (): boolean => STRIPE_SECRET_KEY.value().startsWith('sk_test_');

function getPriceIds(): Record<SubscriptionTier, string | null> {
const suffix = isTestMode() ? 'TEST' : 'LIVE';
return {
free: null,
pro: process.env[`STRIPE_PRO_PRICE_ID_${suffix}`] ?? null,
coach: process.env[`STRIPE_COACH_PRICE_ID_${suffix}`] ?? null,
};
}

function getStripe(): InstanceType<typeof Stripe> {
return new Stripe(STRIPE_SECRET_KEY.value(), { apiVersion: '2026-03-25.dahlia' });
}

// Price IDs should be set in Stripe dashboard and referenced here.
// Override via environment if needed.
const PRICE_IDS: Record<SubscriptionTier, string | null> = {
free: null,
pro: process.env.STRIPE_PRO_PRICE_ID || 'price_1TKcyWPtbwp1t4mSEwScBDRT',
coach: process.env.STRIPE_COACH_PRICE_ID || 'price_1TKcyWPtbwp1t4mScl7d3fyz',
};

/**
* Creates a Stripe Checkout session for subscription signup.
* Expects JSON body: { tier: 'pro' | 'coach', successUrl: string, cancelUrl: string }
Expand Down Expand Up @@ -63,9 +68,16 @@ export const createCheckoutSession = onRequest(
return;
}

const priceId = PRICE_IDS[tier];
const priceIds = getPriceIds();
const priceId = priceIds[tier];
if (!priceId) {
res.status(400).json({ error: `No price configured for tier: ${tier}` });
const mode = isTestMode() ? 'test' : 'live';
res.status(400).json({
error: `No price configured for tier "${tier}" in ${mode} mode. `
+ (isTestMode()
? 'Set STRIPE_PRO_PRICE_ID_TEST / STRIPE_COACH_PRICE_ID_TEST env vars.'
: 'Set STRIPE_PRO_PRICE_ID_LIVE / STRIPE_COACH_PRICE_ID_LIVE env vars. Create live prices in the Stripe dashboard first.'),
});
return;
}

Expand Down Expand Up @@ -355,7 +367,8 @@ function mapStripeStatus(status: StripeSubscriptionStatus): SubscriptionStatus {
}

function tierFromPriceId(priceId: string | undefined): SubscriptionTier {
if (priceId === PRICE_IDS.coach) return 'coach';
if (priceId === PRICE_IDS.pro) return 'pro';
const priceIds = getPriceIds();
if (priceId === priceIds.coach) return 'coach';
if (priceId === priceIds.pro) return 'pro';
return 'pro';
}
Loading