Skip to content

Commit 947ec56

Browse files
fix(checkout): hide offline tab for non-manual payment providers
CheckoutForm always rendered an Offline tab regardless of the product's payment_provider. Submitting it for Stripe products always errored because createPaymentRequest rejects non-manual products. Add paymentProvider prop to CheckoutForm and derive showOfflineTab from it — only manual (or unknown) providers show the offline tab. The checkout page now passes paymentProvider from the product/plan query. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent abd04bb commit 947ec56

2 files changed

Lines changed: 92 additions & 54 deletions

File tree

app/[locale]/(public)/checkout/page.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export default async function CheckoutPage(props: { params: Promise<{ locale: st
6666
let productId: number | undefined = undefined;
6767
let durationDays: number | undefined = undefined;
6868
let features: string | null = null;
69+
let paymentProvider: string | null = null;
6970

7071
if (courseId) {
7172
const { data: course } = await supabase
@@ -91,6 +92,7 @@ export default async function CheckoutPage(props: { params: Promise<{ locale: st
9192
price = parseFloat(product.price);
9293
currency = product.currency?.toUpperCase() || 'USD';
9394
productId = productCourses[0].product_id;
95+
paymentProvider = product.payment_provider ?? null;
9496
if (product.description) description = product.description;
9597

9698
if (product.payment_provider === 'manual') {
@@ -113,6 +115,7 @@ export default async function CheckoutPage(props: { params: Promise<{ locale: st
113115
currency = dbPlan.currency?.toUpperCase() || 'USD';
114116
durationDays = dbPlan.duration_in_days;
115117
features = dbPlan.features;
118+
paymentProvider = dbPlan.payment_provider ?? null;
116119

117120
if (dbPlan.payment_provider === 'manual') {
118121
redirect(`/checkout/manual?planId=${planId}`);
@@ -145,6 +148,7 @@ export default async function CheckoutPage(props: { params: Promise<{ locale: st
145148
features={features}
146149
userName={userName}
147150
userEmail={userEmail}
151+
paymentProvider={paymentProvider}
148152
/>
149153
</div>
150154
</div>

components/public/checkout-form.tsx

Lines changed: 88 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ interface CheckoutFormProps {
3131
features?: string | null;
3232
userName?: string;
3333
userEmail?: string;
34+
paymentProvider?: string | null;
3435
}
3536

3637
export function CheckoutForm({
@@ -45,6 +46,7 @@ export function CheckoutForm({
4546
features,
4647
userName,
4748
userEmail,
49+
paymentProvider,
4850
}: CheckoutFormProps) {
4951
const [loading, setLoading] = useState(false);
5052
const [paymentMethod, setPaymentMethod] = useState<'card' | 'offline'>('card');
@@ -57,7 +59,8 @@ export function CheckoutForm({
5759
const router = useRouter();
5860
const t = useTranslations('checkout');
5961

60-
const isFree = typeof price === 'number' ? price === 0 : price === t('free');
62+
const isFree = typeof price === 'number' ? price === 0 : price === t('free')
63+
const showOfflineTab = !paymentProvider || paymentProvider === 'manual';
6164
const displayPrice = formattedPrice || (typeof price === 'number' ? `$${price}` : price);
6265

6366
// Parse features string into list (features are stored as newline or comma-separated text)
@@ -180,19 +183,89 @@ export function CheckoutForm({
180183

181184
{/* Payment methods */}
182185
<div className="px-6 py-6 sm:px-8">
183-
<Tabs defaultValue="card" onValueChange={(v) => v && setPaymentMethod(v as 'card' | 'offline')}>
184-
<TabsList className="w-full">
185-
<TabsTrigger value="card" className="flex-1 gap-1.5">
186-
<IconCreditCard className="h-3.5 w-3.5" />
187-
{t('payment.card')}
188-
</TabsTrigger>
189-
<TabsTrigger value="offline" className="flex-1 gap-1.5">
190-
<IconBuildingBank className="h-3.5 w-3.5" />
191-
{t('payment.offline')}
192-
</TabsTrigger>
193-
</TabsList>
186+
{showOfflineTab ? (
187+
<Tabs defaultValue="card" onValueChange={(v) => v && setPaymentMethod(v as 'card' | 'offline')}>
188+
<TabsList className="w-full">
189+
<TabsTrigger value="card" className="flex-1 gap-1.5">
190+
<IconCreditCard className="h-3.5 w-3.5" />
191+
{t('payment.card')}
192+
</TabsTrigger>
193+
<TabsTrigger value="offline" className="flex-1 gap-1.5">
194+
<IconBuildingBank className="h-3.5 w-3.5" />
195+
{t('payment.offline')}
196+
</TabsTrigger>
197+
</TabsList>
194198

195-
<TabsContent value="card" className="mt-5 space-y-4">
199+
<TabsContent value="card" className="mt-5 space-y-4">
200+
<div className="space-y-1.5">
201+
<Label htmlFor="cardholder" className="text-xs font-medium">{t('payment.cardholder')}</Label>
202+
<Input id="cardholder" placeholder="John Doe" disabled={loading} />
203+
</div>
204+
<div className="space-y-1.5">
205+
<Label htmlFor="cardNumber" className="text-xs font-medium">{t('payment.cardNumber')}</Label>
206+
<div className="relative">
207+
<Input
208+
id="cardNumber"
209+
placeholder="4242 4242 4242 4242"
210+
className="pl-10"
211+
disabled={loading}
212+
/>
213+
<IconCreditCard className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
214+
</div>
215+
</div>
216+
<div className="grid grid-cols-2 gap-3">
217+
<div className="space-y-1.5">
218+
<Label htmlFor="expiry" className="text-xs font-medium">{t('payment.expiry')}</Label>
219+
<Input id="expiry" placeholder="MM/YY" disabled={loading} />
220+
</div>
221+
<div className="space-y-1.5">
222+
<Label htmlFor="cvc" className="text-xs font-medium">{t('payment.cvc')}</Label>
223+
<Input id="cvc" placeholder="123" disabled={loading} />
224+
</div>
225+
</div>
226+
</TabsContent>
227+
228+
<TabsContent value="offline" className="mt-5 space-y-4">
229+
<p className="rounded-lg bg-muted/50 px-4 py-3 text-xs leading-relaxed text-muted-foreground">
230+
{t('payment.offlineInstructions')}
231+
</p>
232+
<div className="space-y-1.5">
233+
<Label htmlFor="offlineName" className="text-xs font-medium">{t('payment.contactName')}</Label>
234+
<Input
235+
id="offlineName"
236+
placeholder="Your Name"
237+
value={offlineData.name}
238+
onChange={e => setOfflineData({ ...offlineData, name: e.target.value })}
239+
disabled={loading}
240+
/>
241+
</div>
242+
<div className="space-y-1.5">
243+
<Label htmlFor="offlineEmail" className="text-xs font-medium">{t('payment.contactEmail')}</Label>
244+
<Input
245+
id="offlineEmail"
246+
type="email"
247+
placeholder="email@example.com"
248+
value={offlineData.email}
249+
onChange={e => setOfflineData({ ...offlineData, email: e.target.value })}
250+
disabled={loading}
251+
readOnly={!!userEmail}
252+
className={userEmail ? 'bg-muted' : ''}
253+
/>
254+
</div>
255+
<div className="space-y-1.5">
256+
<Label htmlFor="offlinePhone" className="text-xs font-medium">{t('payment.contactPhone')}</Label>
257+
<Input
258+
id="offlinePhone"
259+
placeholder="+1 234 567 8900"
260+
value={offlineData.phone}
261+
onChange={e => setOfflineData({ ...offlineData, phone: e.target.value })}
262+
disabled={loading}
263+
/>
264+
</div>
265+
</TabsContent>
266+
</Tabs>
267+
) : (
268+
<div className="space-y-4">
196269
<div className="space-y-1.5">
197270
<Label htmlFor="cardholder" className="text-xs font-medium">{t('payment.cardholder')}</Label>
198271
<Input id="cardholder" placeholder="John Doe" disabled={loading} />
@@ -219,47 +292,8 @@ export function CheckoutForm({
219292
<Input id="cvc" placeholder="123" disabled={loading} />
220293
</div>
221294
</div>
222-
</TabsContent>
223-
224-
<TabsContent value="offline" className="mt-5 space-y-4">
225-
<p className="rounded-lg bg-muted/50 px-4 py-3 text-xs leading-relaxed text-muted-foreground">
226-
{t('payment.offlineInstructions')}
227-
</p>
228-
<div className="space-y-1.5">
229-
<Label htmlFor="offlineName" className="text-xs font-medium">{t('payment.contactName')}</Label>
230-
<Input
231-
id="offlineName"
232-
placeholder="Your Name"
233-
value={offlineData.name}
234-
onChange={e => setOfflineData({ ...offlineData, name: e.target.value })}
235-
disabled={loading}
236-
/>
237-
</div>
238-
<div className="space-y-1.5">
239-
<Label htmlFor="offlineEmail" className="text-xs font-medium">{t('payment.contactEmail')}</Label>
240-
<Input
241-
id="offlineEmail"
242-
type="email"
243-
placeholder="email@example.com"
244-
value={offlineData.email}
245-
onChange={e => setOfflineData({ ...offlineData, email: e.target.value })}
246-
disabled={loading}
247-
readOnly={!!userEmail}
248-
className={userEmail ? 'bg-muted' : ''}
249-
/>
250-
</div>
251-
<div className="space-y-1.5">
252-
<Label htmlFor="offlinePhone" className="text-xs font-medium">{t('payment.contactPhone')}</Label>
253-
<Input
254-
id="offlinePhone"
255-
placeholder="+1 234 567 8900"
256-
value={offlineData.phone}
257-
onChange={e => setOfflineData({ ...offlineData, phone: e.target.value })}
258-
disabled={loading}
259-
/>
260-
</div>
261-
</TabsContent>
262-
</Tabs>
295+
</div>
296+
)}
263297
</div>
264298

265299
{/* Submit */}

0 commit comments

Comments
 (0)