fix first online resevation
This commit is contained in:
@@ -3,10 +3,7 @@
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { useSignUp } from '@clerk/nextjs'
|
||||
import { apiFetch } from '@/lib/api'
|
||||
import { clerkFrontendEnabled } from '@/lib/clerk'
|
||||
import { useDashboardI18n } from '@/components/I18nProvider'
|
||||
import PublicShell from '@/components/layout/PublicShell'
|
||||
import { marketplaceUrl } from '@/lib/urls'
|
||||
|
||||
@@ -29,325 +26,23 @@ type SignupForm = {
|
||||
type CompletedSignup = {
|
||||
companyName: string
|
||||
email: string
|
||||
emailWarning?: string | null
|
||||
}
|
||||
|
||||
type SignupResponse = {
|
||||
companyId: string
|
||||
companyName: string
|
||||
slug: string
|
||||
trialEndsAt: string | null
|
||||
nextStep: string
|
||||
emailDelivery?: {
|
||||
attempted: boolean
|
||||
success: boolean
|
||||
error: string | null
|
||||
} | null
|
||||
}
|
||||
|
||||
export default function SignUpPage() {
|
||||
return clerkFrontendEnabled ? <ConfiguredSignUpPage /> : <LocalSignUpPage />
|
||||
}
|
||||
|
||||
function ConfiguredSignUpPage() {
|
||||
const { language } = useDashboardI18n()
|
||||
const searchParams = useSearchParams()
|
||||
const dict = {
|
||||
en: {
|
||||
loading: 'Authentication is still loading. Try again in a moment.',
|
||||
passwordShort: 'Choose a password with at least 8 characters.',
|
||||
passwordMismatch: 'Passwords do not match.',
|
||||
startFailed: 'Could not start account creation',
|
||||
resendFailed: 'Could not resend the verification code',
|
||||
completeFailed: 'Could not verify your email and finish setup',
|
||||
working: 'Working…',
|
||||
},
|
||||
fr: {
|
||||
loading: 'L’authentification est en cours de chargement. Réessayez dans un instant.',
|
||||
passwordShort: 'Choisissez un mot de passe d’au moins 8 caractères.',
|
||||
passwordMismatch: 'Les mots de passe ne correspondent pas.',
|
||||
startFailed: 'Impossible de démarrer la création du compte',
|
||||
resendFailed: 'Impossible de renvoyer le code de vérification',
|
||||
completeFailed: 'Impossible de vérifier votre email et finaliser la configuration',
|
||||
working: 'Traitement…',
|
||||
},
|
||||
ar: {
|
||||
loading: 'المصادقة ما زالت قيد التحميل. حاول بعد لحظة.',
|
||||
passwordShort: 'اختر كلمة مرور من 8 أحرف على الأقل.',
|
||||
passwordMismatch: 'كلمتا المرور غير متطابقتين.',
|
||||
startFailed: 'تعذر بدء إنشاء الحساب',
|
||||
resendFailed: 'تعذر إعادة إرسال رمز التحقق',
|
||||
completeFailed: 'تعذر التحقق من البريد وإكمال الإعداد',
|
||||
working: 'جارٍ التنفيذ…',
|
||||
},
|
||||
}[language]
|
||||
const { isLoaded, signUp, setActive } = useSignUp()
|
||||
const initialPlan = normalizePlan(searchParams.get('plan'))
|
||||
const initialBillingPeriod = normalizeBillingPeriod(searchParams.get('billing'))
|
||||
const initialCurrency = normalizeCurrency(searchParams.get('currency'))
|
||||
const [step, setStep] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [verificationCode, setVerificationCode] = useState('')
|
||||
const [awaitingVerification, setAwaitingVerification] = useState(false)
|
||||
const [completedSignup, setCompletedSignup] = useState<CompletedSignup | null>(null)
|
||||
const [form, setForm] = useState<SignupForm>({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
companyName: '',
|
||||
companyPhone: '',
|
||||
city: '',
|
||||
country: '',
|
||||
plan: initialPlan,
|
||||
billingPeriod: initialBillingPeriod,
|
||||
currency: initialCurrency,
|
||||
paymentProvider: 'AMANPAY',
|
||||
})
|
||||
|
||||
async function submitSignup() {
|
||||
if (form.password.length < 8) {
|
||||
setError(dict.passwordShort)
|
||||
return
|
||||
}
|
||||
|
||||
if (form.password !== form.confirmPassword) {
|
||||
setError(dict.passwordMismatch)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
if (!isLoaded || !signUp) {
|
||||
setError(dict.loading)
|
||||
return
|
||||
}
|
||||
|
||||
await signUp.create({
|
||||
emailAddress: form.email,
|
||||
password: form.password,
|
||||
firstName: form.firstName,
|
||||
lastName: form.lastName,
|
||||
unsafeMetadata: {
|
||||
signupFlow: 'company-owner',
|
||||
companyName: form.companyName,
|
||||
companyPhone: form.companyPhone || null,
|
||||
city: form.city || null,
|
||||
country: form.country || null,
|
||||
plan: form.plan,
|
||||
billingPeriod: form.billingPeriod,
|
||||
currency: form.currency,
|
||||
paymentProvider: form.paymentProvider,
|
||||
},
|
||||
})
|
||||
|
||||
await signUp.prepareEmailAddressVerification({ strategy: 'email_code' })
|
||||
setAwaitingVerification(true)
|
||||
} catch (err) {
|
||||
setError(getClerkErrorMessage(err, dict.startFailed))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function resendVerificationCode() {
|
||||
if (!signUp) return
|
||||
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
await signUp.prepareEmailAddressVerification({ strategy: 'email_code' })
|
||||
} catch (err) {
|
||||
setError(getClerkErrorMessage(err, dict.resendFailed))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function completeSignup() {
|
||||
if (!signUp || !setActive) {
|
||||
setError(dict.loading)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const result = await signUp.attemptEmailAddressVerification({ code: verificationCode })
|
||||
|
||||
if (result.status !== 'complete' || !result.createdSessionId) {
|
||||
throw new Error('Email verification is not complete yet.')
|
||||
}
|
||||
|
||||
await setActive({ session: result.createdSessionId })
|
||||
await waitForActiveSession()
|
||||
|
||||
await apiFetch('/auth/company/complete-signup', {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
setCompletedSignup({
|
||||
companyName: form.companyName,
|
||||
email: form.email,
|
||||
})
|
||||
} catch (err) {
|
||||
setError(getClerkErrorMessage(err, dict.completeFailed))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (completedSignup) {
|
||||
return (
|
||||
<PublicShell>
|
||||
<main className="flex-1 px-4 py-16">
|
||||
<div className="mx-auto max-w-2xl rounded-[2rem] border border-slate-200 bg-white p-10 shadow-sm">
|
||||
<Link href={marketplaceUrl} className="text-xs font-semibold uppercase tracking-[0.24em] text-blue-600">RentalDriveGo</Link>
|
||||
<h1 className="mt-4 text-4xl font-black tracking-tight text-slate-900">Workspace ready</h1>
|
||||
<p className="mt-4 text-base leading-7 text-slate-600">
|
||||
<span className="font-semibold text-slate-900">{completedSignup.companyName}</span> is now active and the owner email
|
||||
<span className="font-semibold text-slate-900"> {completedSignup.email}</span> has been registered.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
||||
<Link href="/dashboard" className="btn-primary justify-center">
|
||||
Enter dashboard
|
||||
</Link>
|
||||
<Link href="/sign-in" className="btn-secondary justify-center">
|
||||
Sign in on another device
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</PublicShell>
|
||||
)
|
||||
}
|
||||
|
||||
if (awaitingVerification) {
|
||||
return (
|
||||
<PublicShell>
|
||||
<main className="flex-1 px-4 py-16">
|
||||
<div className="mx-auto max-w-2xl rounded-[2rem] border border-slate-200 bg-white p-10 shadow-sm">
|
||||
<Link href={marketplaceUrl} className="text-xs font-semibold uppercase tracking-[0.24em] text-blue-600">RentalDriveGo</Link>
|
||||
<h1 className="mt-4 text-4xl font-black tracking-tight text-slate-900">Confirm your email</h1>
|
||||
<p className="mt-4 text-base leading-7 text-slate-600">
|
||||
We sent a verification code to <span className="font-semibold text-slate-900">{form.email}</span>. Enter it here to finish creating your workspace.
|
||||
</p>
|
||||
<div className="mt-8 space-y-5">
|
||||
<LabeledInput label="Verification code" value={verificationCode} onChange={setVerificationCode} />
|
||||
{error ? <div className="rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error}</div> : null}
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<button type="button" onClick={() => setAwaitingVerification(false)} className="btn-secondary">
|
||||
Back
|
||||
</button>
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<button type="button" onClick={resendVerificationCode} disabled={loading} className="btn-secondary disabled:cursor-not-allowed disabled:opacity-60">
|
||||
{loading ? 'Sending…' : 'Resend code'}
|
||||
</button>
|
||||
<button type="button" onClick={completeSignup} disabled={loading || !verificationCode.trim()} className="btn-primary disabled:cursor-not-allowed disabled:opacity-60">
|
||||
{loading ? 'Verifying…' : 'Verify and create workspace'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</PublicShell>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PublicShell>
|
||||
<main className="px-4 py-12">
|
||||
<div className="mx-auto max-w-3xl space-y-8">
|
||||
<div className="text-center">
|
||||
<Link href={marketplaceUrl} className="text-xs font-semibold uppercase tracking-[0.24em] text-blue-600">RentalDriveGo</Link>
|
||||
<h1 className="mt-3 text-5xl font-black tracking-tight text-slate-900">Launch your rental workspace</h1>
|
||||
<p className="mt-4 text-base leading-7 text-slate-600">Create the owner account, verify your email, and start your 14-day free trial.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-4">
|
||||
{['Account', 'Company', 'Plan', 'Verify'].map((label, index) => (
|
||||
<div key={label} className={`rounded-full px-4 py-2 text-center text-sm font-semibold ${index + 1 <= step ? 'bg-slate-900 text-white' : 'border border-slate-200 bg-white text-slate-500'}`}>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="rounded-[2rem] border border-slate-200 bg-white p-8 shadow-sm">
|
||||
{error ? <div className="mb-6 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error}</div> : null}
|
||||
|
||||
{step === 1 ? (
|
||||
<div className="space-y-5">
|
||||
<SectionIntro title="Owner account" body="This becomes the primary owner account for the company workspace." />
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<LabeledInput label="First name" value={form.firstName} onChange={(value) => setForm((current) => ({ ...current, firstName: value }))} />
|
||||
<LabeledInput label="Last name" value={form.lastName} onChange={(value) => setForm((current) => ({ ...current, lastName: value }))} />
|
||||
</div>
|
||||
<LabeledInput label="Email" type="email" value={form.email} onChange={(value) => setForm((current) => ({ ...current, email: value }))} />
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<LabeledInput label="Password" type="password" value={form.password} onChange={(value) => setForm((current) => ({ ...current, password: value }))} />
|
||||
<LabeledInput label="Confirm password" type="password" value={form.confirmPassword} onChange={(value) => setForm((current) => ({ ...current, confirmPassword: value }))} />
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">Use at least 8 characters so the owner account can sign in directly after verification.</p>
|
||||
<div className="flex justify-end">
|
||||
<button type="button" onClick={() => setStep(2)} className="btn-primary">Continue</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{step === 2 ? (
|
||||
<div className="space-y-5">
|
||||
<SectionIntro title="Company details" body="We’ll use these details for your trial workspace and public marketplace profile." />
|
||||
<LabeledInput label="Company name" value={form.companyName} onChange={(value) => setForm((current) => ({ ...current, companyName: value }))} />
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<LabeledInput label="Phone" value={form.companyPhone} onChange={(value) => setForm((current) => ({ ...current, companyPhone: value }))} />
|
||||
<LabeledInput label="Country" value={form.country} onChange={(value) => setForm((current) => ({ ...current, country: value }))} />
|
||||
</div>
|
||||
<LabeledInput label="City" value={form.city} onChange={(value) => setForm((current) => ({ ...current, city: value }))} />
|
||||
<NavActions onBack={() => setStep(1)} onNext={() => setStep(3)} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{step === 3 ? (
|
||||
<div className="space-y-5">
|
||||
<SectionIntro title="Choose your plan" body="Your workspace starts immediately on a 14-day free trial in your preferred billing currency." />
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
{['STARTER', 'GROWTH', 'PRO'].map((plan) => (
|
||||
<button
|
||||
key={plan}
|
||||
type="button"
|
||||
onClick={() => setForm((current) => ({ ...current, plan: plan as SignupForm['plan'] }))}
|
||||
className={`rounded-3xl border p-5 text-left ${form.plan === plan ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-200 bg-white text-slate-900'}`}
|
||||
>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.16em]">{plan}</p>
|
||||
<p className="mt-3 text-sm opacity-80">{plan === 'STARTER' ? 'Core fleet, offers, and bookings.' : plan === 'GROWTH' ? 'Featured marketplace placement and more room to scale.' : 'Full white-label and premium controls.'}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<LabeledSelect label="Billing period" value={form.billingPeriod} options={['MONTHLY', 'ANNUAL']} onChange={(value) => setForm((current) => ({ ...current, billingPeriod: value as SignupForm['billingPeriod'] }))} />
|
||||
<LabeledSelect label="Currency" value={form.currency} options={['MAD', 'USD', 'EUR']} onChange={(value) => setForm((current) => ({ ...current, currency: value as SignupForm['currency'] }))} />
|
||||
<LabeledSelect label="Primary provider" value={form.paymentProvider} options={['AMANPAY', 'PAYPAL']} onChange={(value) => setForm((current) => ({ ...current, paymentProvider: value as SignupForm['paymentProvider'] }))} />
|
||||
</div>
|
||||
<NavActions onBack={() => setStep(2)} onNext={() => setStep(4)} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{step === 4 ? (
|
||||
<div className="space-y-5">
|
||||
<SectionIntro title="Verify and launch" body="We’ll create the owner account in Clerk first, email you a verification code, then provision the workspace after that email is confirmed." />
|
||||
<div className="rounded-3xl border border-slate-200 bg-slate-50 p-5 text-sm text-slate-600">
|
||||
<p><span className="font-semibold text-slate-900">Workspace:</span> {form.companyName || 'Your company'}</p>
|
||||
<p className="mt-2"><span className="font-semibold text-slate-900">Plan:</span> {form.plan} · {form.billingPeriod} · {form.currency}</p>
|
||||
<p className="mt-2"><span className="font-semibold text-slate-900">Owner email:</span> {form.email || '—'}</p>
|
||||
</div>
|
||||
<NavActions
|
||||
onBack={() => setStep(3)}
|
||||
onNext={submitSignup}
|
||||
loading={loading}
|
||||
nextLabel="Send verification code"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</PublicShell>
|
||||
)
|
||||
}
|
||||
|
||||
function LocalSignUpPage() {
|
||||
const searchParams = useSearchParams()
|
||||
const initialPlan = normalizePlan(searchParams.get('plan'))
|
||||
const initialBillingPeriod = normalizeBillingPeriod(searchParams.get('billing'))
|
||||
@@ -387,12 +82,13 @@ function LocalSignUpPage() {
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
await apiFetch('/auth/company/signup', {
|
||||
const response = await apiFetch<SignupResponse>('/auth/company/signup', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
firstName: form.firstName,
|
||||
lastName: form.lastName,
|
||||
email: form.email,
|
||||
password: form.password,
|
||||
companyName: form.companyName,
|
||||
companyPhone: form.companyPhone || undefined,
|
||||
city: form.city || undefined,
|
||||
@@ -407,9 +103,10 @@ function LocalSignUpPage() {
|
||||
setCompletedSignup({
|
||||
companyName: form.companyName,
|
||||
email: form.email,
|
||||
emailWarning: getEmailWarning(response.emailDelivery),
|
||||
})
|
||||
} catch (err) {
|
||||
setError(getClerkErrorMessage(err, 'Could not create workspace'))
|
||||
setError(getErrorMessage(err, 'Could not create workspace'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -426,9 +123,17 @@ function LocalSignUpPage() {
|
||||
<span className="font-semibold text-slate-900">{completedSignup.companyName}</span> is now active and the owner email
|
||||
<span className="font-semibold text-slate-900"> {completedSignup.email}</span> has been registered.
|
||||
</p>
|
||||
<div className="mt-8">
|
||||
<Link href="/sign-up" className="btn-primary justify-center">
|
||||
Create another workspace
|
||||
{completedSignup.emailWarning ? (
|
||||
<div className="mt-6 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
|
||||
{completedSignup.emailWarning}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
||||
<Link href="/dashboard" className="btn-primary justify-center">
|
||||
Enter dashboard
|
||||
</Link>
|
||||
<Link href="/sign-in" className="btn-secondary justify-center">
|
||||
Sign in on another device
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -517,7 +222,7 @@ function LocalSignUpPage() {
|
||||
|
||||
{step === 4 ? (
|
||||
<div className="space-y-5">
|
||||
<SectionIntro title="Verify and launch" body="We’ll create the company workspace immediately and start the 14-day trial with the plan you selected." />
|
||||
<SectionIntro title="Review and launch" body="We’ll create the company workspace immediately and start the 14-day trial with the plan you selected." />
|
||||
<div className="rounded-3xl border border-slate-200 bg-slate-50 p-5 text-sm text-slate-600">
|
||||
<p><span className="font-semibold text-slate-900">Workspace:</span> {form.companyName || 'Your company'}</p>
|
||||
<p className="mt-2"><span className="font-semibold text-slate-900">Plan:</span> {form.plan} · {form.billingPeriod} · {form.currency}</p>
|
||||
@@ -547,6 +252,20 @@ function SectionIntro({ title, body }: { title: string; body: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
function getEmailWarning(emailDelivery: SignupResponse['emailDelivery']) {
|
||||
if (!emailDelivery) return null
|
||||
if (emailDelivery.success) return null
|
||||
if (emailDelivery.attempted) {
|
||||
return `The account was created, but the confirmation email could not be delivered${emailDelivery.error ? `: ${emailDelivery.error}` : '.'}`
|
||||
}
|
||||
return 'The account was created, but email sending is not configured on this environment yet.'
|
||||
}
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string) {
|
||||
if (error instanceof Error && error.message) return error.message
|
||||
return fallback
|
||||
}
|
||||
|
||||
function LabeledInput({ label, value, onChange, type = 'text' }: { label: string; value: string; onChange: (value: string) => void; type?: string }) {
|
||||
return (
|
||||
<label className="block">
|
||||
@@ -562,20 +281,22 @@ function LabeledSelect({ label, value, options, onChange }: { label: string; val
|
||||
<span className="mb-1.5 block text-sm font-medium text-slate-700">{label}</span>
|
||||
<select className="input-field" value={value} onChange={(event) => onChange(event.target.value)}>
|
||||
{options.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
<option key={option} value={option}>{option}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
function NavActions({ onBack, onNext, loading = false, nextLabel = 'Continue', disableNext = false }: { onBack?: () => void; onNext: () => void; loading?: boolean; nextLabel?: string; disableNext?: boolean }) {
|
||||
function NavActions({ onBack, onNext, loading = false, nextLabel = 'Continue' }: { onBack?: () => void; onNext: () => void; loading?: boolean; nextLabel?: string }) {
|
||||
return (
|
||||
<div className="flex justify-between gap-3">
|
||||
{onBack ? <button type="button" onClick={onBack} className="btn-secondary">Back</button> : <span />}
|
||||
<button type="button" onClick={onNext} disabled={loading || disableNext} className="btn-primary disabled:cursor-not-allowed disabled:opacity-60">
|
||||
<div className="flex gap-3">
|
||||
{onBack ? (
|
||||
<button type="button" onClick={onBack} className="btn-secondary flex-1 justify-center">
|
||||
Back
|
||||
</button>
|
||||
) : null}
|
||||
<button type="button" onClick={onNext} disabled={loading} className="btn-primary flex-1 justify-center disabled:cursor-not-allowed disabled:opacity-60">
|
||||
{loading ? 'Working…' : nextLabel}
|
||||
</button>
|
||||
</div>
|
||||
@@ -583,50 +304,15 @@ function NavActions({ onBack, onNext, loading = false, nextLabel = 'Continue', d
|
||||
}
|
||||
|
||||
function normalizePlan(value: string | null): SignupForm['plan'] {
|
||||
return value === 'GROWTH' || value === 'PRO' ? value : 'STARTER'
|
||||
if (value === 'GROWTH' || value === 'PRO' || value === 'STARTER') return value
|
||||
return 'STARTER'
|
||||
}
|
||||
|
||||
function normalizeBillingPeriod(value: string | null): SignupForm['billingPeriod'] {
|
||||
return value === 'ANNUAL' || value === 'annual' ? 'ANNUAL' : 'MONTHLY'
|
||||
return value === 'ANNUAL' ? 'ANNUAL' : 'MONTHLY'
|
||||
}
|
||||
|
||||
function normalizeCurrency(value: string | null): SignupForm['currency'] {
|
||||
return value === 'USD' || value === 'EUR' ? value : 'MAD'
|
||||
}
|
||||
|
||||
function getClerkErrorMessage(error: unknown, fallback: string) {
|
||||
if (error && typeof error === 'object' && 'errors' in error && Array.isArray((error as { errors?: Array<{ longMessage?: string; message?: string }> }).errors)) {
|
||||
const first = (error as { errors: Array<{ longMessage?: string; message?: string }> }).errors[0]
|
||||
if (first?.longMessage) return first.longMessage
|
||||
if (first?.message) return first.message
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
async function waitForActiveSession() {
|
||||
for (let attempt = 0; attempt < 10; attempt += 1) {
|
||||
const token = await readActiveSessionToken()
|
||||
if (token) return token
|
||||
await new Promise((resolve) => setTimeout(resolve, 250))
|
||||
}
|
||||
|
||||
throw new Error('Signed in successfully, but could not read the active session token.')
|
||||
}
|
||||
|
||||
async function readActiveSessionToken() {
|
||||
if (typeof window === 'undefined') return null
|
||||
|
||||
const clerk = (window as Window & { __clerk?: { session?: { getToken: () => Promise<string | null> } } }).__clerk
|
||||
if (!clerk?.session?.getToken) return null
|
||||
|
||||
try {
|
||||
return await clerk.session.getToken()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
if (value === 'USD' || value === 'EUR' || value === 'MAD') return value
|
||||
return 'MAD'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user