From f085c11ba30125b8a3ba69aad129bd3bc9833348 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 31 Jan 2026 17:29:54 +0100 Subject: [PATCH] feat: implement loading state and error handling in PricingSection; add Suspense for async components in Pricing and Settings pages --- app/pricing/page.tsx | 5 +++- app/settings/page.tsx | 5 ++++ components/PricingSection.tsx | 31 +++++++++++++------- components/checkout/CheckoutSuccessToast.tsx | 26 ++++++++++++++++ lib/api/billing.ts | 13 ++++++++ 5 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 components/checkout/CheckoutSuccessToast.tsx diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx index d7ce1c2..6381381 100644 --- a/app/pricing/page.tsx +++ b/app/pricing/page.tsx @@ -1,9 +1,12 @@ +import { Suspense } from 'react' import PricingSection from '@/components/PricingSection' export default function PricingPage() { return (
- + Loading...
}> + + ) } diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 012f348..030ef5c 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -1,4 +1,6 @@ +import { Suspense } from 'react' import ProfileSettings from '@/components/settings/ProfileSettings' +import CheckoutSuccessToast from '@/components/checkout/CheckoutSuccessToast' export const metadata = { title: 'Settings - Pulse', @@ -8,6 +10,9 @@ export const metadata = { export default function SettingsPage() { return (
+ + +
) diff --git a/components/PricingSection.tsx b/components/PricingSection.tsx index 0d3b003..88c5ac5 100644 --- a/components/PricingSection.tsx +++ b/components/PricingSection.tsx @@ -1,11 +1,12 @@ 'use client' import { useState, useEffect } from 'react' +import { useSearchParams } from 'next/navigation' import { Button, CheckCircleIcon } from '@ciphera-net/ui' import { useAuth } from '@/lib/auth/context' import { initiateOAuthFlow } from '@/lib/api/oauth' import { toast } from 'sonner' -import { getClient } from '@/lib/api/client' +import { createCheckoutSession } from '@/lib/api/billing' // 1. Define Plans with IDs and Site Limits const PLANS = [ @@ -100,11 +101,22 @@ const TRAFFIC_TIERS = [ ] export default function PricingSection() { + const searchParams = useSearchParams() const [isYearly, setIsYearly] = useState(false) const [sliderIndex, setSliderIndex] = useState(2) // Default to 100k (index 2) const [loadingPlan, setLoadingPlan] = useState(null) const { user } = useAuth() - + + // * Show toast when redirected from Stripe Checkout with canceled=true + useEffect(() => { + if (searchParams.get('canceled') === 'true') { + toast.info('Checkout was canceled. You can try again whenever you’re ready.') + const url = new URL(window.location.href) + url.searchParams.delete('canceled') + window.history.replaceState({}, '', url.pathname + url.search) + } + }, [searchParams]) + // * Check for pending checkout on mount/auth useEffect(() => { if (!user) return @@ -174,19 +186,18 @@ export default function PricingSection() { } // 2. Call backend to create checkout session - const client = getClient() const interval = options?.interval || (isYearly ? 'year' : 'month') const limit = options?.limit || currentTraffic.value - const res = await client.post('/api/billing/checkout', { + const { url } = await createCheckoutSession({ plan_id: planId, - interval: interval, - limit: limit + interval, + limit, }) // 3. Redirect to Stripe Checkout - if (res.data.url) { - window.location.href = res.data.url + if (url) { + window.location.href = url } else { throw new Error('No checkout URL returned') } @@ -317,14 +328,14 @@ export default function PricingSection() {
    diff --git a/components/checkout/CheckoutSuccessToast.tsx b/components/checkout/CheckoutSuccessToast.tsx new file mode 100644 index 0000000..3c13698 --- /dev/null +++ b/components/checkout/CheckoutSuccessToast.tsx @@ -0,0 +1,26 @@ +'use client' + +import { useEffect } from 'react' +import { useSearchParams } from 'next/navigation' +import { toast } from 'sonner' + +/** + * Shows a success toast when redirected from Stripe Checkout with success=true, + * then clears the query params from the URL. + */ +export default function CheckoutSuccessToast() { + const searchParams = useSearchParams() + + useEffect(() => { + const success = searchParams.get('success') + if (success === 'true') { + toast.success('Thank you for subscribing! Your subscription is now active.') + const url = new URL(window.location.href) + url.searchParams.delete('success') + url.searchParams.delete('session_id') + window.history.replaceState({}, '', url.pathname + url.search) + } + }, [searchParams]) + + return null +} diff --git a/lib/api/billing.ts b/lib/api/billing.ts index b7da62d..179591b 100644 --- a/lib/api/billing.ts +++ b/lib/api/billing.ts @@ -45,3 +45,16 @@ export async function createPortalSession(): Promise<{ url: string }> { method: 'POST', }) } + +export interface CreateCheckoutParams { + plan_id: string + interval: string + limit: number +} + +export async function createCheckoutSession(params: CreateCheckoutParams): Promise<{ url: string }> { + return await billingFetch<{ url: string }>('/api/billing/checkout', { + method: 'POST', + body: JSON.stringify(params), + }) +}