diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 20f3f7e..7a6c821 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -21,7 +21,6 @@ Thank you for your interest in contributing to Pulse! We welcome contributions f
NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_AUTH_API_URL=http://localhost:8081
NEXT_PUBLIC_APP_URL=http://localhost:3003
- NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... # For embedded checkout (optional if billing not used)
```
5. **Run the development server**:
```bash
diff --git a/app/checkout/page.tsx b/app/checkout/page.tsx
deleted file mode 100644
index 36b975c..0000000
--- a/app/checkout/page.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-'use client'
-
-/**
- * Embedded Stripe Checkout page.
- * Requires plan_id, interval, limit in URL (e.g. /checkout?plan_id=solo&interval=year&limit=100000).
- * Falls back to pulse_pending_checkout from localStorage (after OAuth).
- */
-
-import { useCallback, useEffect, useMemo, Suspense } from 'react'
-import { useRouter, useSearchParams } from 'next/navigation'
-import { loadStripe } from '@stripe/stripe-js'
-import { EmbeddedCheckoutProvider, EmbeddedCheckout } from '@stripe/react-stripe-js'
-import { useAuth } from '@/lib/auth/context'
-import { createCheckoutSession } from '@/lib/api/billing'
-import { LoadingOverlay } from '@ciphera-net/ui'
-import Link from 'next/link'
-
-const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '')
-
-function CheckoutContent() {
- const router = useRouter()
- const searchParams = useSearchParams()
- const { user } = useAuth()
-
- const planId = searchParams.get('plan_id')
- const interval = searchParams.get('interval')
- const limitParam = searchParams.get('limit')
- const limit = limitParam ? parseInt(limitParam, 10) : null
-
- const paramsValid = planId && interval && limit != null && !Number.isNaN(limit) && limit > 0
-
- const fetchClientSecret = useCallback(async () => {
- let pid = planId
- let int = interval
- let lim = limit
-
- if (!paramsValid) {
- const pending = typeof window !== 'undefined' ? localStorage.getItem('pulse_pending_checkout') : null
- if (pending) {
- try {
- const intent = JSON.parse(pending)
- pid = intent.planId || pid
- int = intent.interval || int
- lim = intent.limit ?? lim
- } catch {
- // ignore
- }
- }
- }
-
- if (!pid || !int || lim == null || lim <= 0) {
- throw new Error('Missing checkout params. Go to Pricing to subscribe.')
- }
-
- const { client_secret } = await createCheckoutSession({
- plan_id: pid,
- interval: int,
- limit: lim,
- })
- return client_secret
- }, [planId, interval, limit, paramsValid])
-
- const options = useMemo(() => ({ fetchClientSecret }), [fetchClientSecret])
-
- useEffect(() => {
- if (!user) {
- const intent = paramsValid
- ? { planId, interval, limit, fromCheckout: true }
- : typeof window !== 'undefined' ? localStorage.getItem('pulse_pending_checkout') : null
- if (intent && typeof intent === 'string') {
- try {
- const parsed = JSON.parse(intent)
- localStorage.setItem('pulse_pending_checkout', JSON.stringify({ ...parsed, fromCheckout: true }))
- } catch {
- // ignore
- }
- } else if (paramsValid && typeof window !== 'undefined') {
- localStorage.setItem('pulse_pending_checkout', JSON.stringify({ planId, interval, limit, fromCheckout: true }))
- }
- router.replace('/login')
- return
- }
-
- if (!paramsValid) {
- const pending = typeof window !== 'undefined' ? localStorage.getItem('pulse_pending_checkout') : null
- if (!pending) {
- router.replace('/pricing')
- return
- }
- }
- }, [user, paramsValid, planId, interval, limit, router])
-
- if (!user) {
- return
- }
-
- if (!paramsValid) {
- const pending = typeof window !== 'undefined' ? localStorage.getItem('pulse_pending_checkout') : null
- if (!pending) {
- return (
-
-
Missing checkout parameters.
-
- Go to Pricing
-
-
- )
- }
- }
-
- if (!process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) {
- return (
-
-
Checkout is not configured.
-
- Back to Pricing
-
-
- )
- }
-
- return (
-
-
-
-
-
- )
-}
-
-export default function CheckoutPage() {
- return (
- }>
-
-
- )
-}
diff --git a/app/checkout/return/page.tsx b/app/checkout/return/page.tsx
deleted file mode 100644
index bd448e4..0000000
--- a/app/checkout/return/page.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-'use client'
-
-/**
- * Return page after Embedded Checkout.
- * Stripe redirects here with ?session_id={CHECKOUT_SESSION_ID}.
- * Fetches session status and redirects to dashboard on success.
- */
-
-import { Suspense, useEffect, useState } from 'react'
-import { useRouter, useSearchParams } from 'next/navigation'
-import { getCheckoutSessionStatus } from '@/lib/api/billing'
-import { LoadingOverlay } from '@ciphera-net/ui'
-import Link from 'next/link'
-
-function CheckoutReturnContent() {
- const router = useRouter()
- const searchParams = useSearchParams()
- const sessionId = searchParams.get('session_id')
- const [status, setStatus] = useState<'loading' | 'complete' | 'open' | 'error'>('loading')
- const [errorMessage, setErrorMessage] = useState('')
-
- useEffect(() => {
- if (!sessionId) {
- setStatus('error')
- setErrorMessage('Missing session ID')
- return
- }
-
- let cancelled = false
-
- async function check() {
- try {
- const data = await getCheckoutSessionStatus(sessionId!)
- if (cancelled) return
-
- if (data.status === 'complete') {
- setStatus('complete')
- router.replace('/')
- return
- }
-
- if (data.status === 'open') {
- setStatus('open')
- setErrorMessage('Payment was not completed. You can try again.')
- return
- }
-
- setStatus('error')
- setErrorMessage('Unexpected session status. Please contact support if you were charged.')
- } catch (err) {
- if (cancelled) return
- setStatus('error')
- setErrorMessage((err as Error)?.message || 'Failed to verify payment')
- }
- }
-
- check()
- return () => { cancelled = true }
- }, [sessionId, router])
-
- if (status === 'loading') {
- return
- }
-
- if (status === 'complete') {
- return
- }
-
- return (
-
-
- {status === 'open' ? 'Payment not completed' : 'Something went wrong'}
-
-
- {errorMessage}
-
-
-
- Back to pricing
-
-
- Go to dashboard
-
-
-
- )
-}
-
-export default function CheckoutReturnPage() {
- return (
- }>
-
-
- )
-}
diff --git a/app/welcome/page.tsx b/app/welcome/page.tsx
index 2fd1ac5..bab5fbe 100644
--- a/app/welcome/page.tsx
+++ b/app/welcome/page.tsx
@@ -16,6 +16,7 @@ import {
type Organization,
type OrganizationMember,
} from '@/lib/api/organization'
+import { createCheckoutSession } from '@/lib/api/billing'
import { createSite, type Site } from '@/lib/api/sites'
import { setSessionAction } from '@/app/actions/auth'
import { useAuth } from '@/lib/auth/context'
@@ -217,14 +218,18 @@ function WelcomeContent() {
try {
trackWelcomePlanContinue()
const intent = JSON.parse(raw)
- const params = new URLSearchParams({
+ const { url } = await createCheckoutSession({
plan_id: intent.planId,
interval: intent.interval || 'month',
- limit: String(intent.limit ?? 100000),
+ limit: intent.limit ?? 100000,
})
localStorage.removeItem('pulse_pending_checkout')
- setRedirectingCheckout(true)
- router.push(`/checkout?${params.toString()}`)
+ if (url) {
+ setRedirectingCheckout(true)
+ window.location.href = url
+ return
+ }
+ throw new Error('No checkout URL returned')
} catch (err: unknown) {
setPlanError(getAuthErrorMessage(err) || (err as Error)?.message || 'Failed to start checkout')
localStorage.removeItem('pulse_pending_checkout')
diff --git a/components/PricingSection.tsx b/components/PricingSection.tsx
index faeb9db..04d8b1f 100644
--- a/components/PricingSection.tsx
+++ b/components/PricingSection.tsx
@@ -1,12 +1,13 @@
'use client'
import { useState, useEffect } from 'react'
-import { useRouter, useSearchParams } from 'next/navigation'
+import { useSearchParams } from 'next/navigation'
import { motion } from 'framer-motion'
import { Button, CheckCircleIcon } from '@ciphera-net/ui'
import { useAuth } from '@/lib/auth/context'
import { initiateOAuthFlow } from '@/lib/api/oauth'
import { toast } from '@ciphera-net/ui'
+import { createCheckoutSession } from '@/lib/api/billing'
// 1. Define Plans with IDs and Site Limits
const PLANS = [
@@ -101,7 +102,6 @@ const TRAFFIC_TIERS = [
]
export default function PricingSection() {
- const router = useRouter()
const searchParams = useSearchParams()
const [isYearly, setIsYearly] = useState(false)
const [sliderIndex, setSliderIndex] = useState(2) // Default to 100k (index 2)
@@ -186,16 +186,22 @@ export default function PricingSection() {
return
}
- // 2. Navigate to embedded checkout page
+ // 2. Call backend to create checkout session
const interval = options?.interval || (isYearly ? 'year' : 'month')
const limit = options?.limit || currentTraffic.value
- const params = new URLSearchParams({
+ const { url } = await createCheckoutSession({
plan_id: planId,
interval,
- limit: String(limit),
+ limit,
})
- router.push(`/checkout?${params.toString()}`)
+
+ // 3. Redirect to Stripe Checkout
+ if (url) {
+ window.location.href = url
+ } else {
+ throw new Error('No checkout URL returned')
+ }
} catch (error: any) {
console.error('Checkout error:', error)
diff --git a/components/settings/OrganizationSettings.tsx b/components/settings/OrganizationSettings.tsx
index df57ee5..72d2b84 100644
--- a/components/settings/OrganizationSettings.tsx
+++ b/components/settings/OrganizationSettings.tsx
@@ -16,7 +16,7 @@ import {
OrganizationInvitation,
Organization
} from '@/lib/api/organization'
-import { getSubscription, createPortalSession, getInvoices, cancelSubscription, changePlan, SubscriptionDetails, Invoice } from '@/lib/api/billing'
+import { getSubscription, createPortalSession, getInvoices, cancelSubscription, changePlan, createCheckoutSession, SubscriptionDetails, Invoice } from '@/lib/api/billing'
import { TRAFFIC_TIERS, PLAN_ID_SOLO, getTierIndexForLimit, getLimitForTierIndex, getSitesLimitForPlan } from '@/lib/plans'
import { getAuditLog, AuditLogEntry, GetAuditLogParams } from '@/lib/api/audit'
import { getNotificationSettings, updateNotificationSettings } from '@/lib/api/notification-settings'
@@ -351,12 +351,9 @@ export default function OrganizationSettings() {
setShowChangePlanModal(false)
loadSubscription()
} else {
- const params = new URLSearchParams({
- plan_id: PLAN_ID_SOLO,
- interval,
- limit: String(limit),
- })
- router.push(`/checkout?${params.toString()}`)
+ const { url } = await createCheckoutSession({ plan_id: PLAN_ID_SOLO, interval, limit })
+ if (url) window.location.href = url
+ else throw new Error('No checkout URL')
}
} catch (error: any) {
toast.error(getAuthErrorMessage(error) || error.message || 'Something went wrong.')
diff --git a/lib/api/billing.ts b/lib/api/billing.ts
index fda5489..43207e3 100644
--- a/lib/api/billing.ts
+++ b/lib/api/billing.ts
@@ -85,24 +85,13 @@ export interface CreateCheckoutParams {
limit: number
}
-export async function createCheckoutSession(params: CreateCheckoutParams): Promise<{ client_secret: string }> {
- return await billingFetch<{ client_secret: string }>('/api/billing/checkout', {
+export async function createCheckoutSession(params: CreateCheckoutParams): Promise<{ url: string }> {
+ return await billingFetch<{ url: string }>('/api/billing/checkout', {
method: 'POST',
body: JSON.stringify(params),
})
}
-export interface CheckoutSessionStatus {
- status: string
- customer_email: string
-}
-
-export async function getCheckoutSessionStatus(sessionId: string): Promise {
- return await billingFetch(`/api/billing/checkout/session-status?session_id=${encodeURIComponent(sessionId)}`, {
- method: 'GET',
- })
-}
-
export interface Invoice {
id: string
amount_paid: number