feat: Stripe integration complete - products created, DB migration, payment_failed handler, credit buttons wired
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m5s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m5s
- Create Stripe products/prices (Starter/Pro/Business monthly+yearly) - Fix CRITICAL bug: add subscription_ends_at + cancel_at_period_end columns to users table - Alembic migration: f6a7b8c9d0e1_add_subscription_ends_at_cancel_at_period_end - Fix: implement handle_payment_failed() to set subscription_status=PAST_DUE - Fix: harmonize .env.production Stripe variable names to match pricing_config.py - Fix: add missing FRONTEND_URL and STRIPE_PUBLISHABLE_KEY to .env.production - Add all Stripe Price IDs (test mode) to .env.production - Wire credit purchase buttons to /api/v1/auth/create-credits-checkout - Dashboard sync post-checkout was already implemented (no change needed) Stripe test keys: configured in .env.production Webhook: must be configured on server via stripe CLI or Stripe Dashboard Webhook URL: https://wordly.art/api/v1/auth/webhook/stripe
This commit is contained in:
@@ -283,6 +283,7 @@ export default function PricingPage() {
|
||||
const [openFAQ, setOpenFAQ] = useState<number | null>(null);
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [loadingPlanId, setLoadingPlanId] = useState<string | null>(null);
|
||||
const [loadingCreditIdx, setLoadingCreditIdx] = useState<number | null>(null);
|
||||
const [toastMsg, setToastMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null);
|
||||
const [annualDiscountPercent, setAnnualDiscountPercent] = useState(ANNUAL_DISCOUNT_PERCENT);
|
||||
/** Until false: don't show STATIC_PLANS (avoids flash of stale prices on refresh). */
|
||||
@@ -401,6 +402,41 @@ export default function PricingPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleBuyCredits = async (packageIndex: number) => {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) {
|
||||
router.push(`/auth/login?redirect=/pricing`);
|
||||
return;
|
||||
}
|
||||
setLoadingCreditIdx(packageIndex);
|
||||
setToastMsg(null);
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/v1/auth/create-credits-checkout`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ package_index: packageIndex }),
|
||||
});
|
||||
const data = await res.json();
|
||||
const url = data.data?.url ?? data.url;
|
||||
if (!res.ok) {
|
||||
throw new Error(data.message ?? data.error ?? t('pricing.toast.paymentError'));
|
||||
}
|
||||
if (url) {
|
||||
window.location.replace(url);
|
||||
} else {
|
||||
setToastMsg({ type: 'ok', text: t('pricing.toast.demo', { planId: 'credits' }) });
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : t('pricing.toast.networkError');
|
||||
setToastMsg({ type: 'err', text: message });
|
||||
} finally {
|
||||
setLoadingCreditIdx(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
{/* ── Top navigation — breadcrumb bar ── */}
|
||||
@@ -774,8 +810,12 @@ export default function PricingPage() {
|
||||
<div className="text-muted-foreground text-xs mb-3">{t('pricing.credits.unit')}</div>
|
||||
<div className="text-xl font-bold text-foreground">{pkg.price} €</div>
|
||||
<div className="text-muted-foreground text-xs">{(pkg.price_per_credit * 100).toFixed(0)} {t('pricing.credits.centsPerCredit')}</div>
|
||||
<button className="mt-3 w-full py-1.5 rounded-lg bg-muted hover:bg-muted/80 text-foreground text-xs transition-all">
|
||||
{t('pricing.credits.buy')}
|
||||
<button
|
||||
onClick={() => handleBuyCredits(i)}
|
||||
disabled={loadingCreditIdx === i}
|
||||
className="mt-3 w-full py-1.5 rounded-lg bg-muted hover:bg-muted/80 text-foreground text-xs transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loadingCreditIdx === i ? '...' : t('pricing.credits.buy')}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user