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