diff --git a/frontend/src/app/dashboard/profile/page.tsx b/frontend/src/app/dashboard/profile/page.tsx index 877fca5..c25b2cd 100644 --- a/frontend/src/app/dashboard/profile/page.tsx +++ b/frontend/src/app/dashboard/profile/page.tsx @@ -71,7 +71,8 @@ export default function ProfilePage() { const [loadingPortal, setLoadingPortal] = useState(false); const [loadingCancel, setLoadingCancel] = useState(false); const [isClearing, setIsClearing] = useState(false); - const [activeTab, setActiveTab] = useState('account'); + const searchParams = typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : null; + const [activeTab, setActiveTab] = useState(searchParams?.get('tab') ?? 'account'); const [defaultLanguage, setDefaultLanguage] = useState(settings.defaultTargetLanguage); const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null; @@ -98,7 +99,7 @@ export default function ProfilePage() { const res = await fetch(`${API_BASE}/api/v1/auth/billing-portal`, { headers: authHeaders }); const j = await res.json(); const url = j.data?.url ?? j.url; - if (url) window.open(url, '_blank'); + if (url) window.location.href = url; // same tab so return_url brings back to app else setStatusMsg({ type: 'err', text: t('profile.subscription.billingUnavailable') }); } catch { setStatusMsg({ type: 'err', text: t('profile.subscription.billingError') }); } finally { setLoadingPortal(false); } @@ -434,8 +435,8 @@ export default function ProfilePage() { )} - {/* Danger zone */} - {!isFreePlan && !isCanceling && user?.stripe_subscription_id && ( + {/* Danger zone — show if user has a paid plan (even if sub ID not yet synced) */} + {!isFreePlan && !isCanceling && (
diff --git a/services/payment_service.py b/services/payment_service.py index 92ebce2..c622495 100644 --- a/services/payment_service.py +++ b/services/payment_service.py @@ -288,22 +288,40 @@ async def handle_checkout_completed(session: Dict): subscription_ends_at = None if isinstance(subscription_raw, str): - subscription_id = subscription_raw + # Not expanded — fetch subscription to get period end + try: + sub = stripe.Subscription.retrieve(subscription_raw) + subscription_id = sub["id"] + if sub.get("current_period_end"): + from datetime import timezone + subscription_ends_at = datetime.fromtimestamp( + sub["current_period_end"], tz=timezone.utc + ) + except Exception: + subscription_id = subscription_raw elif subscription_raw: - # Expanded subscription object (from sync path with expand=["subscription"]) + # Expanded subscription object (from sync path) subscription_id = subscription_raw.get("id") period_end = subscription_raw.get("current_period_end") if period_end: - subscription_ends_at = datetime.fromtimestamp(period_end).isoformat() + from datetime import timezone + subscription_ends_at = datetime.fromtimestamp(period_end, tz=timezone.utc) + + # Derive tier from plan (DB constraint: only 'free' or 'pro') + tier = "pro" if plan in ("pro", "business", "enterprise") else "free" update_user(user_id, { "plan": plan, + "tier": tier, "subscription_status": SubscriptionStatus.ACTIVE.value, "stripe_subscription_id": subscription_id, + "stripe_customer_id": session.get("customer") or None, "subscription_ends_at": subscription_ends_at, + "cancel_at_period_end": False, "docs_translated_this_month": 0, "pages_translated_this_month": 0, }) + logger.info("Checkout synced: user %s → plan=%s tier=%s sub=%s", user_id, plan, tier, subscription_id) async def handle_subscription_updated(subscription: Dict): @@ -420,16 +438,17 @@ async def get_billing_portal_url(user_id: str) -> Optional[str]: """Get Stripe billing portal URL for customer""" if not is_stripe_configured(): return None - + user = get_user_by_id(user_id) if not user or not user.stripe_customer_id: return None - + try: session = stripe.billing_portal.Session.create( customer=user.stripe_customer_id, - return_url=f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/dashboard" + return_url=f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/dashboard/profile?tab=subscription" ) return session.url - except: + except Exception as e: + logger.error("get_billing_portal_url error: %s", e) return None