@youidian/sdk 3.0.0
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/client.cjs +263 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +103 -0
- package/dist/client.d.ts +103 -0
- package/dist/client.js +237 -0
- package/dist/client.js.map +1 -0
- package/dist/hosted-modal-BZmYmXTU.d.cts +20 -0
- package/dist/hosted-modal-BZmYmXTU.d.ts +20 -0
- package/dist/index.cjs +823 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +784 -0
- package/dist/index.js.map +1 -0
- package/dist/login.cjs +358 -0
- package/dist/login.cjs.map +1 -0
- package/dist/login.d.cts +62 -0
- package/dist/login.d.ts +62 -0
- package/dist/login.js +332 -0
- package/dist/login.js.map +1 -0
- package/dist/server.cjs +279 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +385 -0
- package/dist/server.d.ts +385 -0
- package/dist/server.js +246 -0
- package/dist/server.js.map +1 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// src/hosted-modal.ts
|
|
6
|
+
var SDK_SUPPORTED_LOCALES = [
|
|
7
|
+
"en",
|
|
8
|
+
"zh-CN",
|
|
9
|
+
"zh-Hant",
|
|
10
|
+
"fr",
|
|
11
|
+
"de",
|
|
12
|
+
"ja",
|
|
13
|
+
"es",
|
|
14
|
+
"ko",
|
|
15
|
+
"nl",
|
|
16
|
+
"it",
|
|
17
|
+
"pt"
|
|
18
|
+
];
|
|
19
|
+
var SDK_DEFAULT_LOCALE = "zh-CN";
|
|
20
|
+
var SDK_LOCALE_ALIASES = {
|
|
21
|
+
en: "en",
|
|
22
|
+
"en-us": "en",
|
|
23
|
+
"en-gb": "en",
|
|
24
|
+
zh: "zh-CN",
|
|
25
|
+
"zh-cn": "zh-CN",
|
|
26
|
+
"zh-tw": "zh-Hant",
|
|
27
|
+
"zh-hk": "zh-Hant",
|
|
28
|
+
"zh-hant": "zh-Hant",
|
|
29
|
+
"zh-hans": "zh-CN",
|
|
30
|
+
fr: "fr",
|
|
31
|
+
de: "de",
|
|
32
|
+
ja: "ja",
|
|
33
|
+
es: "es",
|
|
34
|
+
ko: "ko",
|
|
35
|
+
nl: "nl",
|
|
36
|
+
it: "it",
|
|
37
|
+
pt: "pt"
|
|
38
|
+
};
|
|
39
|
+
function matchSupportedBaseLocale(locale) {
|
|
40
|
+
const base = locale.toLowerCase().split("-")[0];
|
|
41
|
+
return SDK_SUPPORTED_LOCALES.find((item) => item.toLowerCase() === base);
|
|
42
|
+
}
|
|
43
|
+
function normalizeSdkLocale(locale) {
|
|
44
|
+
if (!locale) return;
|
|
45
|
+
const sanitized = locale.trim().replace(/_/g, "-");
|
|
46
|
+
if (!sanitized) return;
|
|
47
|
+
const lower = sanitized.toLowerCase();
|
|
48
|
+
if (SDK_LOCALE_ALIASES[lower]) {
|
|
49
|
+
return SDK_LOCALE_ALIASES[lower];
|
|
50
|
+
}
|
|
51
|
+
let canonical;
|
|
52
|
+
try {
|
|
53
|
+
canonical = Intl.getCanonicalLocales(sanitized)[0];
|
|
54
|
+
} catch {
|
|
55
|
+
canonical = void 0;
|
|
56
|
+
}
|
|
57
|
+
const candidates = [canonical, sanitized].filter(Boolean);
|
|
58
|
+
for (const candidate of candidates) {
|
|
59
|
+
const candidateLower = candidate.toLowerCase();
|
|
60
|
+
if (SDK_LOCALE_ALIASES[candidateLower]) {
|
|
61
|
+
return SDK_LOCALE_ALIASES[candidateLower];
|
|
62
|
+
}
|
|
63
|
+
if (SDK_SUPPORTED_LOCALES.includes(candidate)) {
|
|
64
|
+
return candidate;
|
|
65
|
+
}
|
|
66
|
+
const baseMatch = matchSupportedBaseLocale(candidate);
|
|
67
|
+
if (baseMatch) {
|
|
68
|
+
return baseMatch;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function getBrowserLocale() {
|
|
73
|
+
if (typeof navigator === "undefined") return;
|
|
74
|
+
for (const candidate of navigator.languages || []) {
|
|
75
|
+
const normalized = normalizeSdkLocale(candidate);
|
|
76
|
+
if (normalized) return normalized;
|
|
77
|
+
}
|
|
78
|
+
return normalizeSdkLocale(navigator.language);
|
|
79
|
+
}
|
|
80
|
+
function getAutoResolvedLocale(locale) {
|
|
81
|
+
return normalizeSdkLocale(locale) || getBrowserLocale() || SDK_DEFAULT_LOCALE;
|
|
82
|
+
}
|
|
83
|
+
function applyLocaleToUrl(urlValue, locale) {
|
|
84
|
+
if (!locale) return urlValue;
|
|
85
|
+
try {
|
|
86
|
+
const url = new URL(urlValue);
|
|
87
|
+
const localePrefix = `/${locale}`;
|
|
88
|
+
if (!url.pathname.startsWith(`${localePrefix}/`) && url.pathname !== localePrefix) {
|
|
89
|
+
url.pathname = `${localePrefix}${url.pathname}`;
|
|
90
|
+
}
|
|
91
|
+
return url.toString();
|
|
92
|
+
} catch (_error) {
|
|
93
|
+
const localePrefix = `/${locale}`;
|
|
94
|
+
if (!urlValue.startsWith(`${localePrefix}/`) && urlValue !== localePrefix) {
|
|
95
|
+
return `${localePrefix}${urlValue.startsWith("/") ? "" : "/"}${urlValue}`;
|
|
96
|
+
}
|
|
97
|
+
return urlValue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
var HostedFrameModal = class {
|
|
101
|
+
constructor() {
|
|
102
|
+
__publicField(this, "iframe", null);
|
|
103
|
+
__publicField(this, "modal", null);
|
|
104
|
+
__publicField(this, "messageHandler", null);
|
|
105
|
+
}
|
|
106
|
+
openHostedFrame(url, options) {
|
|
107
|
+
if (typeof document === "undefined") return;
|
|
108
|
+
if (this.modal) return;
|
|
109
|
+
this.modal = document.createElement("div");
|
|
110
|
+
Object.assign(this.modal.style, {
|
|
111
|
+
position: "fixed",
|
|
112
|
+
top: "0",
|
|
113
|
+
left: "0",
|
|
114
|
+
width: "100%",
|
|
115
|
+
height: "100%",
|
|
116
|
+
backgroundColor: "rgba(15,23,42,0.52)",
|
|
117
|
+
display: "flex",
|
|
118
|
+
alignItems: "center",
|
|
119
|
+
justifyContent: "center",
|
|
120
|
+
zIndex: "9999",
|
|
121
|
+
transition: "opacity 0.3s ease",
|
|
122
|
+
backdropFilter: "blur(14px)",
|
|
123
|
+
padding: "16px"
|
|
124
|
+
});
|
|
125
|
+
const container = document.createElement("div");
|
|
126
|
+
Object.assign(container.style, {
|
|
127
|
+
width: options.width || "450px",
|
|
128
|
+
height: options.height || "min(600px, 90vh)",
|
|
129
|
+
backgroundColor: "transparent",
|
|
130
|
+
borderRadius: "28px",
|
|
131
|
+
overflow: "visible",
|
|
132
|
+
position: "relative",
|
|
133
|
+
boxShadow: "none",
|
|
134
|
+
maxWidth: "calc(100vw - 32px)",
|
|
135
|
+
maxHeight: "calc(100vh - 32px)",
|
|
136
|
+
transition: "height 180ms ease",
|
|
137
|
+
willChange: "height"
|
|
138
|
+
});
|
|
139
|
+
const closeBtn = document.createElement("button");
|
|
140
|
+
closeBtn.innerHTML = "\xD7";
|
|
141
|
+
Object.assign(closeBtn.style, {
|
|
142
|
+
position: "absolute",
|
|
143
|
+
right: "-14px",
|
|
144
|
+
top: "-14px",
|
|
145
|
+
fontSize: "20px",
|
|
146
|
+
width: "36px",
|
|
147
|
+
height: "36px",
|
|
148
|
+
borderRadius: "9999px",
|
|
149
|
+
border: "1px solid rgba(255,255,255,0.5)",
|
|
150
|
+
background: "rgba(255,255,255,0.9)",
|
|
151
|
+
cursor: "pointer",
|
|
152
|
+
color: "#475569",
|
|
153
|
+
zIndex: "2",
|
|
154
|
+
boxShadow: "0 10px 30px rgba(15,23,42,0.16)"
|
|
155
|
+
});
|
|
156
|
+
closeBtn.onclick = () => {
|
|
157
|
+
this.close();
|
|
158
|
+
options.onCloseButton?.();
|
|
159
|
+
};
|
|
160
|
+
this.iframe = document.createElement("iframe");
|
|
161
|
+
this.iframe.src = url;
|
|
162
|
+
Object.assign(this.iframe.style, {
|
|
163
|
+
width: "100%",
|
|
164
|
+
height: "100%",
|
|
165
|
+
border: "none",
|
|
166
|
+
borderRadius: "28px",
|
|
167
|
+
background: "transparent",
|
|
168
|
+
display: "block"
|
|
169
|
+
});
|
|
170
|
+
container.appendChild(closeBtn);
|
|
171
|
+
container.appendChild(this.iframe);
|
|
172
|
+
this.modal.appendChild(container);
|
|
173
|
+
document.body.appendChild(this.modal);
|
|
174
|
+
this.messageHandler = (event) => {
|
|
175
|
+
if (options.allowedOrigin && options.allowedOrigin !== "*") {
|
|
176
|
+
if (event.origin !== options.allowedOrigin) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const data = event.data;
|
|
181
|
+
if (!data || typeof data !== "object" || !data.type) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
options.onMessage(data, container);
|
|
185
|
+
};
|
|
186
|
+
window.addEventListener("message", this.messageHandler);
|
|
187
|
+
}
|
|
188
|
+
close() {
|
|
189
|
+
if (typeof window === "undefined") return;
|
|
190
|
+
if (this.messageHandler) {
|
|
191
|
+
window.removeEventListener("message", this.messageHandler);
|
|
192
|
+
this.messageHandler = null;
|
|
193
|
+
}
|
|
194
|
+
if (this.modal?.parentNode) {
|
|
195
|
+
this.modal.parentNode.removeChild(this.modal);
|
|
196
|
+
}
|
|
197
|
+
this.modal = null;
|
|
198
|
+
this.iframe = null;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// src/client.ts
|
|
203
|
+
var PaymentUI = class extends HostedFrameModal {
|
|
204
|
+
/**
|
|
205
|
+
* Opens the payment checkout page in an iframe modal.
|
|
206
|
+
* @param urlOrParams - The checkout page URL or payment parameters
|
|
207
|
+
* @param options - UI options
|
|
208
|
+
*/
|
|
209
|
+
openPayment(urlOrParams, options) {
|
|
210
|
+
if (typeof document === "undefined") return;
|
|
211
|
+
if (this.modal) return;
|
|
212
|
+
let checkoutUrl;
|
|
213
|
+
if (typeof urlOrParams === "string") {
|
|
214
|
+
checkoutUrl = urlOrParams;
|
|
215
|
+
} else {
|
|
216
|
+
const {
|
|
217
|
+
appId,
|
|
218
|
+
productId,
|
|
219
|
+
priceId,
|
|
220
|
+
productCode,
|
|
221
|
+
userId,
|
|
222
|
+
checkoutUrl: checkoutUrlParam,
|
|
223
|
+
baseUrl = "https://pay.imgto.link"
|
|
224
|
+
} = urlOrParams;
|
|
225
|
+
const base = (checkoutUrlParam || baseUrl).replace(/\/$/, "");
|
|
226
|
+
if (productCode) {
|
|
227
|
+
checkoutUrl = `${base}/checkout/${appId}/code/${productCode}?userId=${encodeURIComponent(userId)}`;
|
|
228
|
+
} else if (productId && priceId) {
|
|
229
|
+
checkoutUrl = `${base}/checkout/${appId}/${productId}/${priceId}?userId=${encodeURIComponent(userId)}`;
|
|
230
|
+
} else {
|
|
231
|
+
throw new Error(
|
|
232
|
+
"Either productCode or both productId and priceId are required"
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const finalUrl = applyLocaleToUrl(checkoutUrl, options?.locale);
|
|
237
|
+
this.openHostedFrame(finalUrl, {
|
|
238
|
+
allowedOrigin: options?.allowedOrigin,
|
|
239
|
+
onCloseButton: () => options?.onCancel?.(),
|
|
240
|
+
onMessage: (data, container) => {
|
|
241
|
+
switch (data.type) {
|
|
242
|
+
case "PAYMENT_SUCCESS":
|
|
243
|
+
options?.onSuccess?.(data.orderId);
|
|
244
|
+
break;
|
|
245
|
+
case "PAYMENT_CANCELLED":
|
|
246
|
+
options?.onCancel?.(data.orderId);
|
|
247
|
+
break;
|
|
248
|
+
case "PAYMENT_RESIZE":
|
|
249
|
+
if (data.height) {
|
|
250
|
+
const maxHeight = window.innerHeight * 0.9;
|
|
251
|
+
const newHeight = Math.min(data.height, maxHeight);
|
|
252
|
+
container.style.height = `${newHeight}px`;
|
|
253
|
+
}
|
|
254
|
+
break;
|
|
255
|
+
case "PAYMENT_CLOSE":
|
|
256
|
+
this.close();
|
|
257
|
+
options?.onClose?.();
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Poll order status from integrator's API endpoint
|
|
265
|
+
* @param statusUrl - The integrator's API endpoint to check order status
|
|
266
|
+
* @param options - Polling options
|
|
267
|
+
* @returns Promise that resolves when order is paid or rejects on timeout/failure
|
|
268
|
+
*/
|
|
269
|
+
async pollOrderStatus(statusUrl, options) {
|
|
270
|
+
const interval = options?.interval || 3e3;
|
|
271
|
+
const timeout = options?.timeout || 3e5;
|
|
272
|
+
const startTime = Date.now();
|
|
273
|
+
return new Promise((resolve, reject) => {
|
|
274
|
+
const poll = async () => {
|
|
275
|
+
try {
|
|
276
|
+
const response = await fetch(statusUrl);
|
|
277
|
+
if (!response.ok) {
|
|
278
|
+
throw new Error(`Status check failed: ${response.status}`);
|
|
279
|
+
}
|
|
280
|
+
const status = await response.json();
|
|
281
|
+
options?.onStatusChange?.(status);
|
|
282
|
+
if (status.status === "PAID") {
|
|
283
|
+
resolve(status);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (status.status === "CANCELLED" || status.status === "FAILED") {
|
|
287
|
+
reject(new Error(`Order ${status.status.toLowerCase()}`));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (Date.now() - startTime > timeout) {
|
|
291
|
+
reject(new Error("Polling timeout"));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
setTimeout(poll, interval);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (Date.now() - startTime > timeout) {
|
|
297
|
+
reject(error);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
setTimeout(poll, interval);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
poll();
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
function createPaymentUI() {
|
|
308
|
+
return new PaymentUI();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/login.ts
|
|
312
|
+
function getOrigin(value) {
|
|
313
|
+
if (!value) return null;
|
|
314
|
+
try {
|
|
315
|
+
return new URL(value).origin;
|
|
316
|
+
} catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function buildLoginUrl(params) {
|
|
321
|
+
const { appId, baseUrl = "https://pay.imgto.link", loginUrl } = params;
|
|
322
|
+
const rawBase = (loginUrl || baseUrl).replace(/\/$/, "");
|
|
323
|
+
const parentOrigin = typeof window !== "undefined" ? window.location.origin : void 0;
|
|
324
|
+
let finalUrl;
|
|
325
|
+
try {
|
|
326
|
+
const url = new URL(rawBase);
|
|
327
|
+
if (!/\/auth\/connect\/[^/]+$/.test(url.pathname)) {
|
|
328
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}/auth/connect/${appId}`.replace(
|
|
329
|
+
/\/{2,}/g,
|
|
330
|
+
"/"
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
finalUrl = url.toString();
|
|
334
|
+
} catch (_error) {
|
|
335
|
+
if (/\/auth\/connect\/[^/]+$/.test(rawBase)) {
|
|
336
|
+
finalUrl = rawBase;
|
|
337
|
+
} else {
|
|
338
|
+
finalUrl = `${rawBase}/auth/connect/${appId}`.replace(/\/{2,}/g, "/");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
finalUrl = applyLocaleToUrl(finalUrl, getAutoResolvedLocale(params.locale));
|
|
342
|
+
try {
|
|
343
|
+
const url = new URL(finalUrl);
|
|
344
|
+
if (params.preferredChannel) {
|
|
345
|
+
url.searchParams.set("preferredChannel", params.preferredChannel);
|
|
346
|
+
}
|
|
347
|
+
if (parentOrigin) {
|
|
348
|
+
url.searchParams.set("origin", parentOrigin);
|
|
349
|
+
}
|
|
350
|
+
return url.toString();
|
|
351
|
+
} catch (_error) {
|
|
352
|
+
const searchParams = new URLSearchParams();
|
|
353
|
+
if (params.preferredChannel) {
|
|
354
|
+
searchParams.set("preferredChannel", params.preferredChannel);
|
|
355
|
+
}
|
|
356
|
+
if (parentOrigin) {
|
|
357
|
+
searchParams.set("origin", parentOrigin);
|
|
358
|
+
}
|
|
359
|
+
const query = searchParams.toString();
|
|
360
|
+
if (!query) {
|
|
361
|
+
return finalUrl;
|
|
362
|
+
}
|
|
363
|
+
const separator = finalUrl.includes("?") ? "&" : "?";
|
|
364
|
+
return `${finalUrl}${separator}${query}`;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function extractLoginResult(data) {
|
|
368
|
+
return {
|
|
369
|
+
avatar: data.avatar,
|
|
370
|
+
channel: data.channel,
|
|
371
|
+
email: data.email,
|
|
372
|
+
expiresAt: data.expiresAt,
|
|
373
|
+
legacyCasdoorId: data.legacyCasdoorId,
|
|
374
|
+
loginToken: data.loginToken,
|
|
375
|
+
name: data.name,
|
|
376
|
+
userId: data.userId,
|
|
377
|
+
wechatOpenId: data.wechatOpenId,
|
|
378
|
+
wechatUnionId: data.wechatUnionId
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
var LOGIN_UI_LOG_PREFIX = "[Youidian LoginUI]";
|
|
382
|
+
function logLoginDebug(message, details) {
|
|
383
|
+
if (typeof console === "undefined") return;
|
|
384
|
+
console.debug(LOGIN_UI_LOG_PREFIX, message, details || {});
|
|
385
|
+
}
|
|
386
|
+
function logLoginInfo(message, details) {
|
|
387
|
+
if (typeof console === "undefined") return;
|
|
388
|
+
console.info(LOGIN_UI_LOG_PREFIX, message, details || {});
|
|
389
|
+
}
|
|
390
|
+
function logLoginWarn(message, details) {
|
|
391
|
+
if (typeof console === "undefined") return;
|
|
392
|
+
console.warn(LOGIN_UI_LOG_PREFIX, message, details || {});
|
|
393
|
+
}
|
|
394
|
+
var LoginUI = class {
|
|
395
|
+
constructor() {
|
|
396
|
+
__publicField(this, "popup", null);
|
|
397
|
+
__publicField(this, "messageHandler", null);
|
|
398
|
+
__publicField(this, "closeMonitor", null);
|
|
399
|
+
__publicField(this, "completed", false);
|
|
400
|
+
}
|
|
401
|
+
cleanup() {
|
|
402
|
+
if (typeof window !== "undefined" && this.messageHandler) {
|
|
403
|
+
window.removeEventListener("message", this.messageHandler);
|
|
404
|
+
}
|
|
405
|
+
if (typeof window !== "undefined" && this.closeMonitor) {
|
|
406
|
+
window.clearInterval(this.closeMonitor);
|
|
407
|
+
}
|
|
408
|
+
this.messageHandler = null;
|
|
409
|
+
this.closeMonitor = null;
|
|
410
|
+
this.popup = null;
|
|
411
|
+
this.completed = false;
|
|
412
|
+
}
|
|
413
|
+
close() {
|
|
414
|
+
logLoginInfo("close() called", {
|
|
415
|
+
hasPopup: Boolean(this.popup),
|
|
416
|
+
popupClosed: this.popup?.closed ?? true
|
|
417
|
+
});
|
|
418
|
+
if (this.popup && !this.popup.closed) {
|
|
419
|
+
this.popup.close();
|
|
420
|
+
}
|
|
421
|
+
this.cleanup();
|
|
422
|
+
}
|
|
423
|
+
openLogin(params, options) {
|
|
424
|
+
if (typeof window === "undefined") return;
|
|
425
|
+
if (this.popup && !this.popup.closed) return;
|
|
426
|
+
const finalUrl = buildLoginUrl(params);
|
|
427
|
+
logLoginInfo("Opening hosted login popup", {
|
|
428
|
+
appId: params.appId,
|
|
429
|
+
loginUrl: finalUrl,
|
|
430
|
+
allowedOrigin: options?.allowedOrigin || null,
|
|
431
|
+
autoClose: options?.autoClose ?? true
|
|
432
|
+
});
|
|
433
|
+
const popupWidth = 520;
|
|
434
|
+
const popupHeight = 720;
|
|
435
|
+
const left = Math.max(
|
|
436
|
+
0,
|
|
437
|
+
Math.round(window.screenX + (window.outerWidth - popupWidth) / 2)
|
|
438
|
+
);
|
|
439
|
+
const top = Math.max(
|
|
440
|
+
0,
|
|
441
|
+
Math.round(window.screenY + (window.outerHeight - popupHeight) / 2)
|
|
442
|
+
);
|
|
443
|
+
const features = [
|
|
444
|
+
"popup=yes",
|
|
445
|
+
`width=${popupWidth}`,
|
|
446
|
+
`height=${popupHeight}`,
|
|
447
|
+
`left=${left}`,
|
|
448
|
+
`top=${top}`,
|
|
449
|
+
"resizable=yes",
|
|
450
|
+
"scrollbars=yes"
|
|
451
|
+
].join(",");
|
|
452
|
+
const popup = window.open(finalUrl, "youidian-login", features);
|
|
453
|
+
if (!popup) {
|
|
454
|
+
logLoginWarn("Popup blocked by the browser");
|
|
455
|
+
options?.onError?.("Login popup was blocked");
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
this.popup = popup;
|
|
459
|
+
this.completed = false;
|
|
460
|
+
const allowedOrigin = options?.allowedOrigin;
|
|
461
|
+
const popupOrigin = getOrigin(finalUrl);
|
|
462
|
+
this.messageHandler = (event) => {
|
|
463
|
+
if (allowedOrigin && allowedOrigin !== "*" && event.origin !== allowedOrigin) {
|
|
464
|
+
logLoginWarn("Ignored message due to allowedOrigin mismatch", {
|
|
465
|
+
allowedOrigin,
|
|
466
|
+
eventOrigin: event.origin
|
|
467
|
+
});
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (!allowedOrigin && popupOrigin && event.origin !== popupOrigin) {
|
|
471
|
+
logLoginWarn("Ignored message due to popup origin mismatch", {
|
|
472
|
+
expectedOrigin: popupOrigin,
|
|
473
|
+
eventOrigin: event.origin
|
|
474
|
+
});
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const data = event.data;
|
|
478
|
+
if (!data || typeof data !== "object" || !data.type) {
|
|
479
|
+
logLoginDebug("Ignored non-login postMessage payload");
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
logLoginInfo("Received login event", {
|
|
483
|
+
type: data.type,
|
|
484
|
+
eventOrigin: event.origin
|
|
485
|
+
});
|
|
486
|
+
switch (data.type) {
|
|
487
|
+
case "LOGIN_SUCCESS":
|
|
488
|
+
this.completed = true;
|
|
489
|
+
logLoginInfo("Login succeeded", {
|
|
490
|
+
channel: data.channel || null,
|
|
491
|
+
userId: data.userId || null
|
|
492
|
+
});
|
|
493
|
+
options?.onSuccess?.(extractLoginResult(data));
|
|
494
|
+
break;
|
|
495
|
+
case "LOGIN_CANCELLED":
|
|
496
|
+
logLoginInfo("Login cancelled");
|
|
497
|
+
options?.onCancel?.();
|
|
498
|
+
break;
|
|
499
|
+
case "LOGIN_RESIZE":
|
|
500
|
+
break;
|
|
501
|
+
case "LOGIN_ERROR":
|
|
502
|
+
logLoginWarn("Login flow reported an error", {
|
|
503
|
+
message: data.message || data.error || null
|
|
504
|
+
});
|
|
505
|
+
options?.onError?.(data.message || data.error, data);
|
|
506
|
+
break;
|
|
507
|
+
case "LOGIN_CLOSE":
|
|
508
|
+
if (options?.autoClose === false) {
|
|
509
|
+
logLoginInfo("Login popup requested close; autoClose disabled, keeping popup open");
|
|
510
|
+
options?.onClose?.();
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
logLoginInfo("Login popup requested close; autoClose enabled");
|
|
514
|
+
this.close();
|
|
515
|
+
options?.onClose?.();
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
window.addEventListener("message", this.messageHandler);
|
|
520
|
+
this.closeMonitor = window.setInterval(() => {
|
|
521
|
+
if (!this.popup || this.popup.closed) {
|
|
522
|
+
const shouldTreatAsCancel = !this.completed;
|
|
523
|
+
logLoginInfo("Detected popup closed", {
|
|
524
|
+
shouldTreatAsCancel
|
|
525
|
+
});
|
|
526
|
+
this.cleanup();
|
|
527
|
+
if (shouldTreatAsCancel) {
|
|
528
|
+
options?.onCancel?.();
|
|
529
|
+
}
|
|
530
|
+
options?.onClose?.();
|
|
531
|
+
}
|
|
532
|
+
}, 500);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
function createLoginUI() {
|
|
536
|
+
return new LoginUI();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/server.ts
|
|
540
|
+
import crypto from "crypto";
|
|
541
|
+
var PaymentClient = class {
|
|
542
|
+
// 用于生成 checkout URL
|
|
543
|
+
constructor(options) {
|
|
544
|
+
__publicField(this, "appId");
|
|
545
|
+
__publicField(this, "appSecret");
|
|
546
|
+
__publicField(this, "apiUrl");
|
|
547
|
+
// 用于 API 调用
|
|
548
|
+
__publicField(this, "checkoutUrl");
|
|
549
|
+
if (!options.appId) throw new Error("appId is required");
|
|
550
|
+
if (!options.appSecret) throw new Error("appSecret is required");
|
|
551
|
+
this.appId = options.appId;
|
|
552
|
+
this.appSecret = options.appSecret;
|
|
553
|
+
const apiUrl = options.apiUrl || options.baseUrl || "https://pay-api.imgto.link";
|
|
554
|
+
this.apiUrl = apiUrl.replace(/\/$/, "");
|
|
555
|
+
const checkoutUrl = options.checkoutUrl || options.baseUrl || "https://pay.imgto.link";
|
|
556
|
+
this.checkoutUrl = checkoutUrl.replace(/\/$/, "");
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Generate SHA256 signature for the request
|
|
560
|
+
* Logic: SHA256(appId + appSecret + timestamp)
|
|
561
|
+
*/
|
|
562
|
+
generateSignature(timestamp) {
|
|
563
|
+
const str = `${this.appId}${this.appSecret}${timestamp}`;
|
|
564
|
+
return crypto.createHash("sha256").update(str).digest("hex");
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Internal request helper for Gateway API
|
|
568
|
+
*/
|
|
569
|
+
async request(method, path, body) {
|
|
570
|
+
const timestamp = Date.now();
|
|
571
|
+
const signature = this.generateSignature(timestamp);
|
|
572
|
+
const url = `${this.apiUrl}/api/v1/gateway/${this.appId}${path}`;
|
|
573
|
+
const headers = {
|
|
574
|
+
"Content-Type": "application/json",
|
|
575
|
+
"X-Pay-Timestamp": timestamp.toString(),
|
|
576
|
+
"X-Pay-Sign": signature
|
|
577
|
+
};
|
|
578
|
+
const options = {
|
|
579
|
+
method,
|
|
580
|
+
headers,
|
|
581
|
+
body: body ? JSON.stringify(body) : void 0
|
|
582
|
+
};
|
|
583
|
+
const response = await fetch(url, options);
|
|
584
|
+
if (!response.ok) {
|
|
585
|
+
const errorText = await response.text();
|
|
586
|
+
throw new Error(`Payment SDK Error (${response.status}): ${errorText}`);
|
|
587
|
+
}
|
|
588
|
+
const json = await response.json();
|
|
589
|
+
if (json.error) {
|
|
590
|
+
throw new Error(`Payment API Error: ${json.error}`);
|
|
591
|
+
}
|
|
592
|
+
return json.data;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Decrypts the callback notification payload using AES-256-GCM.
|
|
596
|
+
* @param notification - The encrypted notification from payment webhook
|
|
597
|
+
* @returns Decrypted payment callback data
|
|
598
|
+
*/
|
|
599
|
+
decryptCallback(notification) {
|
|
600
|
+
try {
|
|
601
|
+
const { iv, encryptedData, authTag } = notification;
|
|
602
|
+
const key = crypto.createHash("sha256").update(this.appSecret).digest();
|
|
603
|
+
const decipher = crypto.createDecipheriv(
|
|
604
|
+
"aes-256-gcm",
|
|
605
|
+
key,
|
|
606
|
+
Buffer.from(iv, "hex")
|
|
607
|
+
);
|
|
608
|
+
decipher.setAuthTag(Buffer.from(authTag, "hex"));
|
|
609
|
+
let decrypted = decipher.update(encryptedData, "hex", "utf8");
|
|
610
|
+
decrypted += decipher.final("utf8");
|
|
611
|
+
return JSON.parse(decrypted);
|
|
612
|
+
} catch (error) {
|
|
613
|
+
throw new Error(
|
|
614
|
+
"Failed to decrypt payment callback: Invalid secret or tampered data."
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Fetch products for the configured app.
|
|
620
|
+
*/
|
|
621
|
+
async getProducts(options) {
|
|
622
|
+
const params = new URLSearchParams();
|
|
623
|
+
if (options?.locale) params.append("locale", options.locale);
|
|
624
|
+
if (options?.currency) params.append("currency", options.currency);
|
|
625
|
+
const path = params.toString() ? `/products?${params.toString()}` : "/products";
|
|
626
|
+
return this.request("GET", path);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Create a new order
|
|
630
|
+
* @param params - Order creation parameters
|
|
631
|
+
* @returns Order details with payment parameters
|
|
632
|
+
*/
|
|
633
|
+
async createOrder(params) {
|
|
634
|
+
return this.request("POST", "/orders", params);
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Create a WeChat Mini Program order (channel fixed to WECHAT_MINI)
|
|
638
|
+
* @param params - Mini program order parameters
|
|
639
|
+
* @returns Order details with payment parameters
|
|
640
|
+
*/
|
|
641
|
+
async createMiniProgramOrder(params) {
|
|
642
|
+
const { openid, ...rest } = params;
|
|
643
|
+
return this.request("POST", "/orders", {
|
|
644
|
+
...rest,
|
|
645
|
+
channel: "WECHAT_MINI",
|
|
646
|
+
openid,
|
|
647
|
+
metadata: { openid }
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Pay for an existing order
|
|
652
|
+
* @param orderId - The order ID to pay
|
|
653
|
+
* @param params - Payment parameters including channel
|
|
654
|
+
*/
|
|
655
|
+
async payOrder(orderId, params) {
|
|
656
|
+
return this.request("POST", `/orders/${orderId}/pay`, params);
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Query order status
|
|
660
|
+
* @param orderId - The order ID to query
|
|
661
|
+
*/
|
|
662
|
+
async getOrderStatus(orderId) {
|
|
663
|
+
return this.request("GET", `/orders/${orderId}`);
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Get order details (full order information)
|
|
667
|
+
* @param orderId - The order ID to query
|
|
668
|
+
*/
|
|
669
|
+
async getOrderDetails(orderId) {
|
|
670
|
+
return this.request("GET", `/orders/${orderId}/details`);
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Get orders list with pagination
|
|
674
|
+
* @param params - Query parameters (pagination, filters)
|
|
675
|
+
* @returns Orders list and pagination info
|
|
676
|
+
*/
|
|
677
|
+
async getOrders(params) {
|
|
678
|
+
const queryParams = new URLSearchParams();
|
|
679
|
+
if (params?.page) queryParams.append("page", params.page.toString());
|
|
680
|
+
if (params?.pageSize)
|
|
681
|
+
queryParams.append("pageSize", params.pageSize.toString());
|
|
682
|
+
if (params?.userId) queryParams.append("userId", params.userId);
|
|
683
|
+
if (params?.status) queryParams.append("status", params.status);
|
|
684
|
+
if (params?.startDate) queryParams.append("startDate", params.startDate);
|
|
685
|
+
if (params?.endDate) queryParams.append("endDate", params.endDate);
|
|
686
|
+
const path = queryParams.toString() ? `/orders?${queryParams.toString()}` : "/orders";
|
|
687
|
+
return this.request("GET", path);
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Get user entitlements in the legacy flat shape.
|
|
691
|
+
* @param userId - User ID
|
|
692
|
+
*/
|
|
693
|
+
async getEntitlements(userId) {
|
|
694
|
+
return this.request("GET", `/users/${userId}/entitlements`);
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Get user entitlements with full details (type, expiry, reset config, source)
|
|
698
|
+
* @param userId - User ID
|
|
699
|
+
*/
|
|
700
|
+
async getEntitlementsDetail(userId) {
|
|
701
|
+
return this.request("GET", `/users/${userId}/entitlements/detail`);
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Ensure user exists and auto-assign trial product if new user
|
|
705
|
+
* This should be called when user first logs in or registers
|
|
706
|
+
* @param userId - User ID
|
|
707
|
+
*/
|
|
708
|
+
async ensureUserWithTrial(userId) {
|
|
709
|
+
return this.request("POST", `/users/${userId}/entitlements/bootstrap`, {});
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Get a single entitlement value
|
|
713
|
+
* @param userId - User ID
|
|
714
|
+
* @param key - Entitlement key
|
|
715
|
+
*/
|
|
716
|
+
async getEntitlementValue(userId, key) {
|
|
717
|
+
const entitlements = await this.getEntitlements(userId);
|
|
718
|
+
return entitlements[key] ?? null;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Consume numeric entitlement
|
|
722
|
+
* @param userId - User ID
|
|
723
|
+
* @param key - Entitlement key
|
|
724
|
+
* @param amount - Amount to consume
|
|
725
|
+
*/
|
|
726
|
+
async consumeEntitlement(userId, key, amount, options) {
|
|
727
|
+
return this.request("POST", `/users/${userId}/entitlements/consume`, {
|
|
728
|
+
key,
|
|
729
|
+
amount,
|
|
730
|
+
...options
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Add numeric entitlement (e.g. refund)
|
|
735
|
+
* @param userId - User ID
|
|
736
|
+
* @param key - Entitlement key
|
|
737
|
+
* @param amount - Amount to add
|
|
738
|
+
*/
|
|
739
|
+
async addEntitlement(userId, key, amount) {
|
|
740
|
+
return this.request("POST", `/users/${userId}/entitlements/add`, {
|
|
741
|
+
key,
|
|
742
|
+
amount
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Toggle boolean entitlement
|
|
747
|
+
* @param userId - User ID
|
|
748
|
+
* @param key - Entitlement key
|
|
749
|
+
* @param enabled - Whether to enable
|
|
750
|
+
*/
|
|
751
|
+
async toggleEntitlement(userId, key, enabled) {
|
|
752
|
+
return this.request("POST", `/users/${userId}/entitlements/toggle`, {
|
|
753
|
+
key,
|
|
754
|
+
enabled
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Generate checkout URL for client-side payment
|
|
759
|
+
* @param productId - Product ID
|
|
760
|
+
* @param priceId - Price ID
|
|
761
|
+
* @returns Checkout page URL
|
|
762
|
+
*/
|
|
763
|
+
getCheckoutUrl(productId, priceId) {
|
|
764
|
+
return `${this.checkoutUrl}/checkout/${this.appId}/${productId}/${priceId}`;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Verify a hosted login token and return the normalized login profile.
|
|
768
|
+
* This request is signed with your app credentials and routed through the worker API.
|
|
769
|
+
*/
|
|
770
|
+
async verifyLoginToken(token) {
|
|
771
|
+
if (!token?.trim()) {
|
|
772
|
+
throw new Error("login token is required");
|
|
773
|
+
}
|
|
774
|
+
return this.request("POST", "/login/tokens/verify", { token: token.trim() });
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
export {
|
|
778
|
+
LoginUI,
|
|
779
|
+
PaymentClient,
|
|
780
|
+
PaymentUI,
|
|
781
|
+
createLoginUI,
|
|
782
|
+
createPaymentUI
|
|
783
|
+
};
|
|
784
|
+
//# sourceMappingURL=index.js.map
|