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/README.md +162 -7
- package/dist/index.cjs +593 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +103 -9
- package/dist/index.d.ts +103 -9
- package/dist/index.js +582 -41
- package/dist/index.js.map +1 -1
- package/dist/recur.umd.js +145 -0
- package/dist/recur.umd.js.map +1 -0
- package/package.json +20 -3
package/dist/index.cjs
CHANGED
|
@@ -1,78 +1,601 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
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
|
-
|
|
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] =
|
|
10
|
-
|
|
91
|
+
const [config, setConfig] = React.useState({
|
|
92
|
+
checkoutMode: "embedded",
|
|
11
93
|
...initialConfig
|
|
12
94
|
});
|
|
13
|
-
const [isCheckingOut, setIsCheckingOut] =
|
|
14
|
-
|
|
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 =
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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 (!
|
|
43
|
-
const errorData = await
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
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
|
|
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 =
|
|
644
|
+
const context = React.useContext(RecurContext);
|
|
100
645
|
if (!context) {
|
|
101
646
|
throw new Error("useRecur must be used within a RecurProvider");
|
|
102
647
|
}
|