recur-tw 0.0.1
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/LICENSE +21 -0
- package/README.md +379 -0
- package/dist/index.cjs +109 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +176 -0
- package/dist/index.d.ts +176 -0
- package/dist/index.js +106 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Recur
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# Recur SDK (Taiwan)
|
|
2
|
+
|
|
3
|
+
A React SDK for embedding subscription checkout flows in your application.
|
|
4
|
+
|
|
5
|
+
**專為台灣市場設計** - 使用 PAYUNi 支付網關處理訂閱式付款。
|
|
6
|
+
|
|
7
|
+
Taiwan-specific subscription checkout SDK powered by PAYUNi payment gateway.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Install the package via npm or pnpm:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install recur-tw
|
|
15
|
+
# or
|
|
16
|
+
pnpm add recur-tw
|
|
17
|
+
# or
|
|
18
|
+
yarn add recur-tw
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then import it in your React application:
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { RecurProvider, useRecur } from 'recur-tw';
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### 1. Wrap your app with RecurProvider
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
// app/layout.tsx or your root component
|
|
33
|
+
import { RecurProvider } from 'recur-tw';
|
|
34
|
+
|
|
35
|
+
export default function RootLayout({ children }) {
|
|
36
|
+
return (
|
|
37
|
+
<html>
|
|
38
|
+
<body>
|
|
39
|
+
<RecurProvider config={{ organizationId: 'your-org-id' }}>
|
|
40
|
+
{children}
|
|
41
|
+
</RecurProvider>
|
|
42
|
+
</body>
|
|
43
|
+
</html>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Use the checkout function in your components
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// components/pricing-button.tsx
|
|
52
|
+
'use client';
|
|
53
|
+
|
|
54
|
+
import { useRecur } from 'recur-tw';
|
|
55
|
+
|
|
56
|
+
export function PricingButton({ planId }: { planId: string }) {
|
|
57
|
+
const { checkout, isCheckingOut } = useRecur();
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Button
|
|
61
|
+
onClick={async () => {
|
|
62
|
+
await checkout({ planId });
|
|
63
|
+
}}
|
|
64
|
+
disabled={isCheckingOut}
|
|
65
|
+
>
|
|
66
|
+
{isCheckingOut ? 'Processing...' : 'Subscribe'}
|
|
67
|
+
</Button>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
### RecurProvider Props
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
interface RecurConfig {
|
|
78
|
+
// Organization ID for the checkout
|
|
79
|
+
organizationId?: string;
|
|
80
|
+
|
|
81
|
+
// Base URL for API calls (defaults to current origin)
|
|
82
|
+
baseUrl?: string;
|
|
83
|
+
|
|
84
|
+
// Redirect mode: 'redirect' (default) or 'popup'
|
|
85
|
+
redirectMode?: 'redirect' | 'popup';
|
|
86
|
+
|
|
87
|
+
// Success callback URL
|
|
88
|
+
successUrl?: string;
|
|
89
|
+
|
|
90
|
+
// Cancel callback URL
|
|
91
|
+
cancelUrl?: string;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Usage Examples
|
|
96
|
+
|
|
97
|
+
### Basic Checkout
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
const { checkout } = useRecur();
|
|
101
|
+
|
|
102
|
+
await checkout({
|
|
103
|
+
planId: 'pro-monthly',
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Checkout with Customer Information
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
const { checkout } = useRecur();
|
|
111
|
+
|
|
112
|
+
await checkout({
|
|
113
|
+
planId: 'pro-monthly',
|
|
114
|
+
customerName: 'John Doe',
|
|
115
|
+
customerEmail: 'john@example.com',
|
|
116
|
+
customerPhone: '+886912345678',
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Checkout with Callbacks
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
const { checkout } = useRecur();
|
|
124
|
+
|
|
125
|
+
await checkout({
|
|
126
|
+
planId: 'pro-monthly',
|
|
127
|
+
onSuccess: (result) => {
|
|
128
|
+
console.log('Checkout initiated:', result.subscription.id);
|
|
129
|
+
// Show success toast
|
|
130
|
+
},
|
|
131
|
+
onError: (error) => {
|
|
132
|
+
console.error('Checkout failed:', error.message);
|
|
133
|
+
// Show error toast
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Popup Mode
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
// In your RecurProvider config
|
|
142
|
+
<RecurProvider config={{ redirectMode: 'popup' }}>
|
|
143
|
+
{children}
|
|
144
|
+
</RecurProvider>
|
|
145
|
+
|
|
146
|
+
// In your component
|
|
147
|
+
const { checkout } = useRecur();
|
|
148
|
+
|
|
149
|
+
await checkout({
|
|
150
|
+
planId: 'pro-monthly',
|
|
151
|
+
onPaymentComplete: (subscription) => {
|
|
152
|
+
console.log('Payment completed!', subscription);
|
|
153
|
+
// Refresh page or update UI
|
|
154
|
+
},
|
|
155
|
+
onPaymentCancel: () => {
|
|
156
|
+
console.log('Payment cancelled');
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Custom Form
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
'use client';
|
|
165
|
+
|
|
166
|
+
import { useState } from 'react';
|
|
167
|
+
import { useRecur } from '@/lib/recur';
|
|
168
|
+
import { Button } from '@/components/ui/button';
|
|
169
|
+
import { Input } from '@/components/ui/input';
|
|
170
|
+
|
|
171
|
+
export function CustomCheckoutForm({ planId }: { planId: string }) {
|
|
172
|
+
const { checkout, isCheckingOut } = useRecur();
|
|
173
|
+
const [email, setEmail] = useState('');
|
|
174
|
+
const [name, setName] = useState('');
|
|
175
|
+
|
|
176
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
|
|
179
|
+
await checkout({
|
|
180
|
+
planId,
|
|
181
|
+
customerEmail: email,
|
|
182
|
+
customerName: name,
|
|
183
|
+
onSuccess: () => {
|
|
184
|
+
// Show success message
|
|
185
|
+
},
|
|
186
|
+
onError: (error) => {
|
|
187
|
+
alert(error.message);
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<form onSubmit={handleSubmit}>
|
|
194
|
+
<Input
|
|
195
|
+
type="text"
|
|
196
|
+
placeholder="Name"
|
|
197
|
+
value={name}
|
|
198
|
+
onChange={(e) => setName(e.target.value)}
|
|
199
|
+
required
|
|
200
|
+
/>
|
|
201
|
+
<Input
|
|
202
|
+
type="email"
|
|
203
|
+
placeholder="Email"
|
|
204
|
+
value={email}
|
|
205
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
206
|
+
required
|
|
207
|
+
/>
|
|
208
|
+
<Button type="submit" disabled={isCheckingOut}>
|
|
209
|
+
{isCheckingOut ? 'Processing...' : 'Subscribe'}
|
|
210
|
+
</Button>
|
|
211
|
+
</form>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Dynamic Organization ID
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
// Override organization ID per checkout
|
|
220
|
+
const { checkout } = useRecur();
|
|
221
|
+
|
|
222
|
+
await checkout({
|
|
223
|
+
planId: 'pro-monthly',
|
|
224
|
+
organizationId: 'different-org-id',
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Update Configuration Dynamically
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
const { updateConfig } = useRecur();
|
|
232
|
+
|
|
233
|
+
// Switch to popup mode
|
|
234
|
+
updateConfig({ redirectMode: 'popup' });
|
|
235
|
+
|
|
236
|
+
// Update organization ID
|
|
237
|
+
updateConfig({ organizationId: 'new-org-id' });
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## API Reference
|
|
241
|
+
|
|
242
|
+
### `useRecur()`
|
|
243
|
+
|
|
244
|
+
Returns a `RecurContextValue` object with the following properties:
|
|
245
|
+
|
|
246
|
+
#### `checkout(options: CheckoutOptions): Promise<void>`
|
|
247
|
+
|
|
248
|
+
Initiates a checkout flow.
|
|
249
|
+
|
|
250
|
+
**Options:**
|
|
251
|
+
- `planId` (required): The ID of the subscription plan
|
|
252
|
+
- `customerName` (optional): Customer's name
|
|
253
|
+
- `customerEmail` (optional): Customer's email
|
|
254
|
+
- `customerPhone` (optional): Customer's phone number
|
|
255
|
+
- `organizationId` (optional): Override the organization ID
|
|
256
|
+
- `onSuccess` (optional): Callback when checkout is initiated successfully
|
|
257
|
+
- `onError` (optional): Callback when checkout fails
|
|
258
|
+
- `onPaymentComplete` (optional): Callback when payment is completed (popup mode only)
|
|
259
|
+
- `onPaymentCancel` (optional): Callback when payment is cancelled (popup mode only)
|
|
260
|
+
|
|
261
|
+
#### `isCheckingOut: boolean`
|
|
262
|
+
|
|
263
|
+
Indicates whether a checkout is currently in progress.
|
|
264
|
+
|
|
265
|
+
#### `config: RecurConfig`
|
|
266
|
+
|
|
267
|
+
Current configuration object.
|
|
268
|
+
|
|
269
|
+
#### `updateConfig(config: Partial<RecurConfig>): void`
|
|
270
|
+
|
|
271
|
+
Updates the configuration.
|
|
272
|
+
|
|
273
|
+
## Error Handling
|
|
274
|
+
|
|
275
|
+
All checkout errors are caught and passed to the `onError` callback:
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
await checkout({
|
|
279
|
+
planId: 'pro-monthly',
|
|
280
|
+
onError: (error) => {
|
|
281
|
+
console.error('Error code:', error.code);
|
|
282
|
+
console.error('Error message:', error.message);
|
|
283
|
+
console.error('Error details:', error.details);
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Common error codes:
|
|
289
|
+
- `CHECKOUT_FAILED`: Failed to initiate checkout
|
|
290
|
+
- `CHECKOUT_ERROR`: General checkout error
|
|
291
|
+
|
|
292
|
+
## TypeScript Support
|
|
293
|
+
|
|
294
|
+
The SDK is written in TypeScript and provides full type definitions:
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
import type {
|
|
298
|
+
RecurConfig,
|
|
299
|
+
CheckoutOptions,
|
|
300
|
+
CheckoutResult,
|
|
301
|
+
CheckoutError,
|
|
302
|
+
SubscriptionResult,
|
|
303
|
+
} from 'recur-tw';
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Advanced Usage
|
|
307
|
+
|
|
308
|
+
### Server-Side Organization Detection
|
|
309
|
+
|
|
310
|
+
```tsx
|
|
311
|
+
// app/layout.tsx
|
|
312
|
+
import { RecurProvider } from '@/lib/recur';
|
|
313
|
+
import { getOrganizationIdFromDomain } from '@/lib/utils';
|
|
314
|
+
|
|
315
|
+
export default async function RootLayout({ children }) {
|
|
316
|
+
const organizationId = await getOrganizationIdFromDomain();
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<html>
|
|
320
|
+
<body>
|
|
321
|
+
<RecurProvider config={{ organizationId }}>
|
|
322
|
+
{children}
|
|
323
|
+
</RecurProvider>
|
|
324
|
+
</body>
|
|
325
|
+
</html>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Multi-Organization Support
|
|
331
|
+
|
|
332
|
+
```tsx
|
|
333
|
+
// Let each checkout specify its own organization
|
|
334
|
+
<RecurProvider>
|
|
335
|
+
<App />
|
|
336
|
+
</RecurProvider>
|
|
337
|
+
|
|
338
|
+
// In component
|
|
339
|
+
const { checkout } = useRecur();
|
|
340
|
+
|
|
341
|
+
// Store A
|
|
342
|
+
await checkout({ planId: 'plan-a', organizationId: 'store-a' });
|
|
343
|
+
|
|
344
|
+
// Store B
|
|
345
|
+
await checkout({ planId: 'plan-b', organizationId: 'store-b' });
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Integration with Toast Notifications
|
|
349
|
+
|
|
350
|
+
```tsx
|
|
351
|
+
import { useRecur } from '@/lib/recur';
|
|
352
|
+
import { toast } from 'sonner';
|
|
353
|
+
|
|
354
|
+
export function CheckoutButton({ planId }: { planId: string }) {
|
|
355
|
+
const { checkout, isCheckingOut } = useRecur();
|
|
356
|
+
|
|
357
|
+
const handleCheckout = async () => {
|
|
358
|
+
await checkout({
|
|
359
|
+
planId,
|
|
360
|
+
onSuccess: (result) => {
|
|
361
|
+
toast.success('Redirecting to payment...');
|
|
362
|
+
},
|
|
363
|
+
onError: (error) => {
|
|
364
|
+
toast.error(error.message);
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
return (
|
|
370
|
+
<button onClick={handleCheckout} disabled={isCheckingOut}>
|
|
371
|
+
{isCheckingOut ? 'Loading...' : 'Subscribe Now'}
|
|
372
|
+
</button>
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## License
|
|
378
|
+
|
|
379
|
+
This SDK is part of the Recur project.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
// src/context.tsx
|
|
7
|
+
var RecurContext = react.createContext(null);
|
|
8
|
+
function RecurProvider({ children, config: initialConfig = {} }) {
|
|
9
|
+
const [config, setConfig] = react.useState({
|
|
10
|
+
redirectMode: "redirect",
|
|
11
|
+
...initialConfig
|
|
12
|
+
});
|
|
13
|
+
const [isCheckingOut, setIsCheckingOut] = react.useState(false);
|
|
14
|
+
const updateConfig = react.useCallback((newConfig) => {
|
|
15
|
+
setConfig((prev) => ({ ...prev, ...newConfig }));
|
|
16
|
+
}, []);
|
|
17
|
+
const checkout = react.useCallback(
|
|
18
|
+
async (options) => {
|
|
19
|
+
try {
|
|
20
|
+
setIsCheckingOut(true);
|
|
21
|
+
if (!options.planId) {
|
|
22
|
+
throw new Error("planId is required");
|
|
23
|
+
}
|
|
24
|
+
const organizationId = options.organizationId || config.organizationId;
|
|
25
|
+
if (!organizationId) {
|
|
26
|
+
throw new Error("organizationId is required. Provide it in RecurProvider config or checkout options.");
|
|
27
|
+
}
|
|
28
|
+
const baseUrl = config.baseUrl || (typeof window !== "undefined" ? window.location.origin : "");
|
|
29
|
+
const response = await fetch(`${baseUrl}/api/subscriptions`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
"Content-Type": "application/json"
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
organizationId,
|
|
36
|
+
planId: options.planId,
|
|
37
|
+
customerName: options.customerName,
|
|
38
|
+
customerEmail: options.customerEmail,
|
|
39
|
+
customerPhone: options.customerPhone
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const errorData = await response.json().catch(() => ({}));
|
|
44
|
+
const error = {
|
|
45
|
+
code: errorData.error || "CHECKOUT_FAILED",
|
|
46
|
+
message: errorData.message || "Failed to initiate checkout",
|
|
47
|
+
details: errorData
|
|
48
|
+
};
|
|
49
|
+
options.onError?.(error);
|
|
50
|
+
throw new Error(error.message);
|
|
51
|
+
}
|
|
52
|
+
const result = await response.json();
|
|
53
|
+
options.onSuccess?.(result);
|
|
54
|
+
if (config.redirectMode === "popup") {
|
|
55
|
+
const popup = window.open(
|
|
56
|
+
result.paymentUrl,
|
|
57
|
+
"recurCheckout",
|
|
58
|
+
"width=600,height=700,scrollbars=yes,resizable=yes"
|
|
59
|
+
);
|
|
60
|
+
if (!popup) {
|
|
61
|
+
throw new Error("Failed to open popup. Please allow popups for this site.");
|
|
62
|
+
}
|
|
63
|
+
const pollInterval = setInterval(() => {
|
|
64
|
+
if (popup.closed) {
|
|
65
|
+
clearInterval(pollInterval);
|
|
66
|
+
setIsCheckingOut(false);
|
|
67
|
+
if (options.onPaymentComplete) {
|
|
68
|
+
options.onPaymentComplete(result.subscription);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}, 500);
|
|
72
|
+
} else {
|
|
73
|
+
window.location.href = result.paymentUrl;
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const checkoutError = {
|
|
77
|
+
code: "CHECKOUT_ERROR",
|
|
78
|
+
message: error instanceof Error ? error.message : "An unknown error occurred"
|
|
79
|
+
};
|
|
80
|
+
options.onError?.(checkoutError);
|
|
81
|
+
setIsCheckingOut(false);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
[config]
|
|
86
|
+
);
|
|
87
|
+
const value = react.useMemo(
|
|
88
|
+
() => ({
|
|
89
|
+
config,
|
|
90
|
+
checkout,
|
|
91
|
+
isCheckingOut,
|
|
92
|
+
updateConfig
|
|
93
|
+
}),
|
|
94
|
+
[config, checkout, isCheckingOut, updateConfig]
|
|
95
|
+
);
|
|
96
|
+
return /* @__PURE__ */ jsxRuntime.jsx(RecurContext.Provider, { value, children });
|
|
97
|
+
}
|
|
98
|
+
function useRecur() {
|
|
99
|
+
const context = react.useContext(RecurContext);
|
|
100
|
+
if (!context) {
|
|
101
|
+
throw new Error("useRecur must be used within a RecurProvider");
|
|
102
|
+
}
|
|
103
|
+
return context;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
exports.RecurProvider = RecurProvider;
|
|
107
|
+
exports.useRecur = useRecur;
|
|
108
|
+
//# sourceMappingURL=index.cjs.map
|
|
109
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context.tsx","../src/use-recur.ts"],"names":["createContext","useState","useCallback","useMemo","jsx","useContext"],"mappings":"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","file":"index.cjs","sourcesContent":["'use client';\n\nimport React, { createContext, useCallback, useState, useMemo } from 'react';\nimport type {\n RecurConfig,\n RecurContextValue,\n CheckoutOptions,\n CheckoutResult,\n CheckoutError,\n} from './types.js';\n\nconst RecurContext = createContext<RecurContextValue | null>(null);\n\nexport interface RecurProviderProps {\n children: React.ReactNode;\n config?: RecurConfig;\n}\n\n/**\n * RecurProvider\n *\n * Provides Recur checkout functionality to your React application\n *\n * @example\n * ```tsx\n * <RecurProvider config={{ organizationId: 'org_xxx' }}>\n * <App />\n * </RecurProvider>\n * ```\n */\nexport function RecurProvider({ children, config: initialConfig = {} }: RecurProviderProps) {\n const [config, setConfig] = useState<RecurConfig>({\n redirectMode: 'redirect',\n ...initialConfig,\n });\n\n const [isCheckingOut, setIsCheckingOut] = useState(false);\n\n const updateConfig = useCallback((newConfig: Partial<RecurConfig>) => {\n setConfig((prev) => ({ ...prev, ...newConfig }));\n }, []);\n\n const checkout = useCallback(\n async (options: CheckoutOptions) => {\n try {\n setIsCheckingOut(true);\n\n // Validate required fields\n if (!options.planId) {\n throw new Error('planId is required');\n }\n\n // Determine organization ID\n const organizationId = options.organizationId || config.organizationId;\n if (!organizationId) {\n throw new Error('organizationId is required. Provide it in RecurProvider config or checkout options.');\n }\n\n // Determine base URL\n const baseUrl = config.baseUrl || (typeof window !== 'undefined' ? window.location.origin : '');\n\n // Call the subscription API\n const response = await fetch(`${baseUrl}/api/subscriptions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n organizationId,\n planId: options.planId,\n customerName: options.customerName,\n customerEmail: options.customerEmail,\n customerPhone: options.customerPhone,\n }),\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const error: CheckoutError = {\n code: errorData.error || 'CHECKOUT_FAILED',\n message: errorData.message || 'Failed to initiate checkout',\n details: errorData,\n };\n\n options.onError?.(error);\n throw new Error(error.message);\n }\n\n const result: CheckoutResult = await response.json();\n\n // Call success callback\n options.onSuccess?.(result);\n\n // Handle redirect based on mode\n if (config.redirectMode === 'popup') {\n // Open payment in popup window\n const popup = window.open(\n result.paymentUrl,\n 'recurCheckout',\n 'width=600,height=700,scrollbars=yes,resizable=yes'\n );\n\n if (!popup) {\n throw new Error('Failed to open popup. Please allow popups for this site.');\n }\n\n // Poll for popup close\n const pollInterval = setInterval(() => {\n if (popup.closed) {\n clearInterval(pollInterval);\n setIsCheckingOut(false);\n\n // Check if payment was completed\n // You might want to implement a message listener here\n // or poll your API to check subscription status\n if (options.onPaymentComplete) {\n // Note: result.subscription may not have all fields for SubscriptionResult\n // You may need to fetch full subscription details from API\n options.onPaymentComplete(result.subscription as any);\n }\n }\n }, 500);\n } else {\n // Full page redirect\n window.location.href = result.paymentUrl;\n }\n } catch (error) {\n const checkoutError: CheckoutError = {\n code: 'CHECKOUT_ERROR',\n message: error instanceof Error ? error.message : 'An unknown error occurred',\n };\n\n options.onError?.(checkoutError);\n setIsCheckingOut(false);\n throw error;\n }\n },\n [config]\n );\n\n const value = useMemo(\n () => ({\n config,\n checkout,\n isCheckingOut,\n updateConfig,\n }),\n [config, checkout, isCheckingOut, updateConfig]\n );\n\n return <RecurContext.Provider value={value}>{children}</RecurContext.Provider>;\n}\n\nexport { RecurContext };\n","'use client';\n\nimport { useContext } from 'react';\nimport { RecurContext } from './context.js';\nimport type { RecurContextValue } from './types.js';\n\n/**\n * useRecur Hook\n *\n * Access Recur checkout functionality from within your components\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { checkout, isCheckingOut } = useRecur();\n *\n * return (\n * <button\n * onClick={() => checkout({ planId: 'pro' })}\n * disabled={isCheckingOut}\n * >\n * {isCheckingOut ? 'Processing...' : 'Subscribe'}\n * </button>\n * );\n * }\n * ```\n *\n * @throws {Error} If used outside of RecurProvider\n */\nexport function useRecur(): RecurContextValue {\n const context = useContext(RecurContext);\n\n if (!context) {\n throw new Error('useRecur must be used within a RecurProvider');\n }\n\n return context;\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Recur SDK Types
|
|
6
|
+
*
|
|
7
|
+
* Type definitions for the Recur checkout SDK
|
|
8
|
+
*/
|
|
9
|
+
interface RecurConfig {
|
|
10
|
+
/**
|
|
11
|
+
* Organization ID for the checkout
|
|
12
|
+
* If not provided, will be inferred from the current domain
|
|
13
|
+
*/
|
|
14
|
+
organizationId?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Base URL for API calls
|
|
17
|
+
* Defaults to current origin
|
|
18
|
+
*/
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Redirect mode - how to handle payment redirects
|
|
22
|
+
* - 'redirect': Full page redirect (default)
|
|
23
|
+
* - 'popup': Open payment in popup window
|
|
24
|
+
*/
|
|
25
|
+
redirectMode?: 'redirect' | 'popup';
|
|
26
|
+
/**
|
|
27
|
+
* Success callback URL
|
|
28
|
+
* Where to redirect after successful payment
|
|
29
|
+
*/
|
|
30
|
+
successUrl?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Cancel callback URL
|
|
33
|
+
* Where to redirect if user cancels payment
|
|
34
|
+
*/
|
|
35
|
+
cancelUrl?: string;
|
|
36
|
+
}
|
|
37
|
+
interface CheckoutOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Plan ID to subscribe to
|
|
40
|
+
*/
|
|
41
|
+
planId: string;
|
|
42
|
+
/**
|
|
43
|
+
* Customer information
|
|
44
|
+
*/
|
|
45
|
+
customerName?: string;
|
|
46
|
+
customerEmail?: string;
|
|
47
|
+
customerPhone?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Override organization ID
|
|
50
|
+
*/
|
|
51
|
+
organizationId?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Callback when checkout is initiated successfully
|
|
54
|
+
*/
|
|
55
|
+
onSuccess?: (result: CheckoutResult) => void;
|
|
56
|
+
/**
|
|
57
|
+
* Callback when checkout fails
|
|
58
|
+
*/
|
|
59
|
+
onError?: (error: CheckoutError) => void;
|
|
60
|
+
/**
|
|
61
|
+
* Callback when payment is completed
|
|
62
|
+
* (only works in popup mode)
|
|
63
|
+
*/
|
|
64
|
+
onPaymentComplete?: (subscription: SubscriptionResult) => void;
|
|
65
|
+
/**
|
|
66
|
+
* Callback when payment is cancelled
|
|
67
|
+
* (only works in popup mode)
|
|
68
|
+
*/
|
|
69
|
+
onPaymentCancel?: () => void;
|
|
70
|
+
}
|
|
71
|
+
interface CheckoutResult {
|
|
72
|
+
/**
|
|
73
|
+
* Created subscription object
|
|
74
|
+
*/
|
|
75
|
+
subscription: {
|
|
76
|
+
id: string;
|
|
77
|
+
status: string;
|
|
78
|
+
planId: string;
|
|
79
|
+
amount: number;
|
|
80
|
+
billingPeriod: string;
|
|
81
|
+
trialEndsAt?: string;
|
|
82
|
+
nextBillingDate?: string;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Payment URL to redirect to
|
|
86
|
+
*/
|
|
87
|
+
paymentUrl: string;
|
|
88
|
+
}
|
|
89
|
+
interface CheckoutError {
|
|
90
|
+
/**
|
|
91
|
+
* Error code
|
|
92
|
+
*/
|
|
93
|
+
code: string;
|
|
94
|
+
/**
|
|
95
|
+
* Human-readable error message
|
|
96
|
+
*/
|
|
97
|
+
message: string;
|
|
98
|
+
/**
|
|
99
|
+
* Additional error details
|
|
100
|
+
*/
|
|
101
|
+
details?: Record<string, unknown>;
|
|
102
|
+
}
|
|
103
|
+
interface SubscriptionResult {
|
|
104
|
+
id: string;
|
|
105
|
+
status: string;
|
|
106
|
+
planId: string;
|
|
107
|
+
amount: number;
|
|
108
|
+
billingPeriod: string;
|
|
109
|
+
trialEndsAt?: string;
|
|
110
|
+
currentPeriodStart: string;
|
|
111
|
+
currentPeriodEnd: string;
|
|
112
|
+
nextBillingDate?: string;
|
|
113
|
+
}
|
|
114
|
+
interface RecurContextValue {
|
|
115
|
+
/**
|
|
116
|
+
* Current configuration
|
|
117
|
+
*/
|
|
118
|
+
config: RecurConfig;
|
|
119
|
+
/**
|
|
120
|
+
* Initiate checkout flow
|
|
121
|
+
*/
|
|
122
|
+
checkout: (options: CheckoutOptions) => Promise<void>;
|
|
123
|
+
/**
|
|
124
|
+
* Check if checkout is in progress
|
|
125
|
+
*/
|
|
126
|
+
isCheckingOut: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Update configuration
|
|
129
|
+
*/
|
|
130
|
+
updateConfig: (config: Partial<RecurConfig>) => void;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface RecurProviderProps {
|
|
134
|
+
children: React.ReactNode;
|
|
135
|
+
config?: RecurConfig;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* RecurProvider
|
|
139
|
+
*
|
|
140
|
+
* Provides Recur checkout functionality to your React application
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```tsx
|
|
144
|
+
* <RecurProvider config={{ organizationId: 'org_xxx' }}>
|
|
145
|
+
* <App />
|
|
146
|
+
* </RecurProvider>
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
declare function RecurProvider({ children, config: initialConfig }: RecurProviderProps): react_jsx_runtime.JSX.Element;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* useRecur Hook
|
|
153
|
+
*
|
|
154
|
+
* Access Recur checkout functionality from within your components
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```tsx
|
|
158
|
+
* function MyComponent() {
|
|
159
|
+
* const { checkout, isCheckingOut } = useRecur();
|
|
160
|
+
*
|
|
161
|
+
* return (
|
|
162
|
+
* <button
|
|
163
|
+
* onClick={() => checkout({ planId: 'pro' })}
|
|
164
|
+
* disabled={isCheckingOut}
|
|
165
|
+
* >
|
|
166
|
+
* {isCheckingOut ? 'Processing...' : 'Subscribe'}
|
|
167
|
+
* </button>
|
|
168
|
+
* );
|
|
169
|
+
* }
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* @throws {Error} If used outside of RecurProvider
|
|
173
|
+
*/
|
|
174
|
+
declare function useRecur(): RecurContextValue;
|
|
175
|
+
|
|
176
|
+
export { type CheckoutError, type CheckoutOptions, type CheckoutResult, type RecurConfig, type RecurContextValue, RecurProvider, type RecurProviderProps, type SubscriptionResult, useRecur };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Recur SDK Types
|
|
6
|
+
*
|
|
7
|
+
* Type definitions for the Recur checkout SDK
|
|
8
|
+
*/
|
|
9
|
+
interface RecurConfig {
|
|
10
|
+
/**
|
|
11
|
+
* Organization ID for the checkout
|
|
12
|
+
* If not provided, will be inferred from the current domain
|
|
13
|
+
*/
|
|
14
|
+
organizationId?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Base URL for API calls
|
|
17
|
+
* Defaults to current origin
|
|
18
|
+
*/
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Redirect mode - how to handle payment redirects
|
|
22
|
+
* - 'redirect': Full page redirect (default)
|
|
23
|
+
* - 'popup': Open payment in popup window
|
|
24
|
+
*/
|
|
25
|
+
redirectMode?: 'redirect' | 'popup';
|
|
26
|
+
/**
|
|
27
|
+
* Success callback URL
|
|
28
|
+
* Where to redirect after successful payment
|
|
29
|
+
*/
|
|
30
|
+
successUrl?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Cancel callback URL
|
|
33
|
+
* Where to redirect if user cancels payment
|
|
34
|
+
*/
|
|
35
|
+
cancelUrl?: string;
|
|
36
|
+
}
|
|
37
|
+
interface CheckoutOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Plan ID to subscribe to
|
|
40
|
+
*/
|
|
41
|
+
planId: string;
|
|
42
|
+
/**
|
|
43
|
+
* Customer information
|
|
44
|
+
*/
|
|
45
|
+
customerName?: string;
|
|
46
|
+
customerEmail?: string;
|
|
47
|
+
customerPhone?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Override organization ID
|
|
50
|
+
*/
|
|
51
|
+
organizationId?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Callback when checkout is initiated successfully
|
|
54
|
+
*/
|
|
55
|
+
onSuccess?: (result: CheckoutResult) => void;
|
|
56
|
+
/**
|
|
57
|
+
* Callback when checkout fails
|
|
58
|
+
*/
|
|
59
|
+
onError?: (error: CheckoutError) => void;
|
|
60
|
+
/**
|
|
61
|
+
* Callback when payment is completed
|
|
62
|
+
* (only works in popup mode)
|
|
63
|
+
*/
|
|
64
|
+
onPaymentComplete?: (subscription: SubscriptionResult) => void;
|
|
65
|
+
/**
|
|
66
|
+
* Callback when payment is cancelled
|
|
67
|
+
* (only works in popup mode)
|
|
68
|
+
*/
|
|
69
|
+
onPaymentCancel?: () => void;
|
|
70
|
+
}
|
|
71
|
+
interface CheckoutResult {
|
|
72
|
+
/**
|
|
73
|
+
* Created subscription object
|
|
74
|
+
*/
|
|
75
|
+
subscription: {
|
|
76
|
+
id: string;
|
|
77
|
+
status: string;
|
|
78
|
+
planId: string;
|
|
79
|
+
amount: number;
|
|
80
|
+
billingPeriod: string;
|
|
81
|
+
trialEndsAt?: string;
|
|
82
|
+
nextBillingDate?: string;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Payment URL to redirect to
|
|
86
|
+
*/
|
|
87
|
+
paymentUrl: string;
|
|
88
|
+
}
|
|
89
|
+
interface CheckoutError {
|
|
90
|
+
/**
|
|
91
|
+
* Error code
|
|
92
|
+
*/
|
|
93
|
+
code: string;
|
|
94
|
+
/**
|
|
95
|
+
* Human-readable error message
|
|
96
|
+
*/
|
|
97
|
+
message: string;
|
|
98
|
+
/**
|
|
99
|
+
* Additional error details
|
|
100
|
+
*/
|
|
101
|
+
details?: Record<string, unknown>;
|
|
102
|
+
}
|
|
103
|
+
interface SubscriptionResult {
|
|
104
|
+
id: string;
|
|
105
|
+
status: string;
|
|
106
|
+
planId: string;
|
|
107
|
+
amount: number;
|
|
108
|
+
billingPeriod: string;
|
|
109
|
+
trialEndsAt?: string;
|
|
110
|
+
currentPeriodStart: string;
|
|
111
|
+
currentPeriodEnd: string;
|
|
112
|
+
nextBillingDate?: string;
|
|
113
|
+
}
|
|
114
|
+
interface RecurContextValue {
|
|
115
|
+
/**
|
|
116
|
+
* Current configuration
|
|
117
|
+
*/
|
|
118
|
+
config: RecurConfig;
|
|
119
|
+
/**
|
|
120
|
+
* Initiate checkout flow
|
|
121
|
+
*/
|
|
122
|
+
checkout: (options: CheckoutOptions) => Promise<void>;
|
|
123
|
+
/**
|
|
124
|
+
* Check if checkout is in progress
|
|
125
|
+
*/
|
|
126
|
+
isCheckingOut: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Update configuration
|
|
129
|
+
*/
|
|
130
|
+
updateConfig: (config: Partial<RecurConfig>) => void;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface RecurProviderProps {
|
|
134
|
+
children: React.ReactNode;
|
|
135
|
+
config?: RecurConfig;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* RecurProvider
|
|
139
|
+
*
|
|
140
|
+
* Provides Recur checkout functionality to your React application
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```tsx
|
|
144
|
+
* <RecurProvider config={{ organizationId: 'org_xxx' }}>
|
|
145
|
+
* <App />
|
|
146
|
+
* </RecurProvider>
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
declare function RecurProvider({ children, config: initialConfig }: RecurProviderProps): react_jsx_runtime.JSX.Element;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* useRecur Hook
|
|
153
|
+
*
|
|
154
|
+
* Access Recur checkout functionality from within your components
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```tsx
|
|
158
|
+
* function MyComponent() {
|
|
159
|
+
* const { checkout, isCheckingOut } = useRecur();
|
|
160
|
+
*
|
|
161
|
+
* return (
|
|
162
|
+
* <button
|
|
163
|
+
* onClick={() => checkout({ planId: 'pro' })}
|
|
164
|
+
* disabled={isCheckingOut}
|
|
165
|
+
* >
|
|
166
|
+
* {isCheckingOut ? 'Processing...' : 'Subscribe'}
|
|
167
|
+
* </button>
|
|
168
|
+
* );
|
|
169
|
+
* }
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* @throws {Error} If used outside of RecurProvider
|
|
173
|
+
*/
|
|
174
|
+
declare function useRecur(): RecurContextValue;
|
|
175
|
+
|
|
176
|
+
export { type CheckoutError, type CheckoutOptions, type CheckoutResult, type RecurConfig, type RecurContextValue, RecurProvider, type RecurProviderProps, type SubscriptionResult, useRecur };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createContext, useState, useCallback, useMemo, useContext } from 'react';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/context.tsx
|
|
5
|
+
var RecurContext = createContext(null);
|
|
6
|
+
function RecurProvider({ children, config: initialConfig = {} }) {
|
|
7
|
+
const [config, setConfig] = useState({
|
|
8
|
+
redirectMode: "redirect",
|
|
9
|
+
...initialConfig
|
|
10
|
+
});
|
|
11
|
+
const [isCheckingOut, setIsCheckingOut] = useState(false);
|
|
12
|
+
const updateConfig = useCallback((newConfig) => {
|
|
13
|
+
setConfig((prev) => ({ ...prev, ...newConfig }));
|
|
14
|
+
}, []);
|
|
15
|
+
const checkout = useCallback(
|
|
16
|
+
async (options) => {
|
|
17
|
+
try {
|
|
18
|
+
setIsCheckingOut(true);
|
|
19
|
+
if (!options.planId) {
|
|
20
|
+
throw new Error("planId is required");
|
|
21
|
+
}
|
|
22
|
+
const organizationId = options.organizationId || config.organizationId;
|
|
23
|
+
if (!organizationId) {
|
|
24
|
+
throw new Error("organizationId is required. Provide it in RecurProvider config or checkout options.");
|
|
25
|
+
}
|
|
26
|
+
const baseUrl = config.baseUrl || (typeof window !== "undefined" ? window.location.origin : "");
|
|
27
|
+
const response = await fetch(`${baseUrl}/api/subscriptions`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
"Content-Type": "application/json"
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
organizationId,
|
|
34
|
+
planId: options.planId,
|
|
35
|
+
customerName: options.customerName,
|
|
36
|
+
customerEmail: options.customerEmail,
|
|
37
|
+
customerPhone: options.customerPhone
|
|
38
|
+
})
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const errorData = await response.json().catch(() => ({}));
|
|
42
|
+
const error = {
|
|
43
|
+
code: errorData.error || "CHECKOUT_FAILED",
|
|
44
|
+
message: errorData.message || "Failed to initiate checkout",
|
|
45
|
+
details: errorData
|
|
46
|
+
};
|
|
47
|
+
options.onError?.(error);
|
|
48
|
+
throw new Error(error.message);
|
|
49
|
+
}
|
|
50
|
+
const result = await response.json();
|
|
51
|
+
options.onSuccess?.(result);
|
|
52
|
+
if (config.redirectMode === "popup") {
|
|
53
|
+
const popup = window.open(
|
|
54
|
+
result.paymentUrl,
|
|
55
|
+
"recurCheckout",
|
|
56
|
+
"width=600,height=700,scrollbars=yes,resizable=yes"
|
|
57
|
+
);
|
|
58
|
+
if (!popup) {
|
|
59
|
+
throw new Error("Failed to open popup. Please allow popups for this site.");
|
|
60
|
+
}
|
|
61
|
+
const pollInterval = setInterval(() => {
|
|
62
|
+
if (popup.closed) {
|
|
63
|
+
clearInterval(pollInterval);
|
|
64
|
+
setIsCheckingOut(false);
|
|
65
|
+
if (options.onPaymentComplete) {
|
|
66
|
+
options.onPaymentComplete(result.subscription);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}, 500);
|
|
70
|
+
} else {
|
|
71
|
+
window.location.href = result.paymentUrl;
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const checkoutError = {
|
|
75
|
+
code: "CHECKOUT_ERROR",
|
|
76
|
+
message: error instanceof Error ? error.message : "An unknown error occurred"
|
|
77
|
+
};
|
|
78
|
+
options.onError?.(checkoutError);
|
|
79
|
+
setIsCheckingOut(false);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
[config]
|
|
84
|
+
);
|
|
85
|
+
const value = useMemo(
|
|
86
|
+
() => ({
|
|
87
|
+
config,
|
|
88
|
+
checkout,
|
|
89
|
+
isCheckingOut,
|
|
90
|
+
updateConfig
|
|
91
|
+
}),
|
|
92
|
+
[config, checkout, isCheckingOut, updateConfig]
|
|
93
|
+
);
|
|
94
|
+
return /* @__PURE__ */ jsx(RecurContext.Provider, { value, children });
|
|
95
|
+
}
|
|
96
|
+
function useRecur() {
|
|
97
|
+
const context = useContext(RecurContext);
|
|
98
|
+
if (!context) {
|
|
99
|
+
throw new Error("useRecur must be used within a RecurProvider");
|
|
100
|
+
}
|
|
101
|
+
return context;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { RecurProvider, useRecur };
|
|
105
|
+
//# sourceMappingURL=index.js.map
|
|
106
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context.tsx","../src/use-recur.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["'use client';\n\nimport React, { createContext, useCallback, useState, useMemo } from 'react';\nimport type {\n RecurConfig,\n RecurContextValue,\n CheckoutOptions,\n CheckoutResult,\n CheckoutError,\n} from './types.js';\n\nconst RecurContext = createContext<RecurContextValue | null>(null);\n\nexport interface RecurProviderProps {\n children: React.ReactNode;\n config?: RecurConfig;\n}\n\n/**\n * RecurProvider\n *\n * Provides Recur checkout functionality to your React application\n *\n * @example\n * ```tsx\n * <RecurProvider config={{ organizationId: 'org_xxx' }}>\n * <App />\n * </RecurProvider>\n * ```\n */\nexport function RecurProvider({ children, config: initialConfig = {} }: RecurProviderProps) {\n const [config, setConfig] = useState<RecurConfig>({\n redirectMode: 'redirect',\n ...initialConfig,\n });\n\n const [isCheckingOut, setIsCheckingOut] = useState(false);\n\n const updateConfig = useCallback((newConfig: Partial<RecurConfig>) => {\n setConfig((prev) => ({ ...prev, ...newConfig }));\n }, []);\n\n const checkout = useCallback(\n async (options: CheckoutOptions) => {\n try {\n setIsCheckingOut(true);\n\n // Validate required fields\n if (!options.planId) {\n throw new Error('planId is required');\n }\n\n // Determine organization ID\n const organizationId = options.organizationId || config.organizationId;\n if (!organizationId) {\n throw new Error('organizationId is required. Provide it in RecurProvider config or checkout options.');\n }\n\n // Determine base URL\n const baseUrl = config.baseUrl || (typeof window !== 'undefined' ? window.location.origin : '');\n\n // Call the subscription API\n const response = await fetch(`${baseUrl}/api/subscriptions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n organizationId,\n planId: options.planId,\n customerName: options.customerName,\n customerEmail: options.customerEmail,\n customerPhone: options.customerPhone,\n }),\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const error: CheckoutError = {\n code: errorData.error || 'CHECKOUT_FAILED',\n message: errorData.message || 'Failed to initiate checkout',\n details: errorData,\n };\n\n options.onError?.(error);\n throw new Error(error.message);\n }\n\n const result: CheckoutResult = await response.json();\n\n // Call success callback\n options.onSuccess?.(result);\n\n // Handle redirect based on mode\n if (config.redirectMode === 'popup') {\n // Open payment in popup window\n const popup = window.open(\n result.paymentUrl,\n 'recurCheckout',\n 'width=600,height=700,scrollbars=yes,resizable=yes'\n );\n\n if (!popup) {\n throw new Error('Failed to open popup. Please allow popups for this site.');\n }\n\n // Poll for popup close\n const pollInterval = setInterval(() => {\n if (popup.closed) {\n clearInterval(pollInterval);\n setIsCheckingOut(false);\n\n // Check if payment was completed\n // You might want to implement a message listener here\n // or poll your API to check subscription status\n if (options.onPaymentComplete) {\n // Note: result.subscription may not have all fields for SubscriptionResult\n // You may need to fetch full subscription details from API\n options.onPaymentComplete(result.subscription as any);\n }\n }\n }, 500);\n } else {\n // Full page redirect\n window.location.href = result.paymentUrl;\n }\n } catch (error) {\n const checkoutError: CheckoutError = {\n code: 'CHECKOUT_ERROR',\n message: error instanceof Error ? error.message : 'An unknown error occurred',\n };\n\n options.onError?.(checkoutError);\n setIsCheckingOut(false);\n throw error;\n }\n },\n [config]\n );\n\n const value = useMemo(\n () => ({\n config,\n checkout,\n isCheckingOut,\n updateConfig,\n }),\n [config, checkout, isCheckingOut, updateConfig]\n );\n\n return <RecurContext.Provider value={value}>{children}</RecurContext.Provider>;\n}\n\nexport { RecurContext };\n","'use client';\n\nimport { useContext } from 'react';\nimport { RecurContext } from './context.js';\nimport type { RecurContextValue } from './types.js';\n\n/**\n * useRecur Hook\n *\n * Access Recur checkout functionality from within your components\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { checkout, isCheckingOut } = useRecur();\n *\n * return (\n * <button\n * onClick={() => checkout({ planId: 'pro' })}\n * disabled={isCheckingOut}\n * >\n * {isCheckingOut ? 'Processing...' : 'Subscribe'}\n * </button>\n * );\n * }\n * ```\n *\n * @throws {Error} If used outside of RecurProvider\n */\nexport function useRecur(): RecurContextValue {\n const context = useContext(RecurContext);\n\n if (!context) {\n throw new Error('useRecur must be used within a RecurProvider');\n }\n\n return context;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "recur-tw",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "React SDK for embedding subscription checkout flows (Taiwan / PAYUNi)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"author": "Recur",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"react",
|
|
11
|
+
"subscription",
|
|
12
|
+
"checkout",
|
|
13
|
+
"payment",
|
|
14
|
+
"payuni",
|
|
15
|
+
"recurring-billing",
|
|
16
|
+
"taiwan",
|
|
17
|
+
"繁體中文"
|
|
18
|
+
],
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/your-org/recur-sdk.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/your-org/recur-sdk/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/your-org/recur-sdk#readme",
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"dev": "tsup --watch",
|
|
30
|
+
"lint": "eslint . --max-warnings 0",
|
|
31
|
+
"type-check": "tsc --noEmit"
|
|
32
|
+
},
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"module": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"import": "./dist/index.js",
|
|
40
|
+
"require": "./dist/index.cjs"
|
|
41
|
+
},
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist",
|
|
46
|
+
"README.md",
|
|
47
|
+
"LICENSE"
|
|
48
|
+
],
|
|
49
|
+
"sideEffects": false,
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"react": ">=18.0.0",
|
|
52
|
+
"react-dom": ">=18.0.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^20.19.9",
|
|
56
|
+
"@types/react": "^19.1.9",
|
|
57
|
+
"@types/react-dom": "^19.1.7",
|
|
58
|
+
"@workspace/eslint-config": "workspace:*",
|
|
59
|
+
"@workspace/typescript-config": "workspace:*",
|
|
60
|
+
"eslint": "^9.32.0",
|
|
61
|
+
"react": "^19.1.1",
|
|
62
|
+
"react-dom": "^19.1.1",
|
|
63
|
+
"tsup": "^8.3.5",
|
|
64
|
+
"typescript": "^5.9.2"
|
|
65
|
+
}
|
|
66
|
+
}
|