recur-tw 0.0.1 → 0.0.2

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/dist/index.cjs CHANGED
@@ -1,78 +1,601 @@
1
1
  'use strict';
2
2
 
3
- var react = require('react');
3
+ var React = require('react');
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
5
 
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ var React__default = /*#__PURE__*/_interopDefault(React);
9
+
6
10
  // src/context.tsx
7
- var RecurContext = react.createContext(null);
11
+
12
+ // src/payuni-loader.ts
13
+ var PAYUNI_SDK_URL = "https://vendor.payuni.com.tw/sdk/uni-payment.js";
14
+ var PAYUNI_SANDBOX_SDK_URL = "https://sandbox-vendor.payuni.com.tw/sdk/uni-payment.js";
15
+ var isLoaded = false;
16
+ var isLoading = false;
17
+ var loadPromise = null;
18
+ function createMockUniPayment() {
19
+ return {
20
+ createSession: (token, options) => {
21
+ console.log("[Mock PAYUNi SDK] Creating session with token:", token.substring(0, 20) + "...");
22
+ console.log("[Mock PAYUNi SDK] Options:", options);
23
+ const { elements } = options;
24
+ const cardNoEl = document.getElementById(elements.CardNo);
25
+ const cardExpEl = document.getElementById(elements.CardExp);
26
+ const cardCvcEl = document.getElementById(elements.CardCvc);
27
+ if (cardNoEl) {
28
+ cardNoEl.innerHTML = '<div style="border: 1px solid #e5e7eb; padding: 8px; border-radius: 4px; background: white; font-size: 14px; color: #6b7280;">4242 4242 4242 4242 (Mock)</div>';
29
+ }
30
+ if (cardExpEl) {
31
+ cardExpEl.innerHTML = '<div style="border: 1px solid #e5e7eb; padding: 8px; border-radius: 4px; background: white; font-size: 14px; color: #6b7280;">12 / 25 (Mock)</div>';
32
+ }
33
+ if (cardCvcEl) {
34
+ cardCvcEl.innerHTML = '<div style="border: 1px solid #e5e7eb; padding: 8px; border-radius: 4px; background: white; font-size: 14px; color: #6b7280;">123 (Mock)</div>';
35
+ }
36
+ return {
37
+ start: async () => {
38
+ console.log("[Mock PAYUNi SDK] \u2713 Started successfully");
39
+ await new Promise((resolve) => setTimeout(resolve, 500));
40
+ },
41
+ getTradeResult: async () => {
42
+ console.log("[Mock PAYUNi SDK] \u2713 Getting trade result");
43
+ await new Promise((resolve) => setTimeout(resolve, 800));
44
+ return {
45
+ Status: "SUCCESS",
46
+ Message: "Mock payment successful",
47
+ TradeNo: `MOCK${Date.now()}`,
48
+ AuthCode: "123456"
49
+ };
50
+ }
51
+ };
52
+ }
53
+ };
54
+ }
55
+ async function loadPayUniSDK(isSandbox = false, mockMode = false) {
56
+ if (mockMode) {
57
+ console.log("[Recur SDK] \u{1F3AD} Mock mode enabled - using mock PAYUNi SDK");
58
+ if (typeof window !== "undefined") {
59
+ window.UniPayment = createMockUniPayment();
60
+ }
61
+ isLoaded = true;
62
+ return Promise.resolve();
63
+ }
64
+ if (isLoaded && window.UniPayment) {
65
+ return Promise.resolve();
66
+ }
67
+ if (isLoading && loadPromise) {
68
+ return loadPromise;
69
+ }
70
+ isLoading = true;
71
+ loadPromise = new Promise((resolve, reject) => {
72
+ const script = document.createElement("script");
73
+ script.src = isSandbox ? PAYUNI_SANDBOX_SDK_URL : PAYUNI_SDK_URL;
74
+ script.async = true;
75
+ script.onload = () => {
76
+ isLoaded = true;
77
+ isLoading = false;
78
+ resolve();
79
+ };
80
+ script.onerror = () => {
81
+ isLoading = false;
82
+ loadPromise = null;
83
+ reject(new Error("Failed to load PAYUNi SDK"));
84
+ };
85
+ document.head.appendChild(script);
86
+ });
87
+ return loadPromise;
88
+ }
89
+ var RecurContext = React.createContext(null);
8
90
  function RecurProvider({ children, config: initialConfig = {} }) {
9
- const [config, setConfig] = react.useState({
10
- redirectMode: "redirect",
91
+ const [config, setConfig] = React.useState({
92
+ checkoutMode: "embedded",
11
93
  ...initialConfig
12
94
  });
13
- const [isCheckingOut, setIsCheckingOut] = react.useState(false);
14
- const updateConfig = react.useCallback((newConfig) => {
95
+ const [isCheckingOut, setIsCheckingOut] = React.useState(false);
96
+ React__default.default.useEffect(() => {
97
+ setConfig({
98
+ checkoutMode: "embedded",
99
+ ...initialConfig
100
+ });
101
+ }, [initialConfig.checkoutMode, initialConfig.publishableKey, initialConfig.containerElementId]);
102
+ React__default.default.useEffect(() => {
103
+ setIsCheckingOut(false);
104
+ }, [config.checkoutMode]);
105
+ const updateConfig = React.useCallback((newConfig) => {
15
106
  setConfig((prev) => ({ ...prev, ...newConfig }));
16
107
  }, []);
17
- const checkout = react.useCallback(
108
+ const checkout = React.useCallback(
18
109
  async (options) => {
110
+ let modalOverlay = null;
19
111
  try {
112
+ console.log("[Recur SDK] Starting checkout flow...", {
113
+ planId: options.planId,
114
+ checkoutMode: config.checkoutMode,
115
+ containerElementId: config.containerElementId
116
+ });
20
117
  setIsCheckingOut(true);
21
118
  if (!options.planId) {
22
119
  throw new Error("planId is required");
23
120
  }
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.");
121
+ if (!config.publishableKey) {
122
+ throw new Error("publishableKey is required");
123
+ }
124
+ if (!options.customerEmail || !options.customerName) {
125
+ throw new Error("customerEmail and customerName are required");
27
126
  }
28
127
  const baseUrl = config.baseUrl || (typeof window !== "undefined" ? window.location.origin : "");
29
- const response = await fetch(`${baseUrl}/api/subscriptions`, {
128
+ console.log("[Recur SDK] Base URL:", baseUrl);
129
+ const headers = {
130
+ "Content-Type": "application/json",
131
+ "X-Recur-Publishable-Key": config.publishableKey
132
+ };
133
+ let modalContent = null;
134
+ let loadingContainer = null;
135
+ let embeddedContainer = null;
136
+ if (config.checkoutMode === "modal") {
137
+ console.log("[Recur SDK] Mode is modal, creating modal with loading state...");
138
+ modalOverlay = document.createElement("div");
139
+ modalOverlay.id = "recur-modal-overlay";
140
+ modalOverlay.className = "recur-sdk__modal-overlay";
141
+ modalOverlay.style.cssText = `
142
+ position: fixed;
143
+ top: 0;
144
+ left: 0;
145
+ width: 100%;
146
+ height: 100%;
147
+ background: rgba(0, 0, 0, 0.5);
148
+ display: flex;
149
+ align-items: center;
150
+ justify-content: center;
151
+ z-index: 9999;
152
+ `;
153
+ modalContent = document.createElement("div");
154
+ modalContent.className = "recur-sdk__modal-content";
155
+ modalContent.style.cssText = `
156
+ background: white;
157
+ padding: 32px;
158
+ border-radius: 12px;
159
+ max-width: 500px;
160
+ width: 90%;
161
+ max-height: 90vh;
162
+ overflow-y: auto;
163
+ position: relative;
164
+ `;
165
+ const closeButton = document.createElement("button");
166
+ closeButton.className = "recur-sdk__close-button";
167
+ closeButton.innerHTML = "\u2715";
168
+ closeButton.style.cssText = `
169
+ position: absolute;
170
+ top: 16px;
171
+ right: 16px;
172
+ background: none;
173
+ border: none;
174
+ font-size: 24px;
175
+ cursor: pointer;
176
+ color: #666;
177
+ padding: 0;
178
+ width: 32px;
179
+ height: 32px;
180
+ display: flex;
181
+ align-items: center;
182
+ justify-content: center;
183
+ `;
184
+ closeButton.onclick = () => {
185
+ modalOverlay?.remove();
186
+ setIsCheckingOut(false);
187
+ options.onPaymentCancel?.();
188
+ };
189
+ const title = document.createElement("h2");
190
+ title.className = "recur-sdk__title";
191
+ title.textContent = "\u8A02\u95B1\u4ED8\u6B3E";
192
+ title.style.cssText = "margin: 0 0 24px 0; font-size: 24px; font-weight: 600;";
193
+ loadingContainer = document.createElement("div");
194
+ loadingContainer.id = "recur-modal-loading";
195
+ loadingContainer.className = "recur-sdk__loading-container";
196
+ loadingContainer.style.cssText = "text-align: center; padding: 40px 0;";
197
+ loadingContainer.innerHTML = `
198
+ <style>
199
+ @keyframes recur-sdk-spin {
200
+ 0% { transform: rotate(0deg); }
201
+ 100% { transform: rotate(360deg); }
202
+ }
203
+ .recur-sdk__spinner {
204
+ border: 3px solid #e5e7eb;
205
+ border-top: 3px solid #3b82f6;
206
+ border-radius: 50%;
207
+ width: 40px;
208
+ height: 40px;
209
+ animation: recur-sdk-spin 1s linear infinite;
210
+ margin: 0 auto 16px;
211
+ }
212
+ .recur-sdk__loading-text {
213
+ color: #6b7280;
214
+ font-size: 14px;
215
+ margin: 0;
216
+ }
217
+ </style>
218
+ <div class="recur-sdk__spinner"></div>
219
+ <p class="recur-sdk__loading-text">\u6B63\u5728\u8655\u7406\u8A02\u95B1...</p>
220
+ `;
221
+ modalContent.appendChild(closeButton);
222
+ modalContent.appendChild(title);
223
+ modalContent.appendChild(loadingContainer);
224
+ modalOverlay.appendChild(modalContent);
225
+ document.body.appendChild(modalOverlay);
226
+ console.log("[Recur SDK] Modal created with loading state");
227
+ } else if (config.checkoutMode === "embedded") {
228
+ console.log("[Recur SDK] Mode is embedded, checking for container...");
229
+ if (!config.containerElementId) {
230
+ console.warn("[Recur SDK] No containerElementId provided for embedded mode");
231
+ } else {
232
+ embeddedContainer = document.getElementById(config.containerElementId);
233
+ if (!embeddedContainer) {
234
+ console.log("[Recur SDK] Container not found, polling for container...");
235
+ for (let i = 0; i < 10; i++) {
236
+ await new Promise((resolve) => setTimeout(resolve, 50));
237
+ embeddedContainer = document.getElementById(config.containerElementId);
238
+ if (embeddedContainer) {
239
+ console.log("[Recur SDK] Container found after polling");
240
+ break;
241
+ }
242
+ }
243
+ }
244
+ if (embeddedContainer) {
245
+ console.log("[Recur SDK] Container found, showing loading state...");
246
+ loadingContainer = document.createElement("div");
247
+ loadingContainer.id = "recur-embedded-loading";
248
+ loadingContainer.className = "recur-sdk__loading-container";
249
+ loadingContainer.style.cssText = "text-align: center; padding: 40px 0;";
250
+ loadingContainer.innerHTML = `
251
+ <style>
252
+ @keyframes recur-sdk-spin {
253
+ 0% { transform: rotate(0deg); }
254
+ 100% { transform: rotate(360deg); }
255
+ }
256
+ .recur-sdk__spinner {
257
+ border: 3px solid #e5e7eb;
258
+ border-top: 3px solid #3b82f6;
259
+ border-radius: 50%;
260
+ width: 40px;
261
+ height: 40px;
262
+ animation: recur-sdk-spin 1s linear infinite;
263
+ margin: 0 auto 16px;
264
+ }
265
+ .recur-sdk__loading-text {
266
+ color: #6b7280;
267
+ font-size: 14px;
268
+ margin: 0;
269
+ }
270
+ </style>
271
+ <div class="recur-sdk__spinner"></div>
272
+ <p class="recur-sdk__loading-text">\u6B63\u5728\u8655\u7406\u8A02\u95B1...</p>
273
+ `;
274
+ embeddedContainer.appendChild(loadingContainer);
275
+ } else {
276
+ console.log("[Recur SDK] Container not found after polling, will continue without early loading state");
277
+ }
278
+ }
279
+ }
280
+ console.log("[Recur SDK] Step 1: Creating subscription...");
281
+ const subscriptionResponse = await fetch(`${baseUrl}/api/v1/subscriptions`, {
30
282
  method: "POST",
31
- headers: {
32
- "Content-Type": "application/json"
33
- },
283
+ headers,
34
284
  body: JSON.stringify({
35
- organizationId,
36
285
  planId: options.planId,
37
286
  customerName: options.customerName,
38
287
  customerEmail: options.customerEmail,
39
288
  customerPhone: options.customerPhone
40
289
  })
41
290
  });
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.");
291
+ if (!subscriptionResponse.ok) {
292
+ const errorData = await subscriptionResponse.json().catch(() => ({}));
293
+ console.error("[Recur SDK] Failed to create subscription:", errorData);
294
+ const errorMessage = errorData.details || errorData.error || "Failed to create subscription";
295
+ throw new Error(errorMessage);
296
+ }
297
+ const subscriptionResult = await subscriptionResponse.json();
298
+ console.log("[Recur SDK] Subscription created successfully:", subscriptionResult);
299
+ options.onSuccess?.(subscriptionResult);
300
+ console.log("[Recur SDK] Step 2: Getting SDK token...");
301
+ const tokenResponse = await fetch(`${baseUrl}/api/v1/subscriptions/sdk-token`, {
302
+ method: "POST",
303
+ headers,
304
+ body: JSON.stringify({
305
+ subscriberId: subscriptionResult.subscriber.id,
306
+ iframeDomain: window.location.origin
307
+ })
308
+ });
309
+ if (!tokenResponse.ok) {
310
+ const errorData = await tokenResponse.json().catch(() => ({}));
311
+ console.error("[Recur SDK] Failed to get SDK token:", errorData);
312
+ throw new Error("Failed to get SDK token");
313
+ }
314
+ const tokenResult = await tokenResponse.json();
315
+ console.log("[Recur SDK] SDK token received:", {
316
+ environment: tokenResult.environment,
317
+ creditToken: tokenResult.creditToken,
318
+ tokenExpired: tokenResult.tokenExpired
319
+ });
320
+ console.log("[Recur SDK] Step 3: Loading PAYUNi SDK...");
321
+ const isSandbox = tokenResult.environment === "SANDBOX";
322
+ await loadPayUniSDK(isSandbox, config.mockMode);
323
+ console.log("[Recur SDK] PAYUNi SDK loaded successfully");
324
+ if (!window.UniPayment) {
325
+ console.error("[Recur SDK] window.UniPayment is not available after loading");
326
+ throw new Error("PAYUNi SDK failed to load");
327
+ }
328
+ console.log("[Recur SDK] Step 4: Preparing container for checkout mode:", config.checkoutMode);
329
+ console.log("[Recur SDK] Initial containerElementId from config:", config.containerElementId);
330
+ let containerElementId = config.containerElementId;
331
+ if (config.checkoutMode === "modal") {
332
+ console.log("[Recur SDK] Mode is modal, updating modal with payment form...");
333
+ if (!loadingContainer || !modalContent) {
334
+ throw new Error("Modal components not initialized");
62
335
  }
63
- const pollInterval = setInterval(() => {
64
- if (popup.closed) {
65
- clearInterval(pollInterval);
66
- setIsCheckingOut(false);
67
- if (options.onPaymentComplete) {
68
- options.onPaymentComplete(result.subscription);
69
- }
336
+ loadingContainer.remove();
337
+ const formContainer = document.createElement("div");
338
+ containerElementId = "recur-modal-payment-container";
339
+ formContainer.id = containerElementId;
340
+ modalContent.appendChild(formContainer);
341
+ console.log("[Recur SDK] Modal updated with container ID:", containerElementId);
342
+ } else if (config.checkoutMode === "embedded") {
343
+ console.log("[Recur SDK] Mode is embedded, preparing container...");
344
+ if (!containerElementId) {
345
+ throw new Error("containerElementId is required for embedded mode");
346
+ }
347
+ if (loadingContainer && embeddedContainer) {
348
+ console.log("[Recur SDK] Removing loading spinner from embedded container");
349
+ loadingContainer.remove();
350
+ }
351
+ }
352
+ console.log("[Recur SDK] Step 5: Initializing PAYUNi form...");
353
+ console.log("[Recur SDK] Available UniPayment methods:", Object.keys(window.UniPayment || {}));
354
+ if (!containerElementId) {
355
+ throw new Error("containerElementId is required");
356
+ }
357
+ const uniPayment = window.UniPayment.createSession(tokenResult.sdkToken, {
358
+ env: tokenResult.environment === "SANDBOX" ? "S" : "P",
359
+ elements: {
360
+ CardNo: `${containerElementId}-card-no`,
361
+ CardExp: `${containerElementId}-card-exp`,
362
+ CardCvc: `${containerElementId}-card-cvc`
363
+ }
364
+ });
365
+ console.log("[Recur SDK] PAYUNi form initialized");
366
+ console.log("[Recur SDK] Step 6: Waiting for container element...");
367
+ let container = document.getElementById(containerElementId);
368
+ if (!container && config.checkoutMode === "embedded") {
369
+ console.log("[Recur SDK] Container not found, waiting for React to render...");
370
+ for (let i = 0; i < 20; i++) {
371
+ await new Promise((resolve) => setTimeout(resolve, 100));
372
+ container = document.getElementById(containerElementId);
373
+ if (container) {
374
+ console.log("[Recur SDK] Container found after waiting");
375
+ break;
70
376
  }
71
- }, 500);
72
- } else {
73
- window.location.href = result.paymentUrl;
377
+ }
378
+ }
379
+ if (!container) {
380
+ throw new Error(`Container element '${containerElementId}' not found`);
74
381
  }
382
+ console.log("[Recur SDK] Creating loading skeleton...");
383
+ container.innerHTML = `
384
+ <style>
385
+ /* Recur SDK Styles - CSS Namespace with BEM */
386
+
387
+ /* Animations */
388
+ @keyframes recur-sdk-pulse {
389
+ 0%, 100% { opacity: 1; }
390
+ 50% { opacity: 0.5; }
391
+ }
392
+
393
+ /* Skeleton Loading State */
394
+ .recur-sdk__skeleton {
395
+ animation: recur-sdk-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
396
+ background-color: #e5e7eb;
397
+ border-radius: 6px;
398
+ }
399
+
400
+ /* Form States */
401
+ .recur-sdk__form--hidden {
402
+ display: none;
403
+ }
404
+
405
+ /* Form Layout */
406
+ .recur-sdk__form-field {
407
+ margin-bottom: 16px;
408
+ }
409
+
410
+ .recur-sdk__form-row {
411
+ display: grid;
412
+ grid-template-columns: 1fr 1fr;
413
+ gap: 16px;
414
+ margin-bottom: 16px;
415
+ }
416
+
417
+ /* Labels and Text */
418
+ .recur-sdk__label {
419
+ display: block;
420
+ margin-bottom: 8px;
421
+ font-weight: 500;
422
+ color: #374151;
423
+ }
424
+
425
+ .recur-sdk__hint-text {
426
+ margin-top: 4px;
427
+ font-size: 12px;
428
+ color: #6b7280;
429
+ }
430
+
431
+ /* Input Container (for PAYUNi iframes) */
432
+ .recur-sdk__input-container {
433
+ /* Container styles applied via ID selectors below for PAYUNi compatibility */
434
+ }
435
+
436
+ /* Button */
437
+ .recur-sdk__button {
438
+ width: 100%;
439
+ padding: 12px;
440
+ background: #3b82f6;
441
+ color: white;
442
+ border: none;
443
+ border-radius: 6px;
444
+ font-weight: 600;
445
+ cursor: pointer;
446
+ transition: background-color 0.15s ease-in-out;
447
+ }
448
+
449
+ .recur-sdk__button:hover {
450
+ background: #2563eb;
451
+ }
452
+
453
+ .recur-sdk__button:disabled {
454
+ opacity: 0.6;
455
+ cursor: not-allowed;
456
+ }
457
+
458
+ /* PAYUNi SDK \u81EA\u8A02\u6A23\u5F0F - \u8207 shadcn input focus \u6A23\u5F0F\u4E00\u81F4 */
459
+ /* Note: .form-input-focus is applied by PAYUNi SDK, not by our code */
460
+ .form-input-focus {
461
+ border-color: hsl(215 16% 47%) !important;
462
+ outline: 0 !important;
463
+ box-shadow: 0 0 0 3px hsl(215 16% 47% / 0.5) !important;
464
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out !important;
465
+ }
466
+
467
+ /* \u78BA\u4FDD PAYUNi iframe \u5BB9\u5668\u9AD8\u5EA6\u4E00\u81F4 */
468
+ /* Note: These IDs are required by PAYUNi SDK and cannot be changed */
469
+ #${containerElementId}-card-no,
470
+ #${containerElementId}-card-exp,
471
+ #${containerElementId}-card-cvc {
472
+ height: 36px !important;
473
+ }
474
+ </style>
475
+
476
+ <!-- Skeleton Loading State -->
477
+ <div id="${containerElementId}-skeleton" class="recur-sdk__skeleton-container">
478
+ <div class="recur-sdk__form-field">
479
+ <label class="recur-sdk__label">\u5361\u865F</label>
480
+ <div class="recur-sdk__skeleton" style="height: 36px;"></div>
481
+ <p class="recur-sdk__hint-text">\u8ACB\u8F38\u5165\u60A8\u7684 16 \u4F4D\u6578\u5361\u865F</p>
482
+ </div>
483
+ <div class="recur-sdk__form-row">
484
+ <div class="recur-sdk__form-field">
485
+ <label class="recur-sdk__label">\u6709\u6548\u671F\u9650</label>
486
+ <div class="recur-sdk__skeleton" style="height: 36px;"></div>
487
+ </div>
488
+ <div class="recur-sdk__form-field">
489
+ <label class="recur-sdk__label">\u5B89\u5168\u78BC</label>
490
+ <div class="recur-sdk__skeleton" style="height: 36px;"></div>
491
+ </div>
492
+ </div>
493
+ <!-- Show button during loading to prevent layout shift -->
494
+ <button class="recur-sdk__button" disabled>
495
+ \u8F09\u5165\u4E2D...
496
+ </button>
497
+ </div>
498
+
499
+ <!-- Actual Payment Form (hidden initially) -->
500
+ <div id="${containerElementId}-form" class="recur-sdk__form recur-sdk__form--hidden">
501
+ <div class="recur-sdk__form-field">
502
+ <label class="recur-sdk__label">\u5361\u865F</label>
503
+ <div id="${containerElementId}-card-no" class="recur-sdk__input-container"></div>
504
+ <p class="recur-sdk__hint-text">\u8ACB\u8F38\u5165\u60A8\u7684 16 \u4F4D\u6578\u5361\u865F</p>
505
+ </div>
506
+ <div class="recur-sdk__form-row">
507
+ <div class="recur-sdk__form-field">
508
+ <label class="recur-sdk__label">\u6709\u6548\u671F\u9650</label>
509
+ <div id="${containerElementId}-card-exp" class="recur-sdk__input-container"></div>
510
+ </div>
511
+ <div class="recur-sdk__form-field">
512
+ <label class="recur-sdk__label">\u5B89\u5168\u78BC</label>
513
+ <div id="${containerElementId}-card-cvc" class="recur-sdk__input-container"></div>
514
+ </div>
515
+ </div>
516
+ <button id="${containerElementId}-submit-btn" class="recur-sdk__button">
517
+ \u78BA\u8A8D\u4ED8\u6B3E
518
+ </button>
519
+ </div>
520
+ `;
521
+ console.log("[Recur SDK] Starting PAYUNi SDK...");
522
+ await uniPayment.start();
523
+ console.log("[Recur SDK] PAYUNi SDK started successfully");
524
+ console.log("[Recur SDK] Showing actual form...");
525
+ const skeleton = document.getElementById(`${containerElementId}-skeleton`);
526
+ const form = document.getElementById(`${containerElementId}-form`);
527
+ if (skeleton && form) {
528
+ skeleton.style.display = "none";
529
+ form.classList.remove("recur-sdk__form--hidden");
530
+ }
531
+ console.log("[Recur SDK] Step 7: Setting up submit button handler...");
532
+ const submitBtn = document.getElementById(`${containerElementId}-submit-btn`);
533
+ if (!submitBtn) {
534
+ throw new Error("Submit button not found");
535
+ }
536
+ submitBtn.addEventListener("click", async () => {
537
+ console.log("[Recur SDK] Submit button clicked");
538
+ submitBtn.setAttribute("disabled", "true");
539
+ submitBtn.textContent = "\u8655\u7406\u4E2D...";
540
+ try {
541
+ console.log("[Recur SDK] Getting trade result from PAYUNi...");
542
+ const tradeResult = await uniPayment.getTradeResult();
543
+ console.log("[Recur SDK] Trade result received:", tradeResult);
544
+ console.log("[Recur SDK] Step 8: Completing subscription...");
545
+ const completeResponse = await fetch(
546
+ `${baseUrl}/api/v1/subscriptions/${subscriptionResult.subscription.id}/complete`,
547
+ {
548
+ method: "POST",
549
+ headers,
550
+ body: JSON.stringify({
551
+ sdkToken: tokenResult.sdkToken,
552
+ timestamp: tokenResult.timestamp,
553
+ creditToken: tokenResult.creditToken
554
+ })
555
+ }
556
+ );
557
+ if (!completeResponse.ok) {
558
+ const errorData = await completeResponse.json().catch(() => ({}));
559
+ console.error("[Recur SDK] Failed to complete subscription:", errorData);
560
+ throw new Error("Failed to complete subscription");
561
+ }
562
+ const completeResult = await completeResponse.json();
563
+ console.log("[Recur SDK] Subscription completed successfully:", completeResult);
564
+ console.log("[Recur SDK] Calling onPaymentComplete callback...");
565
+ if (options.onPaymentComplete) {
566
+ options.onPaymentComplete({
567
+ id: completeResult.subscription.id,
568
+ status: completeResult.subscription.status,
569
+ planId: subscriptionResult.subscription.planId,
570
+ amount: subscriptionResult.subscription.amount,
571
+ billingPeriod: subscriptionResult.subscription.billingPeriod,
572
+ currentPeriodStart: (/* @__PURE__ */ new Date()).toISOString(),
573
+ currentPeriodEnd: (/* @__PURE__ */ new Date()).toISOString()
574
+ });
575
+ }
576
+ console.log("[Recur SDK] Checkout flow completed successfully!");
577
+ if (modalOverlay) {
578
+ modalOverlay.remove();
579
+ }
580
+ setIsCheckingOut(false);
581
+ } catch (error) {
582
+ console.error("[Recur SDK] Error in payment flow:", error);
583
+ const checkoutError = {
584
+ code: "PAYMENT_FAILED",
585
+ message: error instanceof Error ? error.message : "Payment failed"
586
+ };
587
+ options.onError?.(checkoutError);
588
+ submitBtn.removeAttribute("disabled");
589
+ submitBtn.textContent = "\u78BA\u8A8D\u4ED8\u6B3E";
590
+ setIsCheckingOut(false);
591
+ }
592
+ });
593
+ console.log("[Recur SDK] Checkout flow initialized, waiting for user input...");
75
594
  } catch (error) {
595
+ console.error("[Recur SDK] Checkout error:", error);
596
+ if (modalOverlay) {
597
+ modalOverlay.remove();
598
+ }
76
599
  const checkoutError = {
77
600
  code: "CHECKOUT_ERROR",
78
601
  message: error instanceof Error ? error.message : "An unknown error occurred"
@@ -84,19 +607,41 @@ function RecurProvider({ children, config: initialConfig = {} }) {
84
607
  },
85
608
  [config]
86
609
  );
87
- const value = react.useMemo(
610
+ const fetchPlans = React.useCallback(
611
+ async () => {
612
+ if (!config.publishableKey) {
613
+ throw new Error("publishableKey is required");
614
+ }
615
+ const baseUrl = config.baseUrl || (typeof window !== "undefined" ? window.location.origin : "");
616
+ const response = await fetch(`${baseUrl}/api/v1/plans`, {
617
+ method: "GET",
618
+ headers: {
619
+ "Content-Type": "application/json",
620
+ "X-Recur-Publishable-Key": config.publishableKey
621
+ }
622
+ });
623
+ if (!response.ok) {
624
+ const errorData = await response.json().catch(() => ({}));
625
+ throw new Error(errorData.error || "Failed to fetch plans");
626
+ }
627
+ return await response.json();
628
+ },
629
+ [config]
630
+ );
631
+ const value = React.useMemo(
88
632
  () => ({
89
633
  config,
90
634
  checkout,
635
+ fetchPlans,
91
636
  isCheckingOut,
92
637
  updateConfig
93
638
  }),
94
- [config, checkout, isCheckingOut, updateConfig]
639
+ [config, checkout, fetchPlans, isCheckingOut, updateConfig]
95
640
  );
96
641
  return /* @__PURE__ */ jsxRuntime.jsx(RecurContext.Provider, { value, children });
97
642
  }
98
643
  function useRecur() {
99
- const context = react.useContext(RecurContext);
644
+ const context = React.useContext(RecurContext);
100
645
  if (!context) {
101
646
  throw new Error("useRecur must be used within a RecurProvider");
102
647
  }