recur-skills 0.0.10 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/recur-checkout/SKILL.md +205 -121
- package/skills/recur-entitlements/SKILL.md +163 -32
- package/skills/recur-help/SKILL.md +1 -1
- package/skills/recur-portal/SKILL.md +1 -1
- package/skills/recur-quickstart/SKILL.md +35 -15
- package/skills/recur-webhooks/SKILL.md +342 -209
- package/skills/recur-webhooks/scripts/test-webhook.sh +52 -45
- package/skills/recur-webhooks/scripts/verify-signature.ts +14 -9
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
3
3
|
"name": "recur-skills",
|
|
4
4
|
"description": "Claude Code skills for integrating Recur — Taiwan's subscription payment platform",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.13",
|
|
6
6
|
"owner": {
|
|
7
7
|
"name": "Recur",
|
|
8
8
|
"email": "hi@recur.tw",
|
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
},
|
|
11
11
|
"metadata": {
|
|
12
12
|
"description": "Claude Code skills for integrating Recur - Taiwan's subscription payment platform",
|
|
13
|
-
"version": "0.0.
|
|
13
|
+
"version": "0.0.13"
|
|
14
14
|
},
|
|
15
15
|
"plugins": [
|
|
16
16
|
{
|
|
17
17
|
"name": "recur-skills",
|
|
18
18
|
"displayName": "Recur Skills",
|
|
19
19
|
"description": "Skills for Recur SDK integration (quickstart, checkout, webhooks, entitlements, portal) and the Recur MCP server for signup, API keys, and products",
|
|
20
|
-
"version": "0.0.
|
|
20
|
+
"version": "0.0.13",
|
|
21
21
|
"source": "./",
|
|
22
22
|
"category": "development",
|
|
23
23
|
"homepage": "https://docs.recur.tw/guides/skills",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "recur-skills",
|
|
4
4
|
"displayName": "Recur Skills",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.13",
|
|
6
6
|
"description": "Skills for Recur SDK integration (quickstart, checkout, webhooks, entitlements, customer portal) plus the Recur MCP server (https://mcp.recur.tw/) for account signup, API keys, and products.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Recur",
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "recur-skills",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.13",
|
|
5
5
|
"description": "Skills for Recur SDK integration: quickstart, checkout, webhooks, entitlements, and customer portal. Taiwan subscription billing via PAYUNi.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Recur",
|
|
@@ -4,109 +4,161 @@ description: Implement Recur checkout flows including embedded, modal, and redir
|
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: recur
|
|
7
|
-
version: "0.0.
|
|
7
|
+
version: "0.0.13"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Recur Checkout Integration
|
|
11
11
|
|
|
12
|
-
You are helping implement Recur checkout flows.
|
|
12
|
+
You are helping implement Recur checkout flows. Everything below is checked against the
|
|
13
|
+
`recur-tw` typings (`RecurContextValue`, `UseSubscribeResult`, `CheckoutOptions`,
|
|
14
|
+
`RedirectToCheckoutOptions`, `SubscriptionResult`). Product IDs are CUIDs from
|
|
15
|
+
`list_products` / the dashboard (e.g. `cmfxq8n2a0001l8yz3k5p9t7d`); `prod_xxx` in the
|
|
16
|
+
examples is a placeholder. You can also pass `productSlug`.
|
|
13
17
|
|
|
14
18
|
## Checkout Modes
|
|
15
19
|
|
|
16
|
-
| Mode |
|
|
17
|
-
|
|
18
|
-
| `
|
|
19
|
-
| `modal` | Quick purchases
|
|
20
|
-
|
|
|
20
|
+
| Mode | How | Best For |
|
|
21
|
+
|------|-----|----------|
|
|
22
|
+
| **Hosted** (recommended) | `useRecur().redirectToCheckout()` → checkout.recur.tw | Any app, works on localhost |
|
|
23
|
+
| **Modal** | `useSubscribe()` / `useRecur().checkout()` with provider `checkoutMode: 'modal'` | Quick purchases without leaving the page |
|
|
24
|
+
| **Embedded** | same hooks with provider `checkoutMode: 'embedded'` + `containerElementId` | Custom checkout pages |
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
`checkoutMode` lives on `<RecurProvider config>`, not on the call. The provider default is
|
|
27
|
+
`'embedded'`, which throws without `containerElementId` — set `checkoutMode: 'modal'` when
|
|
28
|
+
you want the popup. There is no `mode: 'hosted' | 'modal'` option on any call.
|
|
23
29
|
|
|
24
|
-
|
|
30
|
+
## Hosted Checkout (recommended)
|
|
31
|
+
|
|
32
|
+
`isCheckingOut` is only set by `checkout()`; `redirectToCheckout()` creates the session and
|
|
33
|
+
navigates away, so track your own pending flag.
|
|
25
34
|
|
|
26
35
|
```tsx
|
|
36
|
+
'use client'
|
|
37
|
+
|
|
38
|
+
import { useState } from 'react'
|
|
27
39
|
import { useRecur } from 'recur-tw'
|
|
28
40
|
|
|
29
41
|
function CheckoutButton({ productId }: { productId: string }) {
|
|
30
|
-
const {
|
|
42
|
+
const { redirectToCheckout } = useRecur()
|
|
43
|
+
const [redirecting, setRedirecting] = useState(false)
|
|
31
44
|
|
|
32
45
|
const handleClick = async () => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// result.id - Subscription/Order ID
|
|
48
|
-
// result.status - 'ACTIVE', 'TRIALING', etc.
|
|
49
|
-
},
|
|
50
|
-
onPaymentFailed: (error) => {
|
|
51
|
-
console.error('Failed:', error)
|
|
52
|
-
return { action: 'retry' } // or 'close' or 'custom'
|
|
53
|
-
},
|
|
54
|
-
onPaymentCancel: () => {
|
|
55
|
-
console.log('User cancelled')
|
|
56
|
-
},
|
|
57
|
-
})
|
|
46
|
+
setRedirecting(true)
|
|
47
|
+
try {
|
|
48
|
+
await redirectToCheckout({
|
|
49
|
+
productId, // or productSlug: 'pro-plan'
|
|
50
|
+
successUrl: `${window.location.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
51
|
+
cancelUrl: `${window.location.origin}/pricing`,
|
|
52
|
+
customerEmail: 'user@example.com', // optional, pre-fills the checkout page
|
|
53
|
+
customerName: 'John Doe', // optional
|
|
54
|
+
externalCustomerId: 'user_123', // optional, links to your user system
|
|
55
|
+
})
|
|
56
|
+
} catch (err) {
|
|
57
|
+
setRedirecting(false)
|
|
58
|
+
console.error('Failed to start checkout:', err)
|
|
59
|
+
}
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
return (
|
|
61
|
-
<button onClick={handleClick} disabled={
|
|
62
|
-
{
|
|
63
|
+
<button onClick={handleClick} disabled={redirecting}>
|
|
64
|
+
{redirecting ? 'Redirecting...' : 'Subscribe'}
|
|
63
65
|
</button>
|
|
64
66
|
)
|
|
65
67
|
}
|
|
66
68
|
```
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
The browser navigates away; no callbacks fire. Confirm the payment on your success page
|
|
71
|
+
(via the session id) or with webhooks.
|
|
72
|
+
|
|
73
|
+
## Modal / Embedded Checkout
|
|
74
|
+
|
|
75
|
+
### Using useSubscribe (recommended for modal)
|
|
76
|
+
|
|
77
|
+
Callbacks are options of the hook; `subscribe()` takes the checkout options only.
|
|
69
78
|
|
|
70
79
|
```tsx
|
|
80
|
+
'use client'
|
|
81
|
+
|
|
71
82
|
import { useSubscribe } from 'recur-tw'
|
|
83
|
+
import { useRouter } from 'next/navigation'
|
|
72
84
|
|
|
73
85
|
function SubscribeButton({ productId }: { productId: string }) {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (subscription) {
|
|
87
|
-
return <p>Subscribed! ID: {subscription.id}</p>
|
|
88
|
-
}
|
|
86
|
+
const router = useRouter()
|
|
87
|
+
const { subscribe, isLoading, error, reset } = useSubscribe({
|
|
88
|
+
onPaymentComplete: (subscription) => {
|
|
89
|
+
// subscription: SubscriptionResult — id, status, productId, amount, currentPeriodEnd
|
|
90
|
+
router.push('/dashboard')
|
|
91
|
+
},
|
|
92
|
+
onPaymentFailed: (error) => {
|
|
93
|
+
console.error('Failed:', error.code, error.message)
|
|
94
|
+
return { action: 'retry' } // or 'close' or 'custom'
|
|
95
|
+
},
|
|
96
|
+
onPaymentCancel: () => console.log('User cancelled'),
|
|
97
|
+
})
|
|
89
98
|
|
|
90
99
|
return (
|
|
91
100
|
<>
|
|
92
|
-
<button
|
|
93
|
-
|
|
101
|
+
<button
|
|
102
|
+
onClick={() => subscribe({ productId, customerEmail: 'user@example.com' })}
|
|
103
|
+
disabled={isLoading}
|
|
104
|
+
>
|
|
105
|
+
{isLoading ? 'Processing...' : 'Subscribe'}
|
|
94
106
|
</button>
|
|
95
|
-
{error && <p className="error">{error.message}</p>}
|
|
107
|
+
{error && <p className="error" onClick={reset}>{error.message}</p>}
|
|
96
108
|
</>
|
|
97
109
|
)
|
|
98
110
|
}
|
|
99
111
|
```
|
|
100
112
|
|
|
101
|
-
|
|
113
|
+
`useSubscribe()` returns `{ subscribe, mutate, isLoading, error, reset }` — there is no
|
|
114
|
+
`subscription` field; use `onPaymentComplete` for the result.
|
|
102
115
|
|
|
103
|
-
|
|
116
|
+
### Using useRecur().checkout (callbacks inline)
|
|
104
117
|
|
|
105
118
|
```tsx
|
|
106
|
-
|
|
119
|
+
'use client'
|
|
120
|
+
|
|
121
|
+
import { useRecur } from 'recur-tw'
|
|
122
|
+
|
|
123
|
+
function CheckoutButton({ productId }: { productId: string }) {
|
|
124
|
+
const { checkout, isCheckingOut } = useRecur()
|
|
125
|
+
|
|
126
|
+
const handleClick = () =>
|
|
127
|
+
checkout({
|
|
128
|
+
productId,
|
|
129
|
+
customerEmail: 'user@example.com', // optional; collected in the form if omitted
|
|
130
|
+
customerName: 'John Doe',
|
|
131
|
+
externalCustomerId: 'user_123',
|
|
132
|
+
onPaymentComplete: (subscription) => console.log('Success!', subscription.id, subscription.status),
|
|
133
|
+
onPaymentFailed: (error) => ({ action: 'retry' }),
|
|
134
|
+
onPaymentCancel: () => console.log('User cancelled'),
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<button onClick={handleClick} disabled={isCheckingOut}>
|
|
139
|
+
{isCheckingOut ? 'Processing...' : 'Subscribe'}
|
|
140
|
+
</button>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Provider setup for modal / embedded
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
// Modal (popup)
|
|
149
|
+
<RecurProvider
|
|
150
|
+
config={{
|
|
151
|
+
publishableKey: process.env.NEXT_PUBLIC_RECUR_PUBLISHABLE_KEY!,
|
|
152
|
+
checkoutMode: 'modal',
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
{children}
|
|
156
|
+
</RecurProvider>
|
|
157
|
+
|
|
158
|
+
// Embedded (inline form) — the container must exist in the DOM
|
|
107
159
|
<RecurProvider
|
|
108
160
|
config={{
|
|
109
|
-
publishableKey: process.env.NEXT_PUBLIC_RECUR_PUBLISHABLE_KEY
|
|
161
|
+
publishableKey: process.env.NEXT_PUBLIC_RECUR_PUBLISHABLE_KEY!,
|
|
110
162
|
checkoutMode: 'embedded',
|
|
111
163
|
containerElementId: 'recur-checkout-container',
|
|
112
164
|
}}
|
|
@@ -114,47 +166,76 @@ For embedded mode, you need a container element:
|
|
|
114
166
|
{children}
|
|
115
167
|
</RecurProvider>
|
|
116
168
|
|
|
117
|
-
// In your checkout page
|
|
118
169
|
function CheckoutPage() {
|
|
119
170
|
return (
|
|
120
171
|
<div>
|
|
121
172
|
<h1>Complete Your Purchase</h1>
|
|
122
|
-
{/* Recur
|
|
173
|
+
{/* Recur renders the payment form here */}
|
|
123
174
|
<div id="recur-checkout-container" />
|
|
124
175
|
</div>
|
|
125
176
|
)
|
|
126
177
|
}
|
|
127
178
|
```
|
|
128
179
|
|
|
129
|
-
|
|
180
|
+
Modal/embedded checkout needs a registered domain; on `localhost` use Hosted Checkout.
|
|
130
181
|
|
|
131
|
-
|
|
182
|
+
## Option Types
|
|
132
183
|
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
184
|
+
```typescript
|
|
185
|
+
// redirectToCheckout()
|
|
186
|
+
interface RedirectToCheckoutOptions {
|
|
187
|
+
productId?: string // or productSlug — one is required
|
|
188
|
+
productSlug?: string
|
|
189
|
+
mode?: 'PAYMENT' | 'SUBSCRIPTION' | 'SETUP' // usually inferred from the product
|
|
190
|
+
successUrl: string
|
|
191
|
+
cancelUrl: string
|
|
192
|
+
customerEmail?: string
|
|
193
|
+
customerName?: string
|
|
194
|
+
externalCustomerId?: string
|
|
195
|
+
}
|
|
141
196
|
|
|
142
|
-
|
|
197
|
+
// checkout() / subscribe()
|
|
198
|
+
interface CheckoutOptions {
|
|
199
|
+
productId?: string
|
|
200
|
+
productSlug?: string
|
|
201
|
+
customerEmail?: string
|
|
202
|
+
customerName?: string
|
|
203
|
+
externalCustomerId?: string
|
|
204
|
+
successUrl?: string // used if 3-D Secure needs a redirect
|
|
205
|
+
cancelUrl?: string
|
|
206
|
+
// checkout() only — for subscribe() pass these to useSubscribe():
|
|
207
|
+
onPaymentComplete?: (subscription: SubscriptionResult) => void
|
|
208
|
+
onPaymentFailed?: (error: CheckoutError) => PaymentFailedAction | void
|
|
209
|
+
onPaymentCancel?: () => void
|
|
210
|
+
onSuccess?: (result: CheckoutResult) => void // session created (before payment)
|
|
211
|
+
onError?: (error: CheckoutError) => void
|
|
212
|
+
}
|
|
143
213
|
|
|
144
|
-
|
|
214
|
+
interface SubscriptionResult { // onPaymentComplete argument
|
|
215
|
+
id: string
|
|
216
|
+
status: string // 'ACTIVE', 'TRIALING', ...
|
|
217
|
+
productId: string
|
|
218
|
+
amount: number // whole TWD (499 = NT$499). NEVER divide by 100
|
|
219
|
+
billingPeriod: string
|
|
220
|
+
currentPeriodStart: string
|
|
221
|
+
currentPeriodEnd: string
|
|
222
|
+
trialEndsAt?: string
|
|
223
|
+
nextBillingDate?: string
|
|
224
|
+
}
|
|
225
|
+
```
|
|
145
226
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
checkout({ productId: 'prod_subscription_xxx' })
|
|
227
|
+
No `trialDays`, `quantity`, or `metadata` on these options — trials and pricing come from
|
|
228
|
+
the product (the server SDK's `checkoutSessions.create()` does accept `metadata`).
|
|
149
229
|
|
|
150
|
-
|
|
151
|
-
checkout({ productId: 'prod_onetime_xxx' })
|
|
230
|
+
## Product Types
|
|
152
231
|
|
|
153
|
-
|
|
154
|
-
checkout({ productId: 'prod_credits_xxx' })
|
|
232
|
+
All four types go through the same calls; the product decides the behaviour:
|
|
155
233
|
|
|
156
|
-
|
|
157
|
-
|
|
234
|
+
```tsx
|
|
235
|
+
redirectToCheckout({ productSlug: 'pro-monthly', successUrl, cancelUrl }) // SUBSCRIPTION (recurring)
|
|
236
|
+
redirectToCheckout({ productSlug: 'ebook', successUrl, cancelUrl }) // ONE_TIME
|
|
237
|
+
redirectToCheckout({ productSlug: 'credits-100', successUrl, cancelUrl }) // CREDITS (prepaid wallet)
|
|
238
|
+
redirectToCheckout({ productSlug: 'support-us', successUrl, cancelUrl }) // DONATION (variable amount)
|
|
158
239
|
```
|
|
159
240
|
|
|
160
241
|
## Listing Products
|
|
@@ -163,15 +244,14 @@ checkout({ productId: 'prod_donation_xxx' })
|
|
|
163
244
|
import { useProducts } from 'recur-tw'
|
|
164
245
|
|
|
165
246
|
function PricingPage() {
|
|
166
|
-
const { products, isLoading } = useProducts({
|
|
167
|
-
type: 'SUBSCRIPTION', // Filter by type
|
|
168
|
-
})
|
|
247
|
+
const { data: products, isLoading, error } = useProducts({ type: 'SUBSCRIPTION' })
|
|
169
248
|
|
|
170
249
|
if (isLoading) return <div>Loading...</div>
|
|
250
|
+
if (error) return <div>{error.message}</div>
|
|
171
251
|
|
|
172
252
|
return (
|
|
173
253
|
<div className="pricing-grid">
|
|
174
|
-
{products
|
|
254
|
+
{products?.map((product) => (
|
|
175
255
|
<PricingCard key={product.id} product={product} />
|
|
176
256
|
))}
|
|
177
257
|
</div>
|
|
@@ -179,38 +259,56 @@ function PricingPage() {
|
|
|
179
259
|
}
|
|
180
260
|
```
|
|
181
261
|
|
|
262
|
+
`Product` fields: `id`, `name`, `slug`, `description`, `type`, `billingPeriod`
|
|
263
|
+
(`'MONTHLY' | 'YEARLY' | ... | null`), `price` (whole TWD), `currency`, `trialDays`,
|
|
264
|
+
`metadata`, `productFamily`, `displayOrder`. There is no `priceFormatted` on the SDK type — format with
|
|
265
|
+
`` `NT$${product.price.toLocaleString()}` ``.
|
|
266
|
+
|
|
182
267
|
## Payment Failed Handling
|
|
183
268
|
|
|
269
|
+
`CheckoutError.code` is `'PAYMENT_FAILED'` for a declined payment; the gateway's own code
|
|
270
|
+
is in `error.details.failure_code` (with `failure_message` and `can_retry`).
|
|
271
|
+
`CheckoutErrorDetails` is a union — an array for conflict errors, an object for payment
|
|
272
|
+
failures — so narrow it before reading the fields.
|
|
273
|
+
|
|
184
274
|
```tsx
|
|
185
275
|
onPaymentFailed: (error) => {
|
|
186
|
-
|
|
187
|
-
|
|
276
|
+
const details = Array.isArray(error.details) ? undefined : error.details
|
|
277
|
+
|
|
278
|
+
switch (details?.failure_code) {
|
|
188
279
|
case 'CARD_DECLINED':
|
|
189
280
|
return { action: 'retry' }
|
|
190
281
|
case 'INSUFFICIENT_FUNDS':
|
|
191
|
-
return {
|
|
192
|
-
action: 'custom',
|
|
193
|
-
customTitle: '餘額不足',
|
|
194
|
-
customMessage: '請使用其他付款方式',
|
|
195
|
-
}
|
|
282
|
+
return { action: 'custom', customTitle: '餘額不足', customMessage: '請使用其他付款方式' }
|
|
196
283
|
default:
|
|
197
|
-
return { action: 'close' }
|
|
284
|
+
return details?.can_retry ? { action: 'retry' } : { action: 'close' }
|
|
198
285
|
}
|
|
199
286
|
}
|
|
200
287
|
```
|
|
201
288
|
|
|
202
|
-
## Server-Side Checkout (
|
|
289
|
+
## Server-Side Checkout (Hosted, no React)
|
|
203
290
|
|
|
204
|
-
|
|
291
|
+
```typescript
|
|
292
|
+
import { Recur } from 'recur-tw/server'
|
|
293
|
+
|
|
294
|
+
const recur = new Recur(process.env.RECUR_SECRET_KEY!)
|
|
295
|
+
const session = await recur.checkoutSessions.create({
|
|
296
|
+
productId: 'prod_xxx', // a CUID from list_products
|
|
297
|
+
successUrl: 'https://yourapp.com/success',
|
|
298
|
+
cancelUrl: 'https://yourapp.com/cancel',
|
|
299
|
+
customerEmail: 'user@example.com',
|
|
300
|
+
metadata: { plan: 'pro' }, // optional, server-side only
|
|
301
|
+
})
|
|
302
|
+
// redirect the customer to session.url
|
|
303
|
+
```
|
|
205
304
|
|
|
206
|
-
|
|
305
|
+
Or with plain REST:
|
|
207
306
|
|
|
208
307
|
```typescript
|
|
209
|
-
// Create checkout session
|
|
210
308
|
const response = await fetch('https://api.recur.tw/v1/checkout/sessions', {
|
|
211
309
|
method: 'POST',
|
|
212
310
|
headers: {
|
|
213
|
-
|
|
311
|
+
Authorization: `Bearer ${process.env.RECUR_SECRET_KEY}`, // X-Recur-Secret-Key also works
|
|
214
312
|
'Content-Type': 'application/json',
|
|
215
313
|
},
|
|
216
314
|
body: JSON.stringify({
|
|
@@ -223,31 +321,17 @@ const response = await fetch('https://api.recur.tw/v1/checkout/sessions', {
|
|
|
223
321
|
|
|
224
322
|
// The session object is the response body itself (no outer `data` wrapper, snake_case keys)
|
|
225
323
|
const { url } = await response.json()
|
|
226
|
-
// Redirect the customer to `url
|
|
227
|
-
// (/v1/checkouts is the embedded-form endpoint and returns a different shape.)
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
## Checkout Result Structure
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
interface CheckoutResult {
|
|
234
|
-
id: string // Subscription or Order ID
|
|
235
|
-
status: string // 'ACTIVE', 'TRIALING', 'PENDING'
|
|
236
|
-
productId: string
|
|
237
|
-
amount: number // In cents (e.g., 29900 = NT$299)
|
|
238
|
-
billingPeriod?: string // 'MONTHLY', 'YEARLY' for subscriptions
|
|
239
|
-
currentPeriodEnd?: string // ISO date
|
|
240
|
-
trialEndsAt?: string // ISO date if trial
|
|
241
|
-
}
|
|
324
|
+
// Redirect the customer to `url`. (/v1/checkouts is the embedded-form endpoint, different shape.)
|
|
242
325
|
```
|
|
243
326
|
|
|
244
327
|
## Best Practices
|
|
245
328
|
|
|
246
|
-
1. **
|
|
247
|
-
2. **
|
|
248
|
-
3. **
|
|
249
|
-
4. **
|
|
250
|
-
5. **
|
|
329
|
+
1. **Default to Hosted Checkout** — works everywhere, including localhost
|
|
330
|
+
2. **Handle every callback** on modal/embedded — onPaymentComplete, onPaymentFailed, onPaymentCancel
|
|
331
|
+
3. **Show loading states** — `isCheckingOut` (useRecur, modal/embedded only) / `isLoading` (useSubscribe); for `redirectToCheckout()` track your own flag
|
|
332
|
+
4. **Pre-fill customer info** when you already have it; it is optional
|
|
333
|
+
5. **Use externalCustomerId** to link Recur customers to your user system
|
|
334
|
+
6. **Test in sandbox first** — `pk_test_` / `sk_test_` keys
|
|
251
335
|
|
|
252
336
|
## Related Skills
|
|
253
337
|
|