Skip to content

Commit cc56bd9

Browse files
Sai Sridhar Tarraclaude
authored andcommitted
Fix: Stripe checkout 400, PWA icon 404, and dynamic price ID resolution
- Fix _get_or_create_customer selecting non-existent email column from user_profiles — was silently returning None causing 502 on checkout - Fix success/cancel URLs to use FRONTEND_URL env var (default localhost:3000) and match the query params the billing page already expects (?success=true) - Build PRICE_TO_PLAN dynamically from configured Stripe env vars so webhook-triggered plan upgrades work with real price IDs - Add FRONTEND_URL setting to config - Frontend now reads price IDs from NEXT_PUBLIC_STRIPE_*_PRICE_ID env vars and passes them directly as price_id to bypass backend env var resolution - billingApi.createCheckoutSession accepts optional priceId param - Show descriptive error if Stripe is not configured rather than generic toast - Add icon-192.png and icon-512.png to fix PWA manifest 404 errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 581e33d commit cc56bd9

8 files changed

Lines changed: 52 additions & 18 deletions

File tree

backend/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ def agency_monthly_price_id(self) -> str:
6666
def agency_yearly_price_id(self) -> str:
6767
return self.STRIPE_PRICE_AGENCY_YEARLY or self.STRIPE_AGENCY_PRICE_ID
6868

69+
# Frontend base URL (used for Stripe redirect URLs)
70+
FRONTEND_URL: str = "http://localhost:3000"
71+
6972
# JWT / Auth
7073
SECRET_KEY: str
7174
ALGORITHM: str = "HS256"

backend/routes/billing.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ async def create_checkout(
5353
price_id = plan_to_price.get(body.plan_id, {}).get(body.interval, "")
5454

5555
if not price_id:
56+
logger.error(
57+
"Checkout failed: no price_id resolved. plan_id=%s interval=%s — "
58+
"set STRIPE_PRO_PRICE_ID / STRIPE_AGENCY_PRICE_ID in backend .env",
59+
body.plan_id,
60+
body.interval,
61+
)
5662
raise HTTPException(
5763
status_code=status.HTTP_400_BAD_REQUEST,
5864
detail="No valid price_id or plan_id provided.",

backend/services/stripe_service.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,26 @@
77

88
stripe.api_key = settings.STRIPE_SECRET_KEY
99

10-
# Map Stripe price IDs to plan names (override in .env / config as needed)
11-
PRICE_TO_PLAN: dict[str, str] = {
12-
"price_pro_monthly": "pro",
13-
"price_pro_annual": "pro",
14-
"price_enterprise_monthly": "enterprise",
15-
}
10+
def _build_price_to_plan() -> dict[str, str]:
11+
"""Build the price ID → plan name map from configured env vars."""
12+
mapping: dict[str, str] = {}
13+
for price_id in (
14+
settings.STRIPE_PRO_PRICE_ID,
15+
settings.STRIPE_PRICE_PRO_MONTHLY,
16+
settings.STRIPE_PRICE_PRO_YEARLY,
17+
):
18+
if price_id:
19+
mapping[price_id] = "pro"
20+
for price_id in (
21+
settings.STRIPE_AGENCY_PRICE_ID,
22+
settings.STRIPE_PRICE_AGENCY_MONTHLY,
23+
settings.STRIPE_PRICE_AGENCY_YEARLY,
24+
):
25+
if price_id:
26+
mapping[price_id] = "agency"
27+
return mapping
28+
29+
PRICE_TO_PLAN: dict[str, str] = _build_price_to_plan()
1630

1731

1832
# ---------------------------------------------------------------------------
@@ -49,7 +63,7 @@ async def _get_or_create_customer(user_id: str, email: str) -> str | None:
4963
supabase = get_supabase()
5064
result = (
5165
supabase.table("user_profiles")
52-
.select("stripe_customer_id, email")
66+
.select("stripe_customer_id")
5367
.eq("id", user_id)
5468
.single()
5569
.execute()
@@ -58,9 +72,7 @@ async def _get_or_create_customer(user_id: str, email: str) -> str | None:
5872
existing = profile.get("stripe_customer_id")
5973
if existing:
6074
return existing
61-
# Use provided email or fall back to stored email
62-
resolved_email = email or profile.get("email", "")
63-
return await create_customer(user_id, resolved_email)
75+
return await create_customer(user_id, email)
6476
except Exception as exc:
6577
logger.error(
6678
"_get_or_create_customer error (user_id=%s): %s", user_id, exc
@@ -84,13 +96,14 @@ async def create_checkout_session(
8496
if not customer_id:
8597
return None
8698

99+
frontend_url = settings.FRONTEND_URL.rstrip("/") if hasattr(settings, "FRONTEND_URL") and settings.FRONTEND_URL else "http://localhost:3000"
87100
session = stripe.checkout.Session.create(
88101
customer=customer_id,
89102
payment_method_types=["card"],
90103
line_items=[{"price": price_id, "quantity": 1}],
91104
mode="subscription",
92-
success_url="https://inboxiq.app/billing/success?session_id={CHECKOUT_SESSION_ID}",
93-
cancel_url="https://inboxiq.app/billing/cancel",
105+
success_url=f"{frontend_url}/billing?success=true&session_id={{CHECKOUT_SESSION_ID}}",
106+
cancel_url=f"{frontend_url}/billing?canceled=true",
94107
metadata={"user_id": user_id},
95108
)
96109
return session.url

frontend/.env.local.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
22
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
33
NEXT_PUBLIC_API_URL=http://localhost:8000
44
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_key
5+
NEXT_PUBLIC_STRIPE_PRO_PRICE_ID=price_your_pro_price_id
6+
NEXT_PUBLIC_STRIPE_AGENCY_PRICE_ID=price_your_agency_price_id

frontend/lib/api.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,11 +366,13 @@ export const billingApi = {
366366

367367
createCheckoutSession: async (
368368
planId: string,
369-
interval: 'monthly' | 'yearly' = 'monthly'
369+
interval: 'monthly' | 'yearly' = 'monthly',
370+
priceId?: string
370371
): Promise<{ checkout_url: string }> => {
371372
const { data } = await api.post('/api/billing/checkout', {
372373
plan_id: planId,
373374
interval,
375+
...(priceId ? { price_id: priceId } : {}),
374376
});
375377
return data;
376378
},

frontend/pages/billing/index.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const PLANS: Plan[] = [
5050
'Slack notifications',
5151
'Priority support',
5252
],
53-
stripe_price_id: 'price_pro_monthly',
53+
stripe_price_id: process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID || '',
5454
},
5555
{
5656
id: 'agency',
@@ -66,7 +66,7 @@ const PLANS: Plan[] = [
6666
'API access',
6767
'Dedicated support',
6868
],
69-
stripe_price_id: 'price_agency_monthly',
69+
stripe_price_id: process.env.NEXT_PUBLIC_STRIPE_AGENCY_PRICE_ID || '',
7070
},
7171
];
7272

@@ -111,10 +111,18 @@ export default function BillingPage() {
111111
if (planId === 'free') return;
112112
setLoadingPlan(planId);
113113
try {
114-
const { checkout_url } = await billingApi.createCheckoutSession(planId);
114+
const plan = PLANS.find((p) => p.id === planId);
115+
const priceId = plan?.stripe_price_id || undefined;
116+
const { checkout_url } = await billingApi.createCheckoutSession(planId, 'monthly', priceId);
115117
window.location.href = checkout_url;
116-
} catch {
117-
toast.error('Failed to start checkout. Please try again.');
118+
} catch (err: unknown) {
119+
const msg =
120+
(err as { response?: { data?: { detail?: string } } })?.response?.data?.detail;
121+
toast.error(
122+
msg === 'No valid price_id or plan_id provided.'
123+
? 'Stripe is not configured. Set STRIPE_PRO_PRICE_ID / STRIPE_AGENCY_PRICE_ID in your backend .env.'
124+
: 'Failed to start checkout. Please try again.'
125+
);
118126
setLoadingPlan(null);
119127
}
120128
};

frontend/public/icon-192.png

547 Bytes
Loading

frontend/public/icon-512.png

1.84 KB
Loading

0 commit comments

Comments
 (0)