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 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"]}
@@ -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 };
@@ -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
+ }