skills-ws
Version:
87 agent skills for AI coding assistants — marketing, growth, web3, dev, design & operations. Built for OpenClaw, Claude Code, Cursor, and Codex.
58 lines (47 loc) • 2.75 kB
Markdown
## 11. Frontend Checkout
**Prerequisites for this self-hosted Payment Element flow** (vs. Stripe-hosted Checkout):
- Wrap this form in `<Elements stripe={getStripe()} options={{ clientSecret }}>` (the deferred-intent variant uses `options={{ mode, amount, currency }}`); `clientSecret` is the `confirmation_secret.client_secret` returned by §2's subscribe route.
- The parent generates a `requestId` once (`useState(() => crypto.randomUUID())`) and POSTs it with `priceId`/`paymentMethodId` so the server's idempotency key is stable across retries.
- `clientSecret` may be `null` (trial-only or $0 first invoice → nothing to confirm). Skip confirmation and treat it as success.
- This path charges in the Price's currency — it does **not** get Adaptive Pricing (§7). For localized presentment, use Stripe-hosted Checkout instead.
- SCA/3D Secure is handled by `confirmPayment` + `redirect: 'if_required'`: cards needing a challenge redirect to `return_url`; on return, read the status from the URL's `payment_intent_client_secret` and reconcile via webhook.
```tsx
'use client';
import { useState } from 'react';
import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
export function CheckoutForm({ clientSecret, onSuccess }: { clientSecret: string | null; onSuccess: () => void }) {
const stripe = useStripe();
const elements = useElements();
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) return;
setLoading(true);
setError(null);
// No secret ⇒ trial/$0 invoice: subscription is already active, nothing to confirm.
if (!clientSecret) { onSuccess(); setLoading(false); return; }
const { error: submitErr } = await elements.submit();
if (submitErr) { setError(submitErr.message ?? 'Validation failed'); setLoading(false); return; }
const { error: confirmErr } = await stripe.confirmPayment({
elements, clientSecret,
confirmParams: { return_url: `${window.location.origin}/billing/success` },
redirect: 'if_required',
});
if (confirmErr) { setError(confirmErr.message ?? 'Payment failed'); setLoading(false); return; }
onSuccess();
setLoading(false);
};
return (
<form onSubmit={handleSubmit}>
<PaymentElement options={{ layout: 'tabs' }} />
{error && <p className="mt-2 text-sm text-red-600">{error}</p>}
<button type="submit" disabled={!stripe || loading}
className="mt-4 w-full rounded-lg bg-blue-600 px-4 py-2 text-white disabled:opacity-50">
{loading ? 'Processing...' : 'Subscribe'}
</button>
</form>
);
}
```
---