fix: sync plan+tier after checkout, portal same-tab nav, cancel button always visible for paid plans, portal return URL to profile tab
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m44s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m44s
This commit is contained in:
@@ -71,7 +71,8 @@ export default function ProfilePage() {
|
|||||||
const [loadingPortal, setLoadingPortal] = useState(false);
|
const [loadingPortal, setLoadingPortal] = useState(false);
|
||||||
const [loadingCancel, setLoadingCancel] = useState(false);
|
const [loadingCancel, setLoadingCancel] = useState(false);
|
||||||
const [isClearing, setIsClearing] = 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 [defaultLanguage, setDefaultLanguage] = useState(settings.defaultTargetLanguage);
|
||||||
|
|
||||||
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null;
|
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 res = await fetch(`${API_BASE}/api/v1/auth/billing-portal`, { headers: authHeaders });
|
||||||
const j = await res.json();
|
const j = await res.json();
|
||||||
const url = j.data?.url ?? j.url;
|
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') });
|
else setStatusMsg({ type: 'err', text: t('profile.subscription.billingUnavailable') });
|
||||||
} catch { setStatusMsg({ type: 'err', text: t('profile.subscription.billingError') }); }
|
} catch { setStatusMsg({ type: 'err', text: t('profile.subscription.billingError') }); }
|
||||||
finally { setLoadingPortal(false); }
|
finally { setLoadingPortal(false); }
|
||||||
@@ -434,8 +435,8 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Danger zone */}
|
{/* Danger zone — show if user has a paid plan (even if sub ID not yet synced) */}
|
||||||
{!isFreePlan && !isCanceling && user?.stripe_subscription_id && (
|
{!isFreePlan && !isCanceling && (
|
||||||
<div className="editorial-card p-10 lg:p-12 bg-white dark:bg-[#141414] border-none shadow-editorial border-l-4 border-l-red-500">
|
<div className="editorial-card p-10 lg:p-12 bg-white dark:bg-[#141414] border-none shadow-editorial border-l-4 border-l-red-500">
|
||||||
<div className="flex items-center gap-4 text-red-500 mb-8">
|
<div className="flex items-center gap-4 text-red-500 mb-8">
|
||||||
<ShieldAlert size={20} />
|
<ShieldAlert size={20} />
|
||||||
|
|||||||
@@ -288,22 +288,40 @@ async def handle_checkout_completed(session: Dict):
|
|||||||
subscription_ends_at = None
|
subscription_ends_at = None
|
||||||
|
|
||||||
if isinstance(subscription_raw, str):
|
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:
|
elif subscription_raw:
|
||||||
# Expanded subscription object (from sync path with expand=["subscription"])
|
# Expanded subscription object (from sync path)
|
||||||
subscription_id = subscription_raw.get("id")
|
subscription_id = subscription_raw.get("id")
|
||||||
period_end = subscription_raw.get("current_period_end")
|
period_end = subscription_raw.get("current_period_end")
|
||||||
if 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, {
|
update_user(user_id, {
|
||||||
"plan": plan,
|
"plan": plan,
|
||||||
|
"tier": tier,
|
||||||
"subscription_status": SubscriptionStatus.ACTIVE.value,
|
"subscription_status": SubscriptionStatus.ACTIVE.value,
|
||||||
"stripe_subscription_id": subscription_id,
|
"stripe_subscription_id": subscription_id,
|
||||||
|
"stripe_customer_id": session.get("customer") or None,
|
||||||
"subscription_ends_at": subscription_ends_at,
|
"subscription_ends_at": subscription_ends_at,
|
||||||
|
"cancel_at_period_end": False,
|
||||||
"docs_translated_this_month": 0,
|
"docs_translated_this_month": 0,
|
||||||
"pages_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):
|
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"""
|
"""Get Stripe billing portal URL for customer"""
|
||||||
if not is_stripe_configured():
|
if not is_stripe_configured():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
user = get_user_by_id(user_id)
|
user = get_user_by_id(user_id)
|
||||||
if not user or not user.stripe_customer_id:
|
if not user or not user.stripe_customer_id:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
session = stripe.billing_portal.Session.create(
|
session = stripe.billing_portal.Session.create(
|
||||||
customer=user.stripe_customer_id,
|
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
|
return session.url
|
||||||
except:
|
except Exception as e:
|
||||||
|
logger.error("get_billing_portal_url error: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user