@seaverse/payment-sdk 0.1.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/index.cjs ADDED
@@ -0,0 +1,1177 @@
1
+ 'use strict';
2
+
3
+ var axios = require('axios');
4
+
5
+ /**
6
+ * Payment API error
7
+ */
8
+ class PaymentAPIError extends Error {
9
+ constructor(message, code, response) {
10
+ super(message);
11
+ this.code = code;
12
+ this.response = response;
13
+ this.name = 'PaymentAPIError';
14
+ }
15
+ }
16
+ /**
17
+ * SeaVerse Payment API Client
18
+ *
19
+ * Provides query capabilities for user credit accounts and transactions with multi-tenant support.
20
+ * All methods are read-only - credit spending/granting is handled by platform backend.
21
+ *
22
+ * ## Multi-tenant Architecture
23
+ * - Each app has isolated credit accounts using `app_id`
24
+ * - Same user can have different credit accounts in different apps
25
+ * - Use `appId` to specify which app's credit pool to access
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * // SeaVerse platform usage
30
+ * const client = new PaymentClient({
31
+ * appId: 'seaverse',
32
+ * token: 'your-bearer-token'
33
+ * });
34
+ *
35
+ * // Third-party app usage
36
+ * const gameClient = new PaymentClient({
37
+ * appId: 'game-abc123',
38
+ * token: 'your-bearer-token'
39
+ * });
40
+ *
41
+ * // Get credit account
42
+ * const account = await client.getCreditAccount();
43
+ * console.log(`Balance: ${account.balance} credits`);
44
+ *
45
+ * // List recent transactions
46
+ * const transactions = await client.listTransactions({ page_size: 10 });
47
+ * ```
48
+ */
49
+ class PaymentClient {
50
+ constructor(options) {
51
+ this.axios = axios.create({
52
+ baseURL: options.baseURL || 'https://payment.sg.seaverse.dev',
53
+ timeout: options.timeout || 30000,
54
+ headers: {
55
+ 'Content-Type': 'application/json',
56
+ 'X-App-ID': options.appId,
57
+ Authorization: `Bearer ${options.token}`,
58
+ },
59
+ });
60
+ // Add response interceptor for error handling
61
+ this.axios.interceptors.response.use((response) => response, (error) => {
62
+ if (error.response) {
63
+ const data = error.response.data;
64
+ throw new PaymentAPIError(data.message || error.message, data.code || error.response.status, data);
65
+ }
66
+ throw new PaymentAPIError(error.message, error.code === 'ECONNABORTED' ? 408 : 500);
67
+ });
68
+ }
69
+ /**
70
+ * Get credit detail with 4-pool system information
71
+ *
72
+ * Returns detailed credit information across 4 pools (daily/event/monthly/permanent)
73
+ * with expiration timestamps. This is the recommended method for displaying user credits.
74
+ *
75
+ * **Credit Pool Types:**
76
+ * - `daily`: Daily credits (expires at next day 00:00 UTC)
77
+ * - `event`: Event credits (expires at event deadline)
78
+ * - `monthly`: Monthly credits (expires 30 days after last grant)
79
+ * - `permanent`: Permanent credits (never expires)
80
+ *
81
+ * **Consumption Priority:**
82
+ * Daily → Event → Monthly → Permanent
83
+ *
84
+ * **Response Features:**
85
+ * - Only returns pools with balance > 0
86
+ * - `expires_at` is millisecond timestamp (0 means permanent)
87
+ * - Frontend can calculate remaining time from timestamp
88
+ *
89
+ * **Recommended Refresh Rate:** 30 seconds
90
+ *
91
+ * @returns Credit detail with pool breakdown
92
+ * @throws {PaymentAPIError} If request fails
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * const detail = await client.getCreditDetail();
97
+ * console.log(`Total Balance: ${detail.total_balance}`);
98
+ *
99
+ * detail.pools.forEach(pool => {
100
+ * const label = pool.expires_at === 0
101
+ * ? 'Permanent'
102
+ * : `Expires in ${Math.floor((pool.expires_at - Date.now()) / 86400000)} days`;
103
+ * console.log(`${pool.type}: ${pool.balance} (${label})`);
104
+ * });
105
+ * ```
106
+ */
107
+ async getCreditDetail() {
108
+ const response = await this.axios.get('/sdk/v1/credits/detail');
109
+ return response.data.data;
110
+ }
111
+ /**
112
+ * Get authenticated user's credit account information
113
+ *
114
+ * Returns current balance, total earned/spent, frozen balance, and account status.
115
+ *
116
+ * @deprecated Use getCreditDetail() instead for the new 4-pool credit system
117
+ * @returns Credit account information
118
+ * @throws {PaymentAPIError} If request fails
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * const account = await client.getCreditAccount();
123
+ * console.log(`Balance: ${account.balance}`);
124
+ * console.log(`Status: ${account.status}`);
125
+ * ```
126
+ */
127
+ async getCreditAccount() {
128
+ const response = await this.axios.get('/sdk/v1/credits/account');
129
+ return response.data.data;
130
+ }
131
+ /**
132
+ * List credit transactions with filters and pagination
133
+ *
134
+ * Query user's transaction history with optional filters for type, source, status, and date range.
135
+ * Supports pagination with max 50 items per page.
136
+ *
137
+ * @param request - List transactions request parameters
138
+ * @returns Paginated transaction list with has_more flag
139
+ * @throws {PaymentAPIError} If request fails
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * // Get recent transactions
144
+ * const result = await client.listTransactions({ page_size: 20 });
145
+ *
146
+ * // Filter by type
147
+ * const spending = await client.listTransactions({
148
+ * type: 'spend',
149
+ * page: 1,
150
+ * page_size: 10
151
+ * });
152
+ *
153
+ * // Filter by date range
154
+ * const recentTransactions = await client.listTransactions({
155
+ * start_date: '2024-01-01T00:00:00Z',
156
+ * end_date: '2024-12-31T23:59:59Z',
157
+ * status: 'completed'
158
+ * });
159
+ * ```
160
+ */
161
+ async listTransactions(request = {}) {
162
+ const params = {};
163
+ if (request.type)
164
+ params.type = request.type;
165
+ if (request.source)
166
+ params.source = request.source;
167
+ if (request.status)
168
+ params.status = request.status;
169
+ if (request.start_date)
170
+ params.start_date = request.start_date;
171
+ if (request.end_date)
172
+ params.end_date = request.end_date;
173
+ if (request.page)
174
+ params.page = request.page;
175
+ if (request.page_size)
176
+ params.page_size = request.page_size;
177
+ const response = await this.axios.get('/sdk/v1/credits/transactions', { params });
178
+ return response.data.data;
179
+ }
180
+ /**
181
+ * Update the bearer token for authentication
182
+ *
183
+ * Use this method to update the token when it expires or needs to be refreshed.
184
+ *
185
+ * @param token - New bearer token
186
+ *
187
+ * @example
188
+ * ```typescript
189
+ * client.setToken('new-bearer-token');
190
+ * ```
191
+ */
192
+ setToken(token) {
193
+ this.axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
194
+ }
195
+ /**
196
+ * Update the base URL for the API
197
+ *
198
+ * Use this method to switch between different environments (dev/staging/prod).
199
+ *
200
+ * @param baseURL - New base URL
201
+ *
202
+ * @example
203
+ * ```typescript
204
+ * client.setBaseURL('https://payment.sg.seaverse.dev');
205
+ * ```
206
+ */
207
+ setBaseURL(baseURL) {
208
+ this.axios.defaults.baseURL = baseURL;
209
+ }
210
+ /**
211
+ * Update the app ID for multi-tenant isolation
212
+ *
213
+ * Use this method to switch between different apps at runtime.
214
+ *
215
+ * @param appId - New app ID
216
+ *
217
+ * @example
218
+ * ```typescript
219
+ * // Switch to SeaVerse platform
220
+ * client.setAppId('seaverse');
221
+ *
222
+ * // Switch to third-party app
223
+ * client.setAppId('game-abc123');
224
+ * ```
225
+ */
226
+ setAppId(appId) {
227
+ this.axios.defaults.headers.common['X-App-ID'] = appId;
228
+ }
229
+ }
230
+
231
+ /**
232
+ * SeaVerse Payment SDK - 常量配置
233
+ */
234
+ /**
235
+ * 环境配置
236
+ */
237
+ const ENV_CONFIG = {
238
+ develop: {
239
+ apiHost: 'https://aiart-openresty.dev.seaart.dev',
240
+ iframeUrl: 'https://aiart-payment-page.dev.seaart.dev',
241
+ },
242
+ release: {
243
+ apiHost: 'https://www.seaart.ai',
244
+ iframeUrl: 'https://pay.seaart.ai',
245
+ },
246
+ };
247
+ /**
248
+ * SDK CDN 地址
249
+ */
250
+ const SDK_CDN_URL = 'https://image.cdn2.seaart.me/seaart-payment-sdk/1.0.2/seaart-pay-sdk-umd.min.js';
251
+ /**
252
+ * SDK 加载超时时间(毫秒)
253
+ */
254
+ const SDK_LOAD_TIMEOUT = 15000;
255
+ /**
256
+ * SDK 全局对象名称
257
+ */
258
+ const SDK_GLOBAL_NAME = 'SeaArtPay';
259
+ /**
260
+ * API 端点
261
+ */
262
+ const API_ENDPOINTS = {
263
+ /** SDK 结账接口 */
264
+ SDK_CHECKOUT: '/api/v1/payment/sdk/checkout',
265
+ };
266
+ /**
267
+ * 默认配置
268
+ */
269
+ const DEFAULT_CHECKOUT_CONFIG = {
270
+ environment: 'develop',
271
+ debug: false,
272
+ language: 'en',
273
+ sdkTimeout: SDK_LOAD_TIMEOUT,
274
+ };
275
+ /**
276
+ * HTTP 状态码
277
+ */
278
+ const HTTP_STATUS = {
279
+ OK: 200,
280
+ BAD_REQUEST: 400,
281
+ UNAUTHORIZED: 401,
282
+ FORBIDDEN: 403,
283
+ NOT_FOUND: 404,
284
+ INTERNAL_ERROR: 500,
285
+ };
286
+ /**
287
+ * 业务状态码
288
+ */
289
+ const BIZ_CODE = {
290
+ SUCCESS: 0,
291
+ BAD_REQUEST: 400,
292
+ UNAUTHORIZED: 401,
293
+ SERVER_ERROR: 500,
294
+ };
295
+
296
+ /**
297
+ * SeaArtPay SDK 加载器
298
+ * 负责从 CDN 动态加载 SeaArt 支付 SDK
299
+ */
300
+ /**
301
+ * SDK 加载器类
302
+ */
303
+ class SeaArtPayLoader {
304
+ constructor(config = {}) {
305
+ this.status = 'idle';
306
+ this.sdk = null;
307
+ this.loadPromise = null;
308
+ this.config = {
309
+ cdnUrl: config.cdnUrl ?? SDK_CDN_URL,
310
+ timeout: config.timeout ?? SDK_LOAD_TIMEOUT,
311
+ debug: config.debug ?? false,
312
+ environment: config.environment ?? 'develop',
313
+ };
314
+ }
315
+ /**
316
+ * 获取当前加载状态
317
+ */
318
+ getStatus() {
319
+ return this.status;
320
+ }
321
+ /**
322
+ * 检查 SDK 是否已加载
323
+ */
324
+ isLoaded() {
325
+ return this.status === 'loaded' && this.sdk !== null;
326
+ }
327
+ /**
328
+ * 获取 SDK 实例
329
+ */
330
+ getSDK() {
331
+ return this.sdk;
332
+ }
333
+ /**
334
+ * 加载 SDK
335
+ */
336
+ async load() {
337
+ // 如果已加载,直接返回
338
+ if (this.sdk) {
339
+ return this.sdk;
340
+ }
341
+ // 如果正在加载,返回现有的 Promise
342
+ if (this.loadPromise) {
343
+ return this.loadPromise;
344
+ }
345
+ this.status = 'loading';
346
+ this.log('开始加载 SeaArtPay SDK...');
347
+ this.loadPromise = this.loadSDKFromSources();
348
+ try {
349
+ this.sdk = await this.loadPromise;
350
+ this.status = 'loaded';
351
+ this.initializeSDK();
352
+ this.log('SDK 加载成功');
353
+ return this.sdk;
354
+ }
355
+ catch (error) {
356
+ this.status = 'error';
357
+ this.loadPromise = null;
358
+ throw error;
359
+ }
360
+ }
361
+ /**
362
+ * 从多个源尝试加载 SDK
363
+ */
364
+ async loadSDKFromSources() {
365
+ const sources = [
366
+ { name: 'CDN', url: this.config.cdnUrl },
367
+ ];
368
+ for (const source of sources) {
369
+ this.log(`尝试从 ${source.name} 加载: ${source.url}`);
370
+ try {
371
+ await this.loadScript(source.url);
372
+ const sdk = this.extractSDKInstance();
373
+ if (sdk) {
374
+ this.log(`成功从 ${source.name} 加载 SDK`);
375
+ return sdk;
376
+ }
377
+ this.log(`${source.name} 加载的脚本无效`);
378
+ }
379
+ catch (error) {
380
+ this.log(`从 ${source.name} 加载失败: ${error}`);
381
+ }
382
+ }
383
+ throw new Error('所有 SDK 源加载失败');
384
+ }
385
+ /**
386
+ * 动态加载脚本
387
+ */
388
+ loadScript(url) {
389
+ return new Promise((resolve, reject) => {
390
+ // 检查是否在浏览器环境
391
+ if (typeof document === 'undefined') {
392
+ reject(new Error('非浏览器环境,无法加载脚本'));
393
+ return;
394
+ }
395
+ // 检查是否已加载
396
+ const existingScript = document.querySelector(`script[src="${url}"]`);
397
+ if (existingScript) {
398
+ resolve();
399
+ return;
400
+ }
401
+ const script = document.createElement('script');
402
+ script.src = url;
403
+ script.async = true;
404
+ script.setAttribute('data-sdk', 'seaart-payment');
405
+ const timer = setTimeout(() => {
406
+ cleanup();
407
+ reject(new Error(`脚本加载超时: ${url}`));
408
+ }, this.config.timeout);
409
+ const cleanup = () => {
410
+ clearTimeout(timer);
411
+ script.onload = null;
412
+ script.onerror = null;
413
+ };
414
+ script.onload = () => {
415
+ cleanup();
416
+ resolve();
417
+ };
418
+ script.onerror = () => {
419
+ cleanup();
420
+ document.head.removeChild(script);
421
+ reject(new Error(`脚本加载失败: ${url}`));
422
+ };
423
+ document.head.appendChild(script);
424
+ });
425
+ }
426
+ /**
427
+ * 提取 SDK 实例
428
+ * 处理 UMD 包装对象的情况
429
+ */
430
+ extractSDKInstance() {
431
+ if (typeof window === 'undefined') {
432
+ return null;
433
+ }
434
+ const globalObj = window.SeaArtPay;
435
+ if (!globalObj) {
436
+ return null;
437
+ }
438
+ // 检查是否是实际的 SDK 实例(有 show 方法)
439
+ if (this.isValidSDK(globalObj)) {
440
+ return globalObj;
441
+ }
442
+ // 检查是否是 UMD 包装对象
443
+ const wrapped = globalObj;
444
+ if (wrapped.SeaArtPay && this.isValidSDK(wrapped.SeaArtPay)) {
445
+ // 修复全局引用
446
+ window.SeaArtPay = wrapped.SeaArtPay;
447
+ return wrapped.SeaArtPay;
448
+ }
449
+ return null;
450
+ }
451
+ /**
452
+ * 验证 SDK 实例是否有效
453
+ */
454
+ isValidSDK(sdk) {
455
+ return (typeof sdk === 'object' &&
456
+ sdk !== null &&
457
+ typeof sdk.show === 'function');
458
+ }
459
+ /**
460
+ * 初始化 SDK
461
+ */
462
+ initializeSDK() {
463
+ if (!this.sdk)
464
+ return;
465
+ const envConfig = ENV_CONFIG[this.config.environment];
466
+ if (typeof this.sdk.init === 'function') {
467
+ this.sdk.init({
468
+ debug: this.config.debug,
469
+ iframeUrl: envConfig.iframeUrl,
470
+ });
471
+ this.log(`SDK 已初始化,环境: ${this.config.environment}`);
472
+ }
473
+ }
474
+ /**
475
+ * 日志输出
476
+ */
477
+ log(message) {
478
+ if (this.config.debug) {
479
+ console.log(`[SeaArtPayLoader] ${message}`);
480
+ }
481
+ }
482
+ /**
483
+ * 重置加载器
484
+ */
485
+ reset() {
486
+ this.status = 'idle';
487
+ this.sdk = null;
488
+ this.loadPromise = null;
489
+ }
490
+ }
491
+ /**
492
+ * 创建全局单例加载器
493
+ */
494
+ let globalLoader = null;
495
+ /**
496
+ * 获取或创建全局加载器
497
+ */
498
+ function getGlobalLoader(config) {
499
+ if (!globalLoader) {
500
+ globalLoader = new SeaArtPayLoader(config);
501
+ }
502
+ return globalLoader;
503
+ }
504
+ /**
505
+ * 重置全局加载器
506
+ */
507
+ function resetGlobalLoader() {
508
+ globalLoader?.reset();
509
+ globalLoader = null;
510
+ }
511
+
512
+ /**
513
+ * 结账 API 模块
514
+ * 实现 /api/v1/payment/sdk/checkout 接口调用
515
+ */
516
+ /**
517
+ * 结账 API 客户端
518
+ */
519
+ class CheckoutAPI {
520
+ constructor(config) {
521
+ this.config = config;
522
+ }
523
+ /**
524
+ * 创建一次性购买订单
525
+ */
526
+ async createOneTimeCheckout(params) {
527
+ return this.createCheckout({
528
+ product_id: params.productId,
529
+ product_name: params.productName,
530
+ price: params.price,
531
+ purchase_type: 1,
532
+ extra: params.extra,
533
+ });
534
+ }
535
+ /**
536
+ * 创建订阅订单
537
+ */
538
+ async createSubscriptionCheckout(params) {
539
+ return this.createCheckout({
540
+ product_id: params.productId,
541
+ product_name: params.productName,
542
+ price: params.price,
543
+ purchase_type: 2,
544
+ subscription: params.subscription,
545
+ extra: params.extra,
546
+ });
547
+ }
548
+ /**
549
+ * 创建结账订单
550
+ */
551
+ async createCheckout(request) {
552
+ this.validateRequest(request);
553
+ const url = `${this.config.apiHost}${API_ENDPOINTS.SDK_CHECKOUT}`;
554
+ const token = await this.config.getAuthToken();
555
+ if (!token) {
556
+ throw this.createError('UNAUTHORIZED', '未授权:缺少 JWT Token');
557
+ }
558
+ this.log('发起结账请求:', { url, request });
559
+ try {
560
+ const response = await fetch(url, {
561
+ method: 'POST',
562
+ headers: {
563
+ 'Content-Type': 'application/json',
564
+ Authorization: `Bearer ${token}`,
565
+ },
566
+ body: JSON.stringify(request),
567
+ });
568
+ const data = await response.json();
569
+ this.log('结账响应:', data);
570
+ if (data.code !== BIZ_CODE.SUCCESS) {
571
+ throw this.createError(this.mapErrorCode(data.code), data.msg || '结账失败');
572
+ }
573
+ if (!data.data) {
574
+ throw this.createError('CHECKOUT_FAILED', '结账响应数据为空');
575
+ }
576
+ return this.transformResponse(data.data);
577
+ }
578
+ catch (error) {
579
+ if (this.isPaymentError(error)) {
580
+ throw error;
581
+ }
582
+ if (error instanceof TypeError) {
583
+ throw this.createError('NETWORK_ERROR', '网络请求失败', error);
584
+ }
585
+ throw this.createError('UNKNOWN_ERROR', '未知错误', error);
586
+ }
587
+ }
588
+ /**
589
+ * 验证请求参数
590
+ */
591
+ validateRequest(request) {
592
+ // 必须提供 productId 或 (productName + price)
593
+ const hasProductId = !!request.product_id;
594
+ const hasManualProduct = !!request.product_name && request.price != null;
595
+ if (!hasProductId && !hasManualProduct) {
596
+ throw this.createError('INVALID_PARAMS', '必须提供 productId 或 (productName + price)');
597
+ }
598
+ // 订阅类型必须提供订阅参数
599
+ if (request.purchase_type === 2 && !request.subscription) {
600
+ throw this.createError('INVALID_PARAMS', '订阅类型必须提供 subscription 参数');
601
+ }
602
+ // 验证订阅参数
603
+ if (request.subscription) {
604
+ if (!request.subscription.period) {
605
+ throw this.createError('INVALID_PARAMS', '订阅参数缺少 period');
606
+ }
607
+ if (request.subscription.periodAmount == null) {
608
+ throw this.createError('INVALID_PARAMS', '订阅参数缺少 periodAmount');
609
+ }
610
+ }
611
+ }
612
+ /**
613
+ * 转换响应数据
614
+ */
615
+ transformResponse(response) {
616
+ return {
617
+ orderId: response.order_id,
618
+ transactionId: response.transaction_id,
619
+ price: response.price,
620
+ currency: response.currency,
621
+ sdkConfig: {
622
+ appId: response.sdk_config.app_id,
623
+ apiHost: response.sdk_config.api_host,
624
+ environment: response.sdk_config.environment,
625
+ },
626
+ };
627
+ }
628
+ /**
629
+ * 映射错误代码
630
+ */
631
+ mapErrorCode(code) {
632
+ switch (code) {
633
+ case BIZ_CODE.BAD_REQUEST:
634
+ return 'INVALID_PARAMS';
635
+ case BIZ_CODE.UNAUTHORIZED:
636
+ return 'UNAUTHORIZED';
637
+ case BIZ_CODE.SERVER_ERROR:
638
+ return 'API_ERROR';
639
+ default:
640
+ return 'CHECKOUT_FAILED';
641
+ }
642
+ }
643
+ /**
644
+ * 创建支付错误
645
+ */
646
+ createError(code, message, cause) {
647
+ return { code, message, cause };
648
+ }
649
+ /**
650
+ * 检查是否是支付错误
651
+ */
652
+ isPaymentError(error) {
653
+ return (typeof error === 'object' &&
654
+ error !== null &&
655
+ 'code' in error &&
656
+ 'message' in error);
657
+ }
658
+ /**
659
+ * 日志输出
660
+ */
661
+ log(message, data) {
662
+ if (this.config.debug) {
663
+ console.log(`[CheckoutAPI] ${message}`, data ?? '');
664
+ }
665
+ }
666
+ }
667
+
668
+ /**
669
+ * PaymentCheckoutClient - 支付弹窗客户端
670
+ * 提供支付弹窗的初始化、结账和订阅功能
671
+ */
672
+ /**
673
+ * 支付弹窗客户端类
674
+ *
675
+ * 提供一次性购买和订阅购买功能,自动处理:
676
+ * - SeaArtPay SDK 的动态加载
677
+ * - 创建结账订单
678
+ * - 显示支付弹窗
679
+ * - 支付结果回调
680
+ *
681
+ * @example
682
+ * ```typescript
683
+ * // 创建客户端
684
+ * const checkoutClient = new PaymentCheckoutClient({
685
+ * apiHost: 'https://payment.sg.seaverse.dev',
686
+ * authToken: 'your-jwt-token',
687
+ * environment: 'develop',
688
+ * debug: true,
689
+ * });
690
+ *
691
+ * // 初始化
692
+ * await checkoutClient.init();
693
+ *
694
+ * // 发起购买
695
+ * const result = await checkoutClient.checkout({
696
+ * productId: 'pkg_starter',
697
+ * onSuccess: (res) => console.log('支付成功:', res),
698
+ * onError: (err) => console.error('支付失败:', err),
699
+ * });
700
+ * ```
701
+ */
702
+ class PaymentCheckoutClient {
703
+ constructor(config) {
704
+ this.status = 'idle';
705
+ this.initPromise = null;
706
+ // 验证必须提供 authToken 或 getAuthToken 其中之一
707
+ if (!config.authToken && !config.getAuthToken) {
708
+ throw new Error('必须提供 authToken 或 getAuthToken 其中之一');
709
+ }
710
+ this.config = {
711
+ ...config,
712
+ environment: config.environment ?? DEFAULT_CHECKOUT_CONFIG.environment,
713
+ debug: config.debug ?? DEFAULT_CHECKOUT_CONFIG.debug,
714
+ };
715
+ // 创建 SDK 加载器
716
+ this.loader = new SeaArtPayLoader({
717
+ cdnUrl: config.sdkCdnUrl,
718
+ timeout: config.sdkTimeout ?? DEFAULT_CHECKOUT_CONFIG.sdkTimeout,
719
+ debug: this.config.debug,
720
+ environment: this.config.environment,
721
+ });
722
+ // 创建获取 Token 的函数(支持静态 token 和动态获取)
723
+ const getAuthToken = config.authToken
724
+ ? () => config.authToken
725
+ : config.getAuthToken;
726
+ // 创建结账 API 客户端
727
+ this.checkoutAPI = new CheckoutAPI({
728
+ apiHost: this.config.apiHost,
729
+ getAuthToken,
730
+ debug: this.config.debug,
731
+ });
732
+ }
733
+ /**
734
+ * 获取客户端状态
735
+ */
736
+ getStatus() {
737
+ return this.status;
738
+ }
739
+ /**
740
+ * 检查是否已初始化
741
+ */
742
+ isReady() {
743
+ return this.status === 'ready';
744
+ }
745
+ /**
746
+ * 初始化客户端
747
+ * 加载 SeaArtPay SDK
748
+ */
749
+ async init() {
750
+ if (this.status === 'ready') {
751
+ return;
752
+ }
753
+ if (this.initPromise) {
754
+ return this.initPromise;
755
+ }
756
+ this.status = 'initializing';
757
+ this.log('初始化支付弹窗客户端...');
758
+ this.initPromise = this.doInit();
759
+ try {
760
+ await this.initPromise;
761
+ this.status = 'ready';
762
+ this.log('支付弹窗客户端初始化完成');
763
+ }
764
+ catch (error) {
765
+ this.status = 'error';
766
+ this.initPromise = null;
767
+ throw error;
768
+ }
769
+ }
770
+ async doInit() {
771
+ try {
772
+ await this.loader.load();
773
+ }
774
+ catch (error) {
775
+ throw this.createError('SDK_LOAD_FAILED', 'SeaArt 支付 SDK 加载失败', error);
776
+ }
777
+ }
778
+ /**
779
+ * 一次性购买
780
+ * @param options 购买选项
781
+ * @returns 结账结果(包含 transactionId)
782
+ */
783
+ async checkout(options) {
784
+ await this.ensureReady();
785
+ this.log('发起一次性购买:', options);
786
+ // 调用结账 API
787
+ const result = await this.checkoutAPI.createOneTimeCheckout({
788
+ productId: options.productId,
789
+ productName: options.productName,
790
+ price: options.price,
791
+ extra: options.extra,
792
+ });
793
+ // 自动打开支付弹窗
794
+ this.showPaymentModal({
795
+ transactionId: result.transactionId,
796
+ language: options.language,
797
+ userName: options.userName,
798
+ productName: options.productName,
799
+ onSuccess: options.onSuccess,
800
+ onUnsuccess: (unsuccessResult) => {
801
+ if (unsuccessResult.reason === 'cancel') {
802
+ options.onClose?.();
803
+ }
804
+ else {
805
+ options.onError?.({
806
+ code: 'PAYMENT_FAILED',
807
+ message: unsuccessResult.message,
808
+ });
809
+ }
810
+ },
811
+ });
812
+ return result;
813
+ }
814
+ /**
815
+ * 订阅购买
816
+ * @param options 订阅选项
817
+ * @returns 结账结果(包含 transactionId)
818
+ */
819
+ async subscribe(options) {
820
+ await this.ensureReady();
821
+ this.log('发起订阅购买:', options);
822
+ // 调用结账 API
823
+ const result = await this.checkoutAPI.createSubscriptionCheckout({
824
+ productId: options.productId,
825
+ productName: options.productName,
826
+ price: options.price,
827
+ subscription: {
828
+ period: options.period,
829
+ periodAmount: options.periodAmount,
830
+ firstDays: options.firstDays ?? 0,
831
+ },
832
+ extra: options.extra,
833
+ });
834
+ // 自动打开支付弹窗
835
+ this.showPaymentModal({
836
+ transactionId: result.transactionId,
837
+ language: options.language,
838
+ userName: options.userName,
839
+ productName: options.productName,
840
+ onSuccess: options.onSuccess,
841
+ onUnsuccess: (unsuccessResult) => {
842
+ if (unsuccessResult.reason === 'cancel') {
843
+ options.onClose?.();
844
+ }
845
+ else {
846
+ options.onError?.({
847
+ code: 'PAYMENT_FAILED',
848
+ message: unsuccessResult.message,
849
+ });
850
+ }
851
+ },
852
+ });
853
+ return result;
854
+ }
855
+ /**
856
+ * 直接打开支付弹窗
857
+ * 使用已有的 transactionId
858
+ */
859
+ showPayment(options) {
860
+ if (!this.isReady()) {
861
+ throw this.createError('SDK_NOT_READY', '支付 SDK 未初始化');
862
+ }
863
+ this.showPaymentModal(options);
864
+ }
865
+ /**
866
+ * 关闭支付弹窗
867
+ */
868
+ close() {
869
+ const sdk = this.getSDK();
870
+ if (sdk && typeof sdk.close === 'function') {
871
+ sdk.close();
872
+ this.log('支付弹窗已关闭');
873
+ }
874
+ }
875
+ /**
876
+ * 获取环境配置
877
+ */
878
+ getEnvironmentConfig() {
879
+ return ENV_CONFIG[this.config.environment];
880
+ }
881
+ // ==================== 私有方法 ====================
882
+ /**
883
+ * 确保客户端已就绪
884
+ */
885
+ async ensureReady() {
886
+ if (this.status === 'ready') {
887
+ return;
888
+ }
889
+ if (this.status === 'initializing' && this.initPromise) {
890
+ await this.initPromise;
891
+ return;
892
+ }
893
+ await this.init();
894
+ }
895
+ /**
896
+ * 获取 SDK 实例
897
+ */
898
+ getSDK() {
899
+ return this.loader.getSDK();
900
+ }
901
+ /**
902
+ * 显示支付弹窗
903
+ */
904
+ showPaymentModal(options) {
905
+ const sdk = this.getSDK();
906
+ if (!sdk) {
907
+ throw this.createError('SDK_NOT_READY', '支付 SDK 未加载');
908
+ }
909
+ this.log('打开支付弹窗:', options.transactionId);
910
+ sdk.show({
911
+ transactionId: options.transactionId,
912
+ language: options.language ?? DEFAULT_CHECKOUT_CONFIG.language,
913
+ userName: options.userName,
914
+ productName: options.productName,
915
+ from: options.from,
916
+ onSuccess: (result) => {
917
+ this.log('支付成功:', result);
918
+ options.onSuccess?.({
919
+ transactionId: options.transactionId,
920
+ status: 'success',
921
+ data: result,
922
+ });
923
+ },
924
+ onUnsuccess: (result) => {
925
+ this.log('支付未成功:', result);
926
+ const unsuccessResult = result;
927
+ options.onUnsuccess?.({
928
+ reason: unsuccessResult.reason ?? 'error',
929
+ message: unsuccessResult.message ?? '支付未完成',
930
+ transactionId: options.transactionId,
931
+ });
932
+ },
933
+ });
934
+ }
935
+ /**
936
+ * 创建支付错误
937
+ */
938
+ createError(code, message, cause) {
939
+ return { code, message, cause };
940
+ }
941
+ /**
942
+ * 日志输出
943
+ */
944
+ log(message, data) {
945
+ if (this.config.debug) {
946
+ console.log(`[PaymentCheckoutClient] ${message}`, data ?? '');
947
+ }
948
+ }
949
+ }
950
+
951
+ /**
952
+ * 工具函数模块
953
+ */
954
+ /**
955
+ * 生成唯一的订单引用号
956
+ * @param prefix 前缀
957
+ * @returns 唯一引用号
958
+ */
959
+ function generateOrderReference(prefix = 'sv') {
960
+ const timestamp = Date.now();
961
+ const random = Math.random().toString(36).substring(2, 8);
962
+ return `${prefix}-${timestamp}-${random}`;
963
+ }
964
+ /**
965
+ * 将美元金额转换为分
966
+ * @param dollars 美元金额
967
+ * @returns 分
968
+ */
969
+ function dollarsToCents(dollars) {
970
+ return Math.round(dollars * 100);
971
+ }
972
+ /**
973
+ * 将分转换为美元
974
+ * @param cents 分
975
+ * @returns 美元金额
976
+ */
977
+ function centsToDollars(cents) {
978
+ return cents / 100;
979
+ }
980
+ /**
981
+ * 格式化价格显示
982
+ * @param amount 金额(美元)
983
+ * @param currency 货币代码
984
+ * @returns 格式化的价格字符串
985
+ */
986
+ function formatPrice(amount, currency = 'USD') {
987
+ const symbols = {
988
+ USD: '$',
989
+ CNY: '¥',
990
+ EUR: '€',
991
+ GBP: '£',
992
+ JPY: '¥',
993
+ KRW: '₩',
994
+ };
995
+ const symbol = symbols[currency] || currency + ' ';
996
+ return `${symbol}${amount.toFixed(2)}`;
997
+ }
998
+ /**
999
+ * 创建支付错误对象
1000
+ * @param code 错误代码
1001
+ * @param message 错误消息
1002
+ * @param cause 原始错误
1003
+ * @returns 支付错误对象
1004
+ */
1005
+ function createCheckoutPaymentError(code, message, cause) {
1006
+ return { code, message, cause };
1007
+ }
1008
+ /**
1009
+ * 检查是否是支付错误
1010
+ * @param error 错误对象
1011
+ * @returns 是否是支付错误
1012
+ */
1013
+ function isCheckoutPaymentError(error) {
1014
+ return (typeof error === 'object' &&
1015
+ error !== null &&
1016
+ 'code' in error &&
1017
+ 'message' in error);
1018
+ }
1019
+ /**
1020
+ * 延迟函数
1021
+ * @param ms 毫秒
1022
+ * @returns Promise
1023
+ */
1024
+ function delay(ms) {
1025
+ return new Promise((resolve) => setTimeout(resolve, ms));
1026
+ }
1027
+ /**
1028
+ * 带超时的 Promise
1029
+ * @param promise 原始 Promise
1030
+ * @param ms 超时毫秒数
1031
+ * @param errorMessage 超时错误消息
1032
+ * @returns 带超时的 Promise
1033
+ */
1034
+ function withTimeout(promise, ms, errorMessage = 'Operation timed out') {
1035
+ return Promise.race([
1036
+ promise,
1037
+ new Promise((_, reject) => {
1038
+ setTimeout(() => reject(new Error(errorMessage)), ms);
1039
+ }),
1040
+ ]);
1041
+ }
1042
+ /**
1043
+ * 安全的 JSON 解析
1044
+ * @param json JSON 字符串
1045
+ * @param defaultValue 默认值
1046
+ * @returns 解析结果或默认值
1047
+ */
1048
+ function safeJsonParse(json, defaultValue) {
1049
+ try {
1050
+ return JSON.parse(json);
1051
+ }
1052
+ catch {
1053
+ return defaultValue;
1054
+ }
1055
+ }
1056
+ /**
1057
+ * 检查是否在浏览器环境
1058
+ */
1059
+ function isBrowser() {
1060
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
1061
+ }
1062
+ /**
1063
+ * 获取当前页面 URL
1064
+ */
1065
+ function getCurrentUrl() {
1066
+ if (!isBrowser())
1067
+ return '';
1068
+ return window.location.href;
1069
+ }
1070
+ /**
1071
+ * Locale 映射(项目语言到 SDK 语言)
1072
+ */
1073
+ const LOCALE_MAP = {
1074
+ 'zh-CN': 'zh-CN',
1075
+ 'zh-TW': 'zh-TW',
1076
+ zh: 'zh-CN',
1077
+ en: 'en-US',
1078
+ 'en-US': 'en-US',
1079
+ ko: 'ko-KR',
1080
+ 'ko-KR': 'ko-KR',
1081
+ ja: 'ja-JP',
1082
+ 'ja-JP': 'ja-JP',
1083
+ es: 'es-ES',
1084
+ fr: 'fr-FR',
1085
+ de: 'de-DE',
1086
+ };
1087
+ /**
1088
+ * 获取 SDK 支持的 locale
1089
+ * @param locale 项目 locale
1090
+ * @returns SDK locale
1091
+ */
1092
+ function getSDKLocale(locale) {
1093
+ return LOCALE_MAP[locale] || LOCALE_MAP['en'];
1094
+ }
1095
+
1096
+ /**
1097
+ * @seaverse/payment-sdk
1098
+ *
1099
+ * SeaVerse Payment SDK - Credit management and payment checkout
1100
+ *
1101
+ * This SDK provides two main features:
1102
+ *
1103
+ * ## 1. Credit Query (PaymentClient)
1104
+ * Query user credit accounts and transaction history.
1105
+ *
1106
+ * ```typescript
1107
+ * import { PaymentClient } from '@seaverse/payment-sdk';
1108
+ *
1109
+ * const client = new PaymentClient({
1110
+ * appId: 'seaverse',
1111
+ * token: 'your-bearer-token',
1112
+ * });
1113
+ *
1114
+ * const detail = await client.getCreditDetail();
1115
+ * console.log(`Total Balance: ${detail.total_balance}`);
1116
+ * ```
1117
+ *
1118
+ * ## 2. Payment Checkout (PaymentCheckoutClient)
1119
+ * Show payment modal for one-time purchases and subscriptions.
1120
+ *
1121
+ * ```typescript
1122
+ * import { PaymentCheckoutClient } from '@seaverse/payment-sdk';
1123
+ *
1124
+ * const checkoutClient = new PaymentCheckoutClient({
1125
+ * apiHost: 'https://payment.sg.seaverse.dev',
1126
+ * authToken: 'your-jwt-token',
1127
+ * environment: 'develop',
1128
+ * });
1129
+ *
1130
+ * await checkoutClient.init();
1131
+ *
1132
+ * await checkoutClient.checkout({
1133
+ * productId: 'pkg_starter',
1134
+ * onSuccess: (res) => console.log('Payment success:', res),
1135
+ * });
1136
+ * ```
1137
+ */
1138
+ // ============================================================================
1139
+ // Credit Query API (PaymentClient)
1140
+ // ============================================================================
1141
+ // ============================================================================
1142
+ // Version
1143
+ // ============================================================================
1144
+ /**
1145
+ * SDK version
1146
+ */
1147
+ const VERSION = '0.1.0';
1148
+
1149
+ exports.API_ENDPOINTS = API_ENDPOINTS;
1150
+ exports.BIZ_CODE = BIZ_CODE;
1151
+ exports.CheckoutAPI = CheckoutAPI;
1152
+ exports.DEFAULT_CHECKOUT_CONFIG = DEFAULT_CHECKOUT_CONFIG;
1153
+ exports.ENV_CONFIG = ENV_CONFIG;
1154
+ exports.HTTP_STATUS = HTTP_STATUS;
1155
+ exports.PaymentAPIError = PaymentAPIError;
1156
+ exports.PaymentCheckoutClient = PaymentCheckoutClient;
1157
+ exports.PaymentClient = PaymentClient;
1158
+ exports.SDK_CDN_URL = SDK_CDN_URL;
1159
+ exports.SDK_GLOBAL_NAME = SDK_GLOBAL_NAME;
1160
+ exports.SDK_LOAD_TIMEOUT = SDK_LOAD_TIMEOUT;
1161
+ exports.SeaArtPayLoader = SeaArtPayLoader;
1162
+ exports.VERSION = VERSION;
1163
+ exports.centsToDollars = centsToDollars;
1164
+ exports.createCheckoutPaymentError = createCheckoutPaymentError;
1165
+ exports.delay = delay;
1166
+ exports.dollarsToCents = dollarsToCents;
1167
+ exports.formatPrice = formatPrice;
1168
+ exports.generateOrderReference = generateOrderReference;
1169
+ exports.getCurrentUrl = getCurrentUrl;
1170
+ exports.getGlobalLoader = getGlobalLoader;
1171
+ exports.getSDKLocale = getSDKLocale;
1172
+ exports.isBrowser = isBrowser;
1173
+ exports.isCheckoutPaymentError = isCheckoutPaymentError;
1174
+ exports.resetGlobalLoader = resetGlobalLoader;
1175
+ exports.safeJsonParse = safeJsonParse;
1176
+ exports.withTimeout = withTimeout;
1177
+ //# sourceMappingURL=index.cjs.map