@seaverse/payment-sdk 0.7.0 → 0.8.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 CHANGED
@@ -1093,6 +1093,4068 @@ function getSDKLocale(locale) {
1093
1093
  return LOCALE_MAP[locale] || LOCALE_MAP['en'];
1094
1094
  }
1095
1095
 
1096
+ /**
1097
+ * Payment Storage
1098
+ * 支付数据存储 - 统一管理 localStorage 操作
1099
+ */
1100
+ class PaymentStorage {
1101
+ /**
1102
+ * 保存交易数据
1103
+ */
1104
+ static saveTransaction(data) {
1105
+ const storageData = {
1106
+ transactionId: data.transactionId,
1107
+ orderId: data.orderId,
1108
+ createdAt: Date.now(),
1109
+ expiresAt: Date.now() + data.expiresIn,
1110
+ };
1111
+ try {
1112
+ localStorage.setItem(this.KEY, JSON.stringify(storageData));
1113
+ console.log('[PaymentStorage] Transaction saved:', {
1114
+ transactionId: data.transactionId,
1115
+ expiresIn: `${Math.round(data.expiresIn / 1000)}s`,
1116
+ });
1117
+ }
1118
+ catch (error) {
1119
+ console.error('[PaymentStorage] Failed to save transaction:', error);
1120
+ }
1121
+ }
1122
+ /**
1123
+ * 获取交易数据
1124
+ */
1125
+ static getTransaction() {
1126
+ try {
1127
+ const stored = localStorage.getItem(this.KEY);
1128
+ if (!stored) {
1129
+ return null;
1130
+ }
1131
+ const data = JSON.parse(stored);
1132
+ // 检查是否过期
1133
+ if (Date.now() > data.expiresAt) {
1134
+ console.warn('[PaymentStorage] Transaction expired, removing');
1135
+ this.clearTransaction();
1136
+ return null;
1137
+ }
1138
+ const remainingSeconds = Math.round((data.expiresAt - Date.now()) / 1000);
1139
+ console.log('[PaymentStorage] Transaction retrieved:', {
1140
+ transactionId: data.transactionId,
1141
+ remainingTime: `${remainingSeconds}s`,
1142
+ });
1143
+ return data;
1144
+ }
1145
+ catch (error) {
1146
+ console.error('[PaymentStorage] Failed to get transaction:', error);
1147
+ return null;
1148
+ }
1149
+ }
1150
+ /**
1151
+ * 清除交易数据
1152
+ */
1153
+ static clearTransaction() {
1154
+ try {
1155
+ localStorage.removeItem(this.KEY);
1156
+ console.log('[PaymentStorage] Transaction cleared');
1157
+ }
1158
+ catch (error) {
1159
+ console.error('[PaymentStorage] Failed to clear transaction:', error);
1160
+ }
1161
+ }
1162
+ }
1163
+ PaymentStorage.KEY = 'seaverse_payment_transaction';
1164
+
1165
+ /**
1166
+ * LinkPaymentComponent
1167
+ * Link 支付组件(跳转支付)- payment_type = 1
1168
+ */
1169
+ class LinkPaymentComponent {
1170
+ constructor(params) {
1171
+ this.linkPayment = null;
1172
+ this.paymentInstance = params.paymentInstance;
1173
+ this.orderId = params.orderId;
1174
+ this.accountToken = params.accountToken;
1175
+ this.paymentMethod = params.paymentMethod;
1176
+ this.callbackUrl = params.callbackUrl;
1177
+ console.log('[LinkPaymentComponent] Created:', {
1178
+ orderId: this.orderId,
1179
+ paymentMethod: this.paymentMethod.payment_method_name,
1180
+ callbackUrl: this.callbackUrl,
1181
+ });
1182
+ }
1183
+ /**
1184
+ * 创建订单并获取支付链接
1185
+ */
1186
+ async createOrder() {
1187
+ console.log('[LinkPaymentComponent] Creating order...');
1188
+ try {
1189
+ // 创建 Link 支付实例
1190
+ this.linkPayment = this.paymentInstance.createLink(this.paymentMethod, {
1191
+ callback_url: this.callbackUrl,
1192
+ });
1193
+ // 创建订单
1194
+ const result = await this.linkPayment.createOrder();
1195
+ console.log('[LinkPaymentComponent] Order created:', {
1196
+ transactionId: result.data.transaction_id,
1197
+ });
1198
+ // 保存 transactionId 到 localStorage(30 分钟过期)
1199
+ PaymentStorage.saveTransaction({
1200
+ transactionId: result.data.transaction_id,
1201
+ orderId: this.orderId,
1202
+ expiresIn: 30 * 60 * 1000, // 30 分钟
1203
+ });
1204
+ return result;
1205
+ }
1206
+ catch (error) {
1207
+ console.error('[LinkPaymentComponent] Create order failed:', error);
1208
+ throw error;
1209
+ }
1210
+ }
1211
+ /**
1212
+ * 跳转到支付页面(当前窗口)
1213
+ */
1214
+ async redirectToPayment() {
1215
+ console.log('[LinkPaymentComponent] Redirecting to payment...');
1216
+ try {
1217
+ const result = await this.createOrder();
1218
+ const payment_url = result.data?.payment_url;
1219
+ console.log('[LinkPaymentComponent] Redirecting to:', payment_url);
1220
+ window.location.href = payment_url;
1221
+ }
1222
+ catch (error) {
1223
+ console.error('[LinkPaymentComponent] Redirect failed:', error);
1224
+ throw error;
1225
+ }
1226
+ }
1227
+ /**
1228
+ * 新窗口打开支付页面
1229
+ */
1230
+ async openPayment(windowFeatures = 'width=850,height=680') {
1231
+ console.log('[LinkPaymentComponent] Opening payment in new window...');
1232
+ try {
1233
+ const result = await this.createOrder();
1234
+ const payment_url = result.data?.payment_url;
1235
+ console.log('[LinkPaymentComponent] Opening URL:', payment_url);
1236
+ const newWindow = window.open(payment_url, '_blank', windowFeatures);
1237
+ if (!newWindow) {
1238
+ console.warn('[LinkPaymentComponent] Failed to open new window (popup blocked?)');
1239
+ }
1240
+ return newWindow;
1241
+ }
1242
+ catch (error) {
1243
+ console.error('[LinkPaymentComponent] Open payment failed:', error);
1244
+ throw error;
1245
+ }
1246
+ }
1247
+ }
1248
+
1249
+ /**
1250
+ * DropinPaymentComponent
1251
+ * Dropin 支付组件(嵌入式支付)- payment_type = 2
1252
+ */
1253
+ class DropinPaymentComponent {
1254
+ constructor(params) {
1255
+ this.dropinPayment = null;
1256
+ this.rendered = false;
1257
+ this.paymentInstance = params.paymentInstance;
1258
+ this.orderId = params.orderId;
1259
+ this.accountToken = params.accountToken;
1260
+ this.paymentMethod = params.paymentMethod;
1261
+ this.containerId = params.containerId;
1262
+ this.callbacks = {
1263
+ onSubmit: params.onSubmit,
1264
+ onError: params.onError,
1265
+ onCreateOrder: params.onCreateOrder,
1266
+ onCompleted: params.onCompleted,
1267
+ onFailed: params.onFailed,
1268
+ onLoading: params.onLoading,
1269
+ };
1270
+ console.log('[DropinPaymentComponent] Created:', {
1271
+ orderId: this.orderId,
1272
+ paymentMethod: this.paymentMethod.payment_method_name,
1273
+ containerId: this.containerId,
1274
+ });
1275
+ }
1276
+ /**
1277
+ * 渲染支付组件到容器
1278
+ */
1279
+ async render() {
1280
+ if (this.rendered) {
1281
+ console.warn('[DropinPaymentComponent] Already rendered');
1282
+ return;
1283
+ }
1284
+ console.log('[DropinPaymentComponent] Rendering...');
1285
+ try {
1286
+ // 检查容器是否存在
1287
+ const container = document.querySelector(this.containerId);
1288
+ if (!container) {
1289
+ throw new Error(`Container not found: ${this.containerId}`);
1290
+ }
1291
+ // 创建 Dropin 支付实例
1292
+ this.dropinPayment = this.paymentInstance.createDropin(this.paymentMethod, {
1293
+ onSubmit: (payload) => {
1294
+ console.log('[DropinPaymentComponent] onSubmit:', payload);
1295
+ this.callbacks.onSubmit?.(payload);
1296
+ },
1297
+ onError: (payload, error) => {
1298
+ console.error('[DropinPaymentComponent] onError:', error);
1299
+ this.callbacks.onError?.(payload, error);
1300
+ },
1301
+ onCreateOrder: (payload) => {
1302
+ console.log('[DropinPaymentComponent] onCreateOrder:', payload);
1303
+ // 保存 transactionId
1304
+ if (payload.transactionId || payload.transaction_id) {
1305
+ PaymentStorage.saveTransaction({
1306
+ transactionId: payload.transactionId || payload.transaction_id,
1307
+ orderId: this.orderId,
1308
+ expiresIn: 30 * 60 * 1000, // 30分钟
1309
+ });
1310
+ }
1311
+ this.callbacks.onCreateOrder?.(payload);
1312
+ },
1313
+ onCompleted: (payload) => {
1314
+ console.log('[DropinPaymentComponent] onCompleted:', payload);
1315
+ this.callbacks.onCompleted?.(payload);
1316
+ },
1317
+ onFailed: (payload) => {
1318
+ console.error('[DropinPaymentComponent] onFailed:', payload);
1319
+ this.callbacks.onFailed?.(payload);
1320
+ },
1321
+ onLoading: (loading) => {
1322
+ console.log('[DropinPaymentComponent] onLoading:', loading);
1323
+ this.callbacks.onLoading?.(loading);
1324
+ },
1325
+ });
1326
+ // 渲染到容器
1327
+ await this.dropinPayment.render(this.containerId);
1328
+ this.rendered = true;
1329
+ console.log('[DropinPaymentComponent] Rendered successfully');
1330
+ }
1331
+ catch (error) {
1332
+ console.error('[DropinPaymentComponent] Render failed:', error);
1333
+ throw error;
1334
+ }
1335
+ }
1336
+ /**
1337
+ * 销毁组件
1338
+ */
1339
+ destroy() {
1340
+ if (!this.rendered || !this.dropinPayment) {
1341
+ console.warn('[DropinPaymentComponent] Not rendered, nothing to destroy');
1342
+ return;
1343
+ }
1344
+ try {
1345
+ // 调用 SDK 的销毁方法(防御性检查)
1346
+ if (typeof this.dropinPayment.destroy === 'function') {
1347
+ this.dropinPayment.destroy();
1348
+ }
1349
+ else {
1350
+ console.warn('[DropinPaymentComponent] Dropin payment instance does not have destroy method, skipping');
1351
+ }
1352
+ this.dropinPayment = null;
1353
+ this.rendered = false;
1354
+ console.log('[DropinPaymentComponent] Destroyed');
1355
+ }
1356
+ catch (error) {
1357
+ console.error('[DropinPaymentComponent] Destroy failed:', error);
1358
+ // 即使销毁失败,也清理引用
1359
+ this.dropinPayment = null;
1360
+ this.rendered = false;
1361
+ }
1362
+ }
1363
+ }
1364
+
1365
+ /**
1366
+ * BindCardPaymentComponent
1367
+ * BindCard 支付组件(已绑定卡片支付)
1368
+ */
1369
+ class BindCardPaymentComponent {
1370
+ constructor(params) {
1371
+ this.bindCardPayment = null;
1372
+ this.paymentInstance = params.paymentInstance;
1373
+ this.orderId = params.orderId;
1374
+ this.paymentMethod = params.paymentMethod;
1375
+ this.callbacks = {
1376
+ onCompleted: params.onCompleted,
1377
+ onFailed: params.onFailed,
1378
+ };
1379
+ console.log('[BindCardPaymentComponent] Created:', {
1380
+ orderId: this.orderId,
1381
+ paymentMethod: this.paymentMethod.payment_method_name,
1382
+ });
1383
+ }
1384
+ /**
1385
+ * 获取用户已绑定的卡列表
1386
+ */
1387
+ async getCardList() {
1388
+ console.log('[BindCardPaymentComponent] Getting card list...');
1389
+ try {
1390
+ // 创建 BindCard 支付实例
1391
+ this.bindCardPayment = this.paymentInstance.createBindCard(this.paymentMethod, {
1392
+ onCompleted: this.callbacks.onCompleted,
1393
+ onFailed: this.callbacks.onFailed,
1394
+ });
1395
+ // 获取卡列表
1396
+ const result = await this.bindCardPayment.getCardList();
1397
+ if (result.err) {
1398
+ throw new Error(result.message || 'Failed to get card list');
1399
+ }
1400
+ const cardList = result.data?.user_bind_info_list || [];
1401
+ console.log('[BindCardPaymentComponent] Card list retrieved:', {
1402
+ count: cardList.length,
1403
+ });
1404
+ return cardList;
1405
+ }
1406
+ catch (error) {
1407
+ console.error('[BindCardPaymentComponent] Get card list failed:', error);
1408
+ throw error;
1409
+ }
1410
+ }
1411
+ /**
1412
+ * 使用已绑定的卡创建订单
1413
+ */
1414
+ async createOrder(instrumentId) {
1415
+ console.log('[BindCardPaymentComponent] Creating order with card:', instrumentId);
1416
+ try {
1417
+ // 创建 BindCard 支付实例(如果还没有创建)
1418
+ if (!this.bindCardPayment) {
1419
+ this.bindCardPayment = this.paymentInstance.createBindCard(this.paymentMethod, {
1420
+ onCompleted: (payload) => {
1421
+ console.log('[BindCardPaymentComponent] onCompleted:', payload);
1422
+ this.callbacks.onCompleted?.(payload);
1423
+ },
1424
+ onFailed: (payload) => {
1425
+ console.error('[BindCardPaymentComponent] onFailed:', payload);
1426
+ this.callbacks.onFailed?.(payload);
1427
+ },
1428
+ });
1429
+ }
1430
+ // 创建订单
1431
+ await this.bindCardPayment.createOrder(instrumentId);
1432
+ console.log('[BindCardPaymentComponent] Order created');
1433
+ }
1434
+ catch (error) {
1435
+ console.error('[BindCardPaymentComponent] Create order failed:', error);
1436
+ throw error;
1437
+ }
1438
+ }
1439
+ }
1440
+
1441
+ /**
1442
+ * OrderPayment
1443
+ * 订单支付实例 - 为每个订单创建独立的支付实例
1444
+ */
1445
+ class OrderPayment {
1446
+ constructor(params) {
1447
+ this.paymentInstance = params.paymentInstance;
1448
+ this.orderId = params.orderId;
1449
+ this.accountToken = params.accountToken;
1450
+ console.log('[OrderPayment] Created:', {
1451
+ orderId: this.orderId,
1452
+ hasAccountToken: !!this.accountToken,
1453
+ });
1454
+ }
1455
+ /**
1456
+ * 获取订单信息
1457
+ */
1458
+ async getOrderInfo() {
1459
+ console.log('[OrderPayment] Getting order info...');
1460
+ try {
1461
+ const result = await this.paymentInstance.getOrderInfo();
1462
+ if (result.err) {
1463
+ throw new Error(result.message || 'Failed to get order info');
1464
+ }
1465
+ const orderInfo = result.data?.order_info;
1466
+ if (!orderInfo) {
1467
+ throw new Error('No order info returned');
1468
+ }
1469
+ // 检查订单状态
1470
+ if (orderInfo.order_status !== 1) {
1471
+ console.warn('[OrderPayment] Order status is not pending:', orderInfo.order_status);
1472
+ }
1473
+ console.log('[OrderPayment] Order info retrieved:', {
1474
+ orderId: orderInfo.sys_order_id,
1475
+ status: orderInfo.order_status,
1476
+ expireTime: orderInfo.expire_time,
1477
+ });
1478
+ return orderInfo;
1479
+ }
1480
+ catch (error) {
1481
+ console.error('[OrderPayment] Get order info failed:', error);
1482
+ throw error;
1483
+ }
1484
+ }
1485
+ /**
1486
+ * 创建 Link 支付组件(按需加载)
1487
+ * 适用于 payment_type = 1
1488
+ */
1489
+ createLinkPayment(paymentMethod, options) {
1490
+ if (paymentMethod.payment_type !== 1) {
1491
+ throw new Error('[OrderPayment] Payment method is not Link type (payment_type should be 1)');
1492
+ }
1493
+ return new LinkPaymentComponent({
1494
+ paymentInstance: this.paymentInstance,
1495
+ orderId: this.orderId,
1496
+ accountToken: this.accountToken,
1497
+ paymentMethod,
1498
+ callbackUrl: options.callback_url,
1499
+ });
1500
+ }
1501
+ /**
1502
+ * 创建 Dropin 支付组件(按需加载)
1503
+ * 适用于 payment_type = 2
1504
+ */
1505
+ createDropinPayment(paymentMethod, options) {
1506
+ if (paymentMethod.payment_type !== 2) {
1507
+ throw new Error('[OrderPayment] Payment method is not Dropin type (payment_type should be 2)');
1508
+ }
1509
+ return new DropinPaymentComponent({
1510
+ paymentInstance: this.paymentInstance,
1511
+ orderId: this.orderId,
1512
+ accountToken: this.accountToken,
1513
+ paymentMethod,
1514
+ ...options,
1515
+ });
1516
+ }
1517
+ /**
1518
+ * 创建 BindCard 支付组件(按需加载)
1519
+ * 适用于 payment_type = 2 且支持绑卡的支付方式
1520
+ */
1521
+ createBindCardPayment(paymentMethod, options) {
1522
+ if (paymentMethod.payment_type !== 2) {
1523
+ throw new Error('[OrderPayment] Payment method is not Dropin type (payment_type should be 2)');
1524
+ }
1525
+ return new BindCardPaymentComponent({
1526
+ paymentInstance: this.paymentInstance,
1527
+ orderId: this.orderId,
1528
+ accountToken: this.accountToken,
1529
+ paymentMethod,
1530
+ ...options,
1531
+ });
1532
+ }
1533
+ }
1534
+
1535
+ /**
1536
+ * Error Handler
1537
+ * 错误处理器 - 标准化错误和友好提示
1538
+ */
1539
+ /**
1540
+ * 支付错误类
1541
+ */
1542
+ class PaymentError extends Error {
1543
+ constructor(code, bizCode, message) {
1544
+ super(message);
1545
+ this.code = code;
1546
+ this.bizCode = bizCode;
1547
+ this.name = 'PaymentError';
1548
+ }
1549
+ }
1550
+ class ErrorHandler {
1551
+ /**
1552
+ * 标准化错误
1553
+ */
1554
+ static normalize(error) {
1555
+ if (error instanceof PaymentError) {
1556
+ return error;
1557
+ }
1558
+ // 从错误对象中提取信息
1559
+ const code = error.code || error.status || -1;
1560
+ const bizCode = error.bizCode || error.business_code;
1561
+ const message = error.message || error.msg || 'Unknown payment error';
1562
+ return new PaymentError(code, bizCode, message);
1563
+ }
1564
+ /**
1565
+ * 根据错误码获取友好提示
1566
+ */
1567
+ static getFriendlyMessage(error) {
1568
+ // 发行服务错误码
1569
+ const errorMessages = {
1570
+ 300000: '请稍后重试或切换支付方式',
1571
+ 301000: '系统异常,请稍后再试',
1572
+ 302000: '卡授权状态异常',
1573
+ 303000: '交易异常识别高风险',
1574
+ 304000: '交易金额超范围限制,使用其它支付方式',
1575
+ 305000: '支付交易被拒绝',
1576
+ 306000: '退款失败',
1577
+ 307000: '支付验证错误',
1578
+ 308000: '账户余额不足',
1579
+ 309000: '商户服务可用性异常',
1580
+ };
1581
+ return errorMessages[error.code] || error.message;
1582
+ }
1583
+ }
1584
+
1585
+ /**
1586
+ * Script Loader
1587
+ * 动态加载 SeaartPaymentComponent SDK 脚本
1588
+ */
1589
+ class ScriptLoader {
1590
+ /**
1591
+ * 动态加载 SeaartPaymentComponent SDK
1592
+ */
1593
+ static async loadSeaartPaymentComponent(options) {
1594
+ const { scriptUrl, timeout = 10000 } = options;
1595
+ // 1. 检查是否已经加载
1596
+ if (this.loadedScripts.has(scriptUrl) && window.SeaartPaymentComponent) {
1597
+ console.log('[ScriptLoader] SeaartPaymentComponent already loaded');
1598
+ return;
1599
+ }
1600
+ // 2. 检查脚本标签是否已存在
1601
+ const existingScript = document.querySelector(`script[src="${scriptUrl}"]`);
1602
+ if (existingScript) {
1603
+ console.log('[ScriptLoader] Script tag exists, waiting for load...');
1604
+ return this.waitForSeaartPaymentComponent(timeout);
1605
+ }
1606
+ // 3. 创建并注入脚本标签
1607
+ console.log('[ScriptLoader] Loading script:', scriptUrl);
1608
+ return new Promise((resolve, reject) => {
1609
+ const script = document.createElement('script');
1610
+ script.src = scriptUrl;
1611
+ script.defer = true;
1612
+ // 超时处理
1613
+ const timeoutId = setTimeout(() => {
1614
+ reject(new Error(`[ScriptLoader] Script load timeout after ${timeout}ms`));
1615
+ }, timeout);
1616
+ // 加载成功
1617
+ script.onload = async () => {
1618
+ clearTimeout(timeoutId);
1619
+ console.log('[ScriptLoader] Script loaded');
1620
+ try {
1621
+ // 等待 window.SeaartPaymentComponent 可用
1622
+ await this.waitForSeaartPaymentComponent(timeout);
1623
+ this.loadedScripts.add(scriptUrl);
1624
+ resolve();
1625
+ }
1626
+ catch (error) {
1627
+ reject(error);
1628
+ }
1629
+ };
1630
+ // 加载失败
1631
+ script.onerror = () => {
1632
+ clearTimeout(timeoutId);
1633
+ reject(new Error('[ScriptLoader] Failed to load script'));
1634
+ };
1635
+ // 添加到 DOM
1636
+ document.head.appendChild(script);
1637
+ });
1638
+ }
1639
+ /**
1640
+ * 等待 window.SeaartPaymentComponent 可用
1641
+ */
1642
+ static async waitForSeaartPaymentComponent(timeout) {
1643
+ const startTime = Date.now();
1644
+ const checkInterval = 100; // 每 100ms 检查一次
1645
+ return new Promise((resolve, reject) => {
1646
+ const check = () => {
1647
+ // 检查是否超时
1648
+ if (Date.now() - startTime > timeout) {
1649
+ reject(new Error('[ScriptLoader] window.SeaartPaymentComponent not available'));
1650
+ return;
1651
+ }
1652
+ // 检查是否可用
1653
+ if (window.SeaartPaymentComponent) {
1654
+ console.log('[ScriptLoader] window.SeaartPaymentComponent is available');
1655
+ resolve();
1656
+ return;
1657
+ }
1658
+ // 继续等待
1659
+ setTimeout(check, checkInterval);
1660
+ };
1661
+ check();
1662
+ });
1663
+ }
1664
+ /**
1665
+ * 清除已加载的脚本记录(测试用)
1666
+ */
1667
+ static clearLoadedScripts() {
1668
+ this.loadedScripts.clear();
1669
+ }
1670
+ }
1671
+ ScriptLoader.loadedScripts = new Set();
1672
+ /**
1673
+ * Stylesheet Loader
1674
+ * 动态加载 CSS 样式表
1675
+ */
1676
+ class StylesheetLoader {
1677
+ /**
1678
+ * 加载支付组件样式表
1679
+ * @param url CSS 文件 URL
1680
+ * @returns Promise<void>
1681
+ */
1682
+ static async loadPaymentStylesheet(url) {
1683
+ console.log('[StylesheetLoader] Loading stylesheet:', url);
1684
+ // 检查是否已加载
1685
+ if (this.loadedStylesheets.has(url)) {
1686
+ console.log('[StylesheetLoader] Stylesheet already loaded (cached)');
1687
+ return;
1688
+ }
1689
+ // 检查 DOM 中是否已存在
1690
+ const existingLink = document.querySelector(`link[href="${url}"]`);
1691
+ if (existingLink) {
1692
+ console.log('[StylesheetLoader] Stylesheet already exists in DOM');
1693
+ this.loadedStylesheets.add(url);
1694
+ return;
1695
+ }
1696
+ // 创建并注入 link 标签
1697
+ return new Promise((resolve, reject) => {
1698
+ const link = document.createElement('link');
1699
+ link.rel = 'stylesheet';
1700
+ link.href = url;
1701
+ link.onload = () => {
1702
+ console.log('[StylesheetLoader] Stylesheet loaded successfully:', url);
1703
+ this.loadedStylesheets.add(url);
1704
+ resolve();
1705
+ };
1706
+ link.onerror = () => {
1707
+ const error = new Error(`Failed to load stylesheet: ${url}`);
1708
+ console.error('[StylesheetLoader] Load failed:', error);
1709
+ reject(error);
1710
+ };
1711
+ document.head.appendChild(link);
1712
+ console.log('[StylesheetLoader] Link tag appended to head');
1713
+ });
1714
+ }
1715
+ /**
1716
+ * 清除缓存(用于测试)
1717
+ */
1718
+ static clearCache() {
1719
+ this.loadedStylesheets.clear();
1720
+ console.log('[StylesheetLoader] Cache cleared');
1721
+ }
1722
+ }
1723
+ /**
1724
+ * 已加载的样式表 URL 缓存
1725
+ */
1726
+ StylesheetLoader.loadedStylesheets = new Set();
1727
+
1728
+ /**
1729
+ * Order API
1730
+ * 订单管理 - 查询和轮询订单状态
1731
+ */
1732
+ /**
1733
+ * 查询订单状态
1734
+ * @param transactionId SDK checkout 返回的 transaction_id
1735
+ * @param apiHost Payment API 地址
1736
+ * @param authToken 认证 Token
1737
+ * @returns 订单状态
1738
+ */
1739
+ async function checkOrderStatus(transactionId, apiHost, authToken) {
1740
+ if (!authToken) {
1741
+ throw new Error('Authentication token is required');
1742
+ }
1743
+ const response = await fetch(`${apiHost}/api/v1/payment/sdk/order/status`, {
1744
+ method: 'POST',
1745
+ headers: {
1746
+ 'Content-Type': 'application/json',
1747
+ Authorization: `Bearer ${authToken}`,
1748
+ },
1749
+ body: JSON.stringify({
1750
+ transaction_id: transactionId,
1751
+ }),
1752
+ });
1753
+ if (!response.ok) {
1754
+ throw new Error(`Failed to check order status: ${response.status} ${response.statusText}`);
1755
+ }
1756
+ const result = await response.json();
1757
+ if (result.code !== 0 || !result.data) {
1758
+ throw new Error(result.msg || 'Failed to check order status');
1759
+ }
1760
+ return result.data.status;
1761
+ }
1762
+ /**
1763
+ * 轮询订单状态直到支付完成或超时
1764
+ * @param transactionId SDK checkout 返回的 transaction_id
1765
+ * @param apiHost Payment API 地址
1766
+ * @param authToken 认证 Token
1767
+ * @param options 轮询配置
1768
+ * @returns 最终订单状态
1769
+ */
1770
+ async function pollOrderStatus(transactionId, apiHost, authToken, options = {}) {
1771
+ const { interval = 2000, maxAttempts = 30, onPoll } = options;
1772
+ let attempt = 0;
1773
+ console.log('[OrderAPI] Starting poll:', {
1774
+ transactionId,
1775
+ interval: `${interval}ms`,
1776
+ maxAttempts,
1777
+ });
1778
+ while (attempt < maxAttempts) {
1779
+ attempt++;
1780
+ try {
1781
+ const status = await checkOrderStatus(transactionId, apiHost, authToken);
1782
+ console.log(`[OrderAPI] Poll attempt ${attempt}/${maxAttempts}, status: ${status}`);
1783
+ // 回调
1784
+ if (onPoll) {
1785
+ onPoll(status, attempt);
1786
+ }
1787
+ // 终止条件:支付成功、失败、过期、退款
1788
+ if (status === 'paid' || status === 'failed' || status === 'expired' || status === 'refunded') {
1789
+ console.log(`[OrderAPI] Final status reached: ${status}`);
1790
+ return status;
1791
+ }
1792
+ // 如果还在 pending 状态,等待后继续轮询
1793
+ if (status === 'pending') {
1794
+ await new Promise((resolve) => setTimeout(resolve, interval));
1795
+ continue;
1796
+ }
1797
+ // 未知状态,停止轮询
1798
+ console.warn(`[OrderAPI] Unknown order status: ${status}`);
1799
+ return status;
1800
+ }
1801
+ catch (error) {
1802
+ console.error(`[OrderAPI] Poll attempt ${attempt} failed:`, error);
1803
+ // 如果还有剩余次数,继续轮询
1804
+ if (attempt < maxAttempts) {
1805
+ await new Promise((resolve) => setTimeout(resolve, interval));
1806
+ continue;
1807
+ }
1808
+ // 超过最大次数,抛出错误
1809
+ throw error;
1810
+ }
1811
+ }
1812
+ // 超时,返回 pending 状态
1813
+ console.warn(`[OrderAPI] Polling timeout after ${maxAttempts} attempts`);
1814
+ return 'pending';
1815
+ }
1816
+
1817
+ /**
1818
+ * PaymentModal - 原生 JavaScript 支付弹框组件
1819
+ * 提供类似 next-meta 的 BaseModal 体验,但使用纯 JavaScript 实现
1820
+ * 支持 Dropin 支付模式的嵌入式展示
1821
+ */
1822
+ class PaymentModal {
1823
+ constructor(options = {}) {
1824
+ this.overlay = null;
1825
+ this.modal = null;
1826
+ this.contentContainer = null;
1827
+ this.isOpen = false;
1828
+ this.isExiting = false;
1829
+ this.escHandler = null;
1830
+ this.savedScrollY = 0;
1831
+ this.savedScrollbarWidth = 0;
1832
+ this.options = {
1833
+ title: options.title ?? '',
1834
+ showCloseButton: options.showCloseButton ?? true,
1835
+ closeOnOverlayClick: options.closeOnOverlayClick ?? true,
1836
+ closeOnEsc: options.closeOnEsc ?? true,
1837
+ onClose: options.onClose ?? (() => { }),
1838
+ className: options.className ?? '',
1839
+ maxWidth: options.maxWidth ?? '480px',
1840
+ };
1841
+ }
1842
+ /**
1843
+ * 打开弹框
1844
+ */
1845
+ open() {
1846
+ if (this.isOpen)
1847
+ return;
1848
+ this.isOpen = true;
1849
+ this.isExiting = false;
1850
+ // 保存滚动位置并阻止 body 滚动
1851
+ this.lockBodyScroll();
1852
+ // 创建弹框元素
1853
+ this.createModal();
1854
+ // 添加到 DOM
1855
+ document.body.appendChild(this.overlay);
1856
+ // 触发动画
1857
+ requestAnimationFrame(() => {
1858
+ this.overlay?.classList.add('payment-modal-fade-in');
1859
+ this.modal?.classList.add('payment-modal-slide-in');
1860
+ });
1861
+ // 绑定 ESC 键
1862
+ if (this.options.closeOnEsc) {
1863
+ this.escHandler = (e) => {
1864
+ if (e.key === 'Escape') {
1865
+ this.close();
1866
+ }
1867
+ };
1868
+ document.addEventListener('keydown', this.escHandler);
1869
+ }
1870
+ }
1871
+ /**
1872
+ * 关闭弹框
1873
+ */
1874
+ close() {
1875
+ if (!this.isOpen || this.isExiting)
1876
+ return;
1877
+ this.isExiting = true;
1878
+ // 触发退出动画
1879
+ this.overlay?.classList.remove('payment-modal-fade-in');
1880
+ this.overlay?.classList.add('payment-modal-fade-out');
1881
+ this.modal?.classList.remove('payment-modal-slide-in');
1882
+ this.modal?.classList.add('payment-modal-slide-out');
1883
+ // 等待动画完成后移除
1884
+ setTimeout(() => {
1885
+ this.destroy();
1886
+ this.isOpen = false;
1887
+ this.isExiting = false;
1888
+ this.options.onClose();
1889
+ }, 200);
1890
+ }
1891
+ /**
1892
+ * 设置弹框内容
1893
+ */
1894
+ setContent(content) {
1895
+ if (!this.contentContainer)
1896
+ return;
1897
+ if (typeof content === 'string') {
1898
+ this.contentContainer.innerHTML = content;
1899
+ }
1900
+ else {
1901
+ this.contentContainer.innerHTML = '';
1902
+ this.contentContainer.appendChild(content);
1903
+ }
1904
+ }
1905
+ /**
1906
+ * 获取内容容器
1907
+ */
1908
+ getContentContainer() {
1909
+ return this.contentContainer;
1910
+ }
1911
+ /**
1912
+ * 销毁弹框
1913
+ */
1914
+ destroy() {
1915
+ // 解绑事件
1916
+ if (this.escHandler) {
1917
+ document.removeEventListener('keydown', this.escHandler);
1918
+ this.escHandler = null;
1919
+ }
1920
+ // 恢复 body 滚动
1921
+ this.unlockBodyScroll();
1922
+ // 移除 DOM
1923
+ if (this.overlay && this.overlay.parentNode) {
1924
+ this.overlay.parentNode.removeChild(this.overlay);
1925
+ }
1926
+ this.overlay = null;
1927
+ this.modal = null;
1928
+ this.contentContainer = null;
1929
+ }
1930
+ /**
1931
+ * 创建弹框元素
1932
+ */
1933
+ createModal() {
1934
+ // 创建遮罩层
1935
+ this.overlay = document.createElement('div');
1936
+ this.overlay.className = 'payment-modal-overlay';
1937
+ this.overlay.style.cssText = `
1938
+ position: fixed;
1939
+ inset: 0;
1940
+ z-index: 9999;
1941
+ display: flex;
1942
+ align-items: center;
1943
+ justify-content: center;
1944
+ padding: 20px;
1945
+ background: rgba(0, 0, 0, 0.7);
1946
+ backdrop-filter: blur(8px);
1947
+ opacity: 0;
1948
+ `;
1949
+ // 点击遮罩层关闭
1950
+ if (this.options.closeOnOverlayClick) {
1951
+ this.overlay.addEventListener('click', (e) => {
1952
+ if (e.target === this.overlay) {
1953
+ this.close();
1954
+ }
1955
+ });
1956
+ }
1957
+ // 创建弹框主体
1958
+ this.modal = document.createElement('div');
1959
+ this.modal.className = `payment-modal ${this.options.className}`;
1960
+ this.modal.style.cssText = `
1961
+ position: relative;
1962
+ max-height: 90vh;
1963
+ width: 100%;
1964
+ max-width: ${this.options.maxWidth};
1965
+ overflow-y: auto;
1966
+ border-radius: 16px;
1967
+ border: 1px solid rgba(0, 0, 0, 0.1);
1968
+ background: #ffffff;
1969
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
1970
+ transform: translateY(20px);
1971
+ opacity: 0;
1972
+ `;
1973
+ // 自定义滚动条样式
1974
+ this.modal.style.cssText += `
1975
+ scrollbar-width: thin;
1976
+ scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
1977
+ `;
1978
+ // 关闭按钮
1979
+ if (this.options.showCloseButton) {
1980
+ const closeButton = document.createElement('button');
1981
+ closeButton.className = 'payment-modal-close';
1982
+ closeButton.innerHTML = `
1983
+ <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1984
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
1985
+ </svg>
1986
+ `;
1987
+ closeButton.style.cssText = `
1988
+ position: absolute;
1989
+ top: 16px;
1990
+ right: 16px;
1991
+ z-index: 10;
1992
+ display: flex;
1993
+ align-items: center;
1994
+ justify-content: center;
1995
+ width: 32px;
1996
+ height: 32px;
1997
+ border: none;
1998
+ border-radius: 8px;
1999
+ background: rgba(0, 0, 0, 0.05);
2000
+ color: rgba(0, 0, 0, 0.6);
2001
+ cursor: pointer;
2002
+ transition: all 0.2s;
2003
+ `;
2004
+ closeButton.addEventListener('mouseenter', () => {
2005
+ closeButton.style.background = 'rgba(0, 0, 0, 0.1)';
2006
+ closeButton.style.color = 'rgba(0, 0, 0, 0.9)';
2007
+ });
2008
+ closeButton.addEventListener('mouseleave', () => {
2009
+ closeButton.style.background = 'rgba(0, 0, 0, 0.05)';
2010
+ closeButton.style.color = 'rgba(0, 0, 0, 0.6)';
2011
+ });
2012
+ closeButton.addEventListener('click', () => this.close());
2013
+ this.modal.appendChild(closeButton);
2014
+ }
2015
+ // 标题(如果有)
2016
+ if (this.options.title) {
2017
+ const title = document.createElement('h2');
2018
+ title.className = 'payment-modal-title';
2019
+ title.textContent = this.options.title;
2020
+ title.style.cssText = `
2021
+ margin: 0 0 24px 0;
2022
+ padding: 40px 32px 0 32px;
2023
+ font-size: 24px;
2024
+ font-weight: 600;
2025
+ color: #1a1a1a;
2026
+ text-align: center;
2027
+ `;
2028
+ this.modal.appendChild(title);
2029
+ }
2030
+ // 内容容器
2031
+ this.contentContainer = document.createElement('div');
2032
+ this.contentContainer.className = 'payment-modal-content';
2033
+ this.contentContainer.style.cssText = `
2034
+ padding: ${this.options.title ? '0 32px 32px 32px' : '40px 32px 32px 32px'};
2035
+ `;
2036
+ this.modal.appendChild(this.contentContainer);
2037
+ this.overlay.appendChild(this.modal);
2038
+ // 添加样式到 head(如果还没有)
2039
+ this.injectStyles();
2040
+ }
2041
+ /**
2042
+ * 注入动画样式
2043
+ */
2044
+ injectStyles() {
2045
+ if (document.getElementById('payment-modal-styles'))
2046
+ return;
2047
+ const style = document.createElement('style');
2048
+ style.id = 'payment-modal-styles';
2049
+ style.textContent = `
2050
+ @keyframes payment-modal-fade-in {
2051
+ from { opacity: 0; }
2052
+ to { opacity: 1; }
2053
+ }
2054
+ @keyframes payment-modal-fade-out {
2055
+ from { opacity: 1; }
2056
+ to { opacity: 0; }
2057
+ }
2058
+ @keyframes payment-modal-slide-in {
2059
+ from {
2060
+ transform: translateY(20px);
2061
+ opacity: 0;
2062
+ }
2063
+ to {
2064
+ transform: translateY(0);
2065
+ opacity: 1;
2066
+ }
2067
+ }
2068
+ @keyframes payment-modal-slide-out {
2069
+ from {
2070
+ transform: translateY(0);
2071
+ opacity: 1;
2072
+ }
2073
+ to {
2074
+ transform: translateY(20px);
2075
+ opacity: 0;
2076
+ }
2077
+ }
2078
+ .payment-modal-fade-in {
2079
+ animation: payment-modal-fade-in 0.2s ease-out forwards;
2080
+ }
2081
+ .payment-modal-fade-out {
2082
+ animation: payment-modal-fade-out 0.2s ease-out forwards;
2083
+ }
2084
+ .payment-modal-slide-in {
2085
+ animation: payment-modal-slide-in 0.3s ease-out forwards;
2086
+ }
2087
+ .payment-modal-slide-out {
2088
+ animation: payment-modal-slide-out 0.2s ease-out forwards;
2089
+ }
2090
+
2091
+ /* Webkit 滚动条样式 */
2092
+ .payment-modal::-webkit-scrollbar {
2093
+ width: 6px;
2094
+ }
2095
+ .payment-modal::-webkit-scrollbar-track {
2096
+ background: transparent;
2097
+ }
2098
+ .payment-modal::-webkit-scrollbar-thumb {
2099
+ background: rgba(0, 0, 0, 0.2);
2100
+ border-radius: 3px;
2101
+ }
2102
+ .payment-modal::-webkit-scrollbar-thumb:hover {
2103
+ background: rgba(0, 0, 0, 0.3);
2104
+ }
2105
+ `;
2106
+ document.head.appendChild(style);
2107
+ }
2108
+ /**
2109
+ * 锁定 body 滚动
2110
+ */
2111
+ lockBodyScroll() {
2112
+ this.savedScrollY = window.scrollY;
2113
+ this.savedScrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
2114
+ document.body.style.position = 'fixed';
2115
+ document.body.style.top = `-${this.savedScrollY}px`;
2116
+ document.body.style.left = '0';
2117
+ document.body.style.right = '0';
2118
+ document.body.style.overflow = 'hidden';
2119
+ // 补偿滚动条宽度,防止页面跳动
2120
+ if (this.savedScrollbarWidth > 0) {
2121
+ document.body.style.paddingRight = `${this.savedScrollbarWidth}px`;
2122
+ }
2123
+ }
2124
+ /**
2125
+ * 解锁 body 滚动
2126
+ */
2127
+ unlockBodyScroll() {
2128
+ document.body.style.position = '';
2129
+ document.body.style.top = '';
2130
+ document.body.style.left = '';
2131
+ document.body.style.right = '';
2132
+ document.body.style.overflow = '';
2133
+ document.body.style.paddingRight = '';
2134
+ // 恢复滚动位置
2135
+ window.scrollTo(0, this.savedScrollY);
2136
+ }
2137
+ /**
2138
+ * 检查弹框是否打开
2139
+ */
2140
+ isModalOpen() {
2141
+ return this.isOpen;
2142
+ }
2143
+ }
2144
+
2145
+ /**
2146
+ * DropinPaymentModal - Dropin 支付弹框
2147
+ * 封装 DropinPaymentComponent 并在弹框中展示
2148
+ */
2149
+ class DropinPaymentModal {
2150
+ constructor(paymentInstance, orderId, accountToken, paymentMethod, options) {
2151
+ this.paymentInstance = paymentInstance;
2152
+ this.orderId = orderId;
2153
+ this.accountToken = accountToken;
2154
+ this.paymentMethod = paymentMethod;
2155
+ this.options = options;
2156
+ this.dropinPayment = null;
2157
+ this.containerElement = null;
2158
+ // 创建弹框
2159
+ this.modal = new PaymentModal({
2160
+ title: options.modalTitle ?? `Pay with ${paymentMethod.payment_method_name}`,
2161
+ showCloseButton: true,
2162
+ closeOnOverlayClick: false, // Dropin 支付中不允许点击遮罩关闭
2163
+ closeOnEsc: true,
2164
+ maxWidth: '600px',
2165
+ onClose: () => {
2166
+ this.cleanup();
2167
+ },
2168
+ ...options.modalOptions,
2169
+ });
2170
+ console.log('[DropinPaymentModal] Created:', {
2171
+ orderId: this.orderId,
2172
+ paymentMethod: this.paymentMethod.payment_method_name,
2173
+ });
2174
+ }
2175
+ /**
2176
+ * 打开弹框并渲染支付组件
2177
+ */
2178
+ async open() {
2179
+ console.log('[DropinPaymentModal] Opening modal...');
2180
+ // 打开弹框
2181
+ this.modal.open();
2182
+ // 创建容器元素
2183
+ this.containerElement = document.createElement('div');
2184
+ this.containerElement.id = 'dropin-payment-container';
2185
+ this.containerElement.style.cssText = `
2186
+ min-height: 500px;
2187
+ width: 100%;
2188
+ `;
2189
+ // 设置弹框内容 - 必须先添加到 DOM,再创建 Dropin 组件
2190
+ const contentContainer = this.modal.getContentContainer();
2191
+ if (!contentContainer) {
2192
+ throw new Error('Modal content container not found');
2193
+ }
2194
+ contentContainer.appendChild(this.containerElement);
2195
+ // 验证订单信息(关键:确保后端订单已创建)
2196
+ try {
2197
+ console.log('[DropinPaymentModal] Validating order info...');
2198
+ const response = await this.paymentInstance.getOrderInfo();
2199
+ if (response.err || !response.data?.order_info) {
2200
+ throw new Error(response.message || 'Order information not found');
2201
+ }
2202
+ const orderInfo = response.data.order_info;
2203
+ if (!orderInfo.sys_order_id) {
2204
+ throw new Error('Order ID (sys_order_id) is missing');
2205
+ }
2206
+ console.log('[DropinPaymentModal] Order validated:', {
2207
+ orderInfo,
2208
+ orderId: orderInfo.sys_order_id,
2209
+ status: orderInfo.order_status,
2210
+ });
2211
+ }
2212
+ catch (error) {
2213
+ console.error('[DropinPaymentModal] Order validation failed:', error);
2214
+ this.close();
2215
+ throw new Error(`Order validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
2216
+ }
2217
+ // 创建并渲染 Dropin 支付组件
2218
+ try {
2219
+ this.dropinPayment = new DropinPaymentComponent({
2220
+ paymentInstance: this.paymentInstance,
2221
+ orderId: this.orderId,
2222
+ accountToken: this.accountToken,
2223
+ paymentMethod: this.paymentMethod,
2224
+ containerId: '#dropin-payment-container',
2225
+ onSubmit: (payload) => {
2226
+ console.log('[DropinPaymentModal] onSubmit:', payload);
2227
+ this.options.onSubmit?.(payload);
2228
+ },
2229
+ onError: (payload, error) => {
2230
+ console.error('[DropinPaymentModal] onError:', error);
2231
+ this.options.onError?.(payload, error);
2232
+ },
2233
+ onCreateOrder: (payload) => {
2234
+ console.log('[DropinPaymentModal] onCreateOrder:', payload);
2235
+ this.options.onCreateOrder?.(payload);
2236
+ },
2237
+ onCompleted: (payload) => {
2238
+ console.log('[DropinPaymentModal] onCompleted:', payload);
2239
+ this.options.onCompleted?.(payload);
2240
+ // 支付成功后自动关闭弹框(可选)
2241
+ setTimeout(() => this.close(), 2000);
2242
+ },
2243
+ onFailed: (payload) => {
2244
+ console.error('[DropinPaymentModal] onFailed:', payload);
2245
+ this.options.onFailed?.(payload);
2246
+ },
2247
+ onLoading: (loading) => {
2248
+ this.options.onLoading?.(loading);
2249
+ // 可以在这里显示加载状态
2250
+ if (loading) {
2251
+ this.showLoading();
2252
+ }
2253
+ else {
2254
+ this.hideLoading();
2255
+ }
2256
+ },
2257
+ });
2258
+ await this.dropinPayment.render();
2259
+ console.log('[DropinPaymentModal] Dropin component rendered');
2260
+ }
2261
+ catch (error) {
2262
+ console.error('[DropinPaymentModal] Failed to render dropin:', error);
2263
+ this.close();
2264
+ throw error;
2265
+ }
2266
+ }
2267
+ /**
2268
+ * 关闭弹框
2269
+ */
2270
+ close() {
2271
+ console.log('[DropinPaymentModal] Closing modal...');
2272
+ this.modal.close();
2273
+ }
2274
+ /**
2275
+ * 清理资源
2276
+ */
2277
+ cleanup() {
2278
+ console.log('[DropinPaymentModal] Cleaning up...');
2279
+ if (this.dropinPayment) {
2280
+ this.dropinPayment.destroy();
2281
+ this.dropinPayment = null;
2282
+ }
2283
+ this.containerElement = null;
2284
+ }
2285
+ /**
2286
+ * 显示加载状态
2287
+ */
2288
+ showLoading() {
2289
+ if (!this.containerElement)
2290
+ return;
2291
+ const loadingEl = document.getElementById('dropin-loading');
2292
+ if (loadingEl)
2293
+ return; // 已经存在
2294
+ const loading = document.createElement('div');
2295
+ loading.id = 'dropin-loading';
2296
+ loading.style.cssText = `
2297
+ position: absolute;
2298
+ top: 0;
2299
+ left: 0;
2300
+ right: 0;
2301
+ bottom: 0;
2302
+ display: flex;
2303
+ align-items: center;
2304
+ justify-content: center;
2305
+ background: rgba(0, 0, 0, 0.3);
2306
+ backdrop-filter: blur(2px);
2307
+ z-index: 10;
2308
+ `;
2309
+ loading.innerHTML = `
2310
+ <div style="
2311
+ width: 40px;
2312
+ height: 40px;
2313
+ border: 3px solid rgba(255, 255, 255, 0.2);
2314
+ border-top-color: #ffffff;
2315
+ border-radius: 50%;
2316
+ animation: spin 0.8s linear infinite;
2317
+ "></div>
2318
+ `;
2319
+ // 添加旋转动画
2320
+ if (!document.getElementById('dropin-loading-styles')) {
2321
+ const style = document.createElement('style');
2322
+ style.id = 'dropin-loading-styles';
2323
+ style.textContent = `
2324
+ @keyframes spin {
2325
+ to { transform: rotate(360deg); }
2326
+ }
2327
+ `;
2328
+ document.head.appendChild(style);
2329
+ }
2330
+ this.containerElement.style.position = 'relative';
2331
+ this.containerElement.appendChild(loading);
2332
+ }
2333
+ /**
2334
+ * 隐藏加载状态
2335
+ */
2336
+ hideLoading() {
2337
+ const loadingEl = document.getElementById('dropin-loading');
2338
+ if (loadingEl && loadingEl.parentNode) {
2339
+ loadingEl.parentNode.removeChild(loadingEl);
2340
+ }
2341
+ }
2342
+ /**
2343
+ * 检查弹框是否打开
2344
+ */
2345
+ isOpen() {
2346
+ return this.modal.isModalOpen();
2347
+ }
2348
+ }
2349
+
2350
+ /**
2351
+ * SeaartPaymentSDK
2352
+ * 基于 SeaartPaymentComponent 的支付 SDK 封装
2353
+ *
2354
+ * 核心职责:
2355
+ * 1. 动态加载 SeaartPaymentComponent 脚本
2356
+ * 2. 全局初始化(仅一次)
2357
+ * 3. 获取支付方式列表
2358
+ * 4. 创建订单支付实例
2359
+ */
2360
+ class SeaartPaymentSDK {
2361
+ /**
2362
+ * 私有构造函数(单例模式)
2363
+ */
2364
+ constructor() {
2365
+ this.initialized = false;
2366
+ this.config = null;
2367
+ }
2368
+ /**
2369
+ * 获取 SDK 单例
2370
+ */
2371
+ static getInstance() {
2372
+ if (!this.instance) {
2373
+ this.instance = new SeaartPaymentSDK();
2374
+ }
2375
+ return this.instance;
2376
+ }
2377
+ /**
2378
+ * 全局初始化(应用启动时调用一次)
2379
+ */
2380
+ async init(config) {
2381
+ if (this.initialized) {
2382
+ console.warn('[SeaartPaymentSDK] Already initialized');
2383
+ return;
2384
+ }
2385
+ console.log('[SeaartPaymentSDK] Initializing...', {
2386
+ scriptUrl: config.scriptUrl,
2387
+ clientId: config.clientId,
2388
+ language: config.language,
2389
+ cssUrl: config.cssUrl,
2390
+ });
2391
+ try {
2392
+ // 1. 并行加载 CSS 和 JavaScript
2393
+ const loadTasks = [
2394
+ ScriptLoader.loadSeaartPaymentComponent({
2395
+ scriptUrl: config.scriptUrl,
2396
+ timeout: config.scriptTimeout || 10000,
2397
+ }),
2398
+ ];
2399
+ // 2. 如果提供了 CSS URL,则加载样式表
2400
+ if (config.cssUrl) {
2401
+ loadTasks.push(StylesheetLoader.loadPaymentStylesheet(config.cssUrl));
2402
+ }
2403
+ // 3. 等待所有资源加载完成
2404
+ await Promise.all(loadTasks);
2405
+ // 4. 初始化 SDK
2406
+ await window.SeaartPaymentComponent.init({
2407
+ client_id: config.clientId,
2408
+ language: config.language || 'zhCN',
2409
+ });
2410
+ // 5. 保存配置并标记已初始化
2411
+ this.config = config;
2412
+ this.initialized = true;
2413
+ console.log('[SeaartPaymentSDK] Initialized successfully');
2414
+ }
2415
+ catch (error) {
2416
+ this.initialized = false;
2417
+ this.config = null;
2418
+ console.error('[SeaartPaymentSDK] Initialization failed:', error);
2419
+ throw ErrorHandler.normalize(error);
2420
+ }
2421
+ }
2422
+ /**
2423
+ * 获取支付方式列表
2424
+ */
2425
+ async getPaymentMethods(params) {
2426
+ this.ensureInitialized();
2427
+ console.log('[SeaartPaymentSDK] Getting payment methods...', params);
2428
+ try {
2429
+ const result = await window.SeaartPaymentComponent.getPaymentMethodList({
2430
+ country_code: params.country_code,
2431
+ business_type: params.business_type,
2432
+ });
2433
+ if (result.err) {
2434
+ throw new Error(result.message || 'Failed to get payment methods');
2435
+ }
2436
+ const methods = result.data?.payment_method_list || [];
2437
+ console.log('[SeaartPaymentSDK] Payment methods retrieved:', {
2438
+ count: methods.length,
2439
+ methods: methods.map((m) => ({
2440
+ name: m.payment_method_name,
2441
+ type: m.payment_type === 1 ? 'Link' : 'Dropin',
2442
+ })),
2443
+ });
2444
+ return methods;
2445
+ }
2446
+ catch (error) {
2447
+ console.error('[SeaartPaymentSDK] Get payment methods failed:', error);
2448
+ throw ErrorHandler.normalize(error);
2449
+ }
2450
+ }
2451
+ /**
2452
+ * 创建订单支付实例
2453
+ */
2454
+ createOrderPayment(params) {
2455
+ this.ensureInitialized();
2456
+ console.log('[SeaartPaymentSDK] Creating order payment instance:', {
2457
+ orderId: params.orderId,
2458
+ hasAccountToken: !!params.accountToken,
2459
+ });
2460
+ // 创建支付实例(基于 SDK 的 createPayment)
2461
+ const paymentInstance = window.SeaartPaymentComponent.createPayment({
2462
+ sys_order_id: params.orderId,
2463
+ account_token: params.accountToken,
2464
+ });
2465
+ return new OrderPayment({
2466
+ paymentInstance,
2467
+ orderId: params.orderId,
2468
+ accountToken: params.accountToken,
2469
+ });
2470
+ }
2471
+ /**
2472
+ * 创建 Dropin 支付弹框(快捷方法)
2473
+ * 在弹框中直接展示 Dropin 支付组件
2474
+ */
2475
+ createDropinPaymentModal(params) {
2476
+ this.ensureInitialized();
2477
+ console.log('[SeaartPaymentSDK] Creating dropin payment modal:', {
2478
+ orderId: params.orderId,
2479
+ paymentMethod: params.paymentMethod.payment_method_name,
2480
+ });
2481
+ // 创建支付实例
2482
+ const paymentInstance = window.SeaartPaymentComponent.createPayment({
2483
+ sys_order_id: params.orderId,
2484
+ account_token: params.accountToken,
2485
+ });
2486
+ // 创建弹框
2487
+ return new DropinPaymentModal(paymentInstance, params.orderId, params.accountToken, params.paymentMethod, params);
2488
+ }
2489
+ /**
2490
+ * 处理支付回调(在回调页面调用)
2491
+ * @param params 回调参数
2492
+ * @param apiHost Payment API 地址
2493
+ * @param authToken 认证 Token
2494
+ */
2495
+ async handleCallback(params, apiHost, authToken) {
2496
+ console.log('[SeaartPaymentSDK] Handling payment callback...', params);
2497
+ try {
2498
+ // 1. 从 localStorage 获取 transactionId
2499
+ const transaction = PaymentStorage.getTransaction();
2500
+ if (!transaction) {
2501
+ throw new Error('No transaction found in storage');
2502
+ }
2503
+ // 2. 轮询订单状态
2504
+ const finalStatus = await pollOrderStatus(transaction.transactionId, apiHost, authToken, {
2505
+ interval: 1000,
2506
+ maxAttempts: 30,
2507
+ });
2508
+ // 3. 清除存储
2509
+ PaymentStorage.clearTransaction();
2510
+ // 4. 返回结果
2511
+ if (finalStatus === 'paid') {
2512
+ console.log('[SeaartPaymentSDK] Payment callback succeeded');
2513
+ return {
2514
+ success: true,
2515
+ status: 'paid',
2516
+ data: {
2517
+ orderId: transaction.orderId,
2518
+ transactionId: transaction.transactionId,
2519
+ },
2520
+ };
2521
+ }
2522
+ else {
2523
+ console.warn('[SeaartPaymentSDK] Payment callback failed:', finalStatus);
2524
+ return {
2525
+ success: false,
2526
+ status: finalStatus,
2527
+ };
2528
+ }
2529
+ }
2530
+ catch (error) {
2531
+ console.error('[SeaartPaymentSDK] Handle callback failed:', error);
2532
+ PaymentStorage.clearTransaction();
2533
+ throw ErrorHandler.normalize(error);
2534
+ }
2535
+ }
2536
+ /**
2537
+ * 确保 SDK 已初始化
2538
+ */
2539
+ ensureInitialized() {
2540
+ if (!this.initialized) {
2541
+ throw new Error('[SeaartPaymentSDK] Not initialized. Call init() first.');
2542
+ }
2543
+ }
2544
+ /**
2545
+ * 获取当前配置(用于组件访问)
2546
+ */
2547
+ getConfig() {
2548
+ return this.config;
2549
+ }
2550
+ /**
2551
+ * 销毁 SDK(测试用)
2552
+ */
2553
+ destroy() {
2554
+ this.config = null;
2555
+ this.initialized = false;
2556
+ console.log('[SeaartPaymentSDK] Destroyed');
2557
+ }
2558
+ }
2559
+ SeaartPaymentSDK.instance = null;
2560
+
2561
+ /**
2562
+ * 积分套餐数据
2563
+ */
2564
+ const CREDIT_PACKAGES = [
2565
+ {
2566
+ id: 'pkg_starter',
2567
+ name: 'Starter Pack',
2568
+ price: '9.99',
2569
+ currency: 'USD',
2570
+ base_credits: '1000',
2571
+ bonus_credits: '0',
2572
+ credits: '1000',
2573
+ day_limit: 1,
2574
+ is_popular: true, // Daily Limit 标签
2575
+ },
2576
+ {
2577
+ id: 'pkg_value',
2578
+ name: 'Value Pack',
2579
+ price: '19.99',
2580
+ currency: 'USD',
2581
+ base_credits: '1000',
2582
+ bonus_credits: '200',
2583
+ credits: '1200',
2584
+ day_limit: 0,
2585
+ },
2586
+ {
2587
+ id: 'pkg_pro',
2588
+ name: 'Pro Pack',
2589
+ price: '49.99',
2590
+ currency: 'USD',
2591
+ base_credits: '2500',
2592
+ bonus_credits: '800',
2593
+ credits: '3300',
2594
+ day_limit: 0,
2595
+ },
2596
+ {
2597
+ id: 'pkg_ultra',
2598
+ name: 'Ultra Pack',
2599
+ price: '99.99',
2600
+ currency: 'USD',
2601
+ base_credits: '5000',
2602
+ bonus_credits: '2000',
2603
+ credits: '7000',
2604
+ day_limit: 0,
2605
+ },
2606
+ ];
2607
+ const CREATIVE_POWER_TYPES = [
2608
+ {
2609
+ icon: '🎬',
2610
+ name: 'Video',
2611
+ name_cn: '电影片段',
2612
+ credits_range: '100-300',
2613
+ description: '10-30s AI Movie Clips',
2614
+ description_cn: '10-30 秒的 AI 电影片段',
2615
+ },
2616
+ {
2617
+ icon: '🎮',
2618
+ name: 'Game',
2619
+ name_cn: '游戏场景',
2620
+ credits_range: '50-200',
2621
+ description: 'Interactive Game Scenes',
2622
+ description_cn: '交互式游戏场景机制',
2623
+ },
2624
+ {
2625
+ icon: '🎵',
2626
+ name: 'Music',
2627
+ name_cn: '音乐',
2628
+ credits_range: '30-100',
2629
+ description: '1-3 Min AI Music',
2630
+ description_cn: '1-3 分钟 AI 生成的音乐',
2631
+ },
2632
+ {
2633
+ icon: '🌍',
2634
+ name: '3D World',
2635
+ name_cn: '3D 世界',
2636
+ credits_range: '150-500',
2637
+ description: 'Immersive 3D Environment',
2638
+ description_cn: '沉浸式 3D 环境和体验',
2639
+ },
2640
+ {
2641
+ icon: '💬',
2642
+ name: 'AI Chat',
2643
+ name_cn: '智能对话',
2644
+ credits_range: '10',
2645
+ description: 'AI Smart Conversation',
2646
+ description_cn: 'AI 智能对话协助',
2647
+ },
2648
+ ];
2649
+
2650
+ /**
2651
+ * Subscription API
2652
+ * 订阅管理 - 查询、变更、重启订阅
2653
+ */
2654
+ /**
2655
+ * 获取当前订阅状态(用于升降级判断)
2656
+ */
2657
+ async function getCurrentSubscription(apiHost, authToken, signal) {
2658
+ if (!authToken) {
2659
+ return null;
2660
+ }
2661
+ try {
2662
+ const response = await fetch(`${apiHost}/api/v1/payment/sdk/subscription/current`, {
2663
+ method: 'POST',
2664
+ headers: {
2665
+ Authorization: `Bearer ${authToken}`,
2666
+ 'Content-Type': 'application/json',
2667
+ },
2668
+ body: JSON.stringify({}),
2669
+ signal,
2670
+ });
2671
+ // 检查 HTTP 状态码
2672
+ if (!response.ok) {
2673
+ console.error(`Failed to get current subscription: HTTP ${response.status}`);
2674
+ return null;
2675
+ }
2676
+ const result = await response.json();
2677
+ if (result.code === 0) {
2678
+ return result.data;
2679
+ }
2680
+ // code !== 0 视为无订阅
2681
+ return null;
2682
+ }
2683
+ catch (error) {
2684
+ console.error('Failed to get current subscription:', error);
2685
+ return null;
2686
+ }
2687
+ }
2688
+ /**
2689
+ * 创建订单
2690
+ * @param apiHost 接口域名
2691
+ * @param authToken 认证令牌
2692
+ * @param params 创建订单参数
2693
+ * @param product_id 产品ID
2694
+ * @param purchase_type 购买类型(1=一次性, 2=订阅)
2695
+ * @returns 创建订单响应
2696
+ */
2697
+ async function createOrder(apiHost, authToken, params) {
2698
+ if (!authToken) {
2699
+ return null;
2700
+ }
2701
+ try {
2702
+ // 确保 purchase_type 是 number 类型
2703
+ const requestBody = {
2704
+ product_id: params.product_id,
2705
+ purchase_type: typeof params.purchase_type === 'string'
2706
+ ? parseInt(params.purchase_type, 10)
2707
+ : params.purchase_type,
2708
+ };
2709
+ const response = await fetch(`${apiHost}/api/v1/payment/sdk/checkout`, {
2710
+ method: 'POST',
2711
+ headers: {
2712
+ Authorization: `Bearer ${authToken}`,
2713
+ 'Content-Type': 'application/json',
2714
+ },
2715
+ body: JSON.stringify(requestBody),
2716
+ });
2717
+ // 检查 HTTP 状态码
2718
+ if (!response.ok) {
2719
+ console.error(`Failed to get active subscription: HTTP ${response.status}`);
2720
+ return null;
2721
+ }
2722
+ const result = await response.json();
2723
+ if (result.code === 0) {
2724
+ return result.data;
2725
+ }
2726
+ }
2727
+ catch (error) {
2728
+ console.error('Failed to get active subscription:', error);
2729
+ throw error;
2730
+ }
2731
+ }
2732
+ /**
2733
+ * 获取活跃订阅详情
2734
+ */
2735
+ async function getActiveSubscription(apiHost, authToken) {
2736
+ if (!authToken) {
2737
+ return null;
2738
+ }
2739
+ try {
2740
+ const response = await fetch(`${apiHost}/api/v1/payment/sdk/subscription/active`, {
2741
+ method: 'GET',
2742
+ headers: {
2743
+ Authorization: `Bearer ${authToken}`,
2744
+ },
2745
+ });
2746
+ // 检查 HTTP 状态码
2747
+ if (!response.ok) {
2748
+ console.error(`Failed to get active subscription: HTTP ${response.status}`);
2749
+ return null;
2750
+ }
2751
+ const result = await response.json();
2752
+ if (result.code === 0) {
2753
+ return result.data;
2754
+ }
2755
+ return null;
2756
+ }
2757
+ catch (error) {
2758
+ console.error('Failed to get active subscription:', error);
2759
+ return null;
2760
+ }
2761
+ }
2762
+ /**
2763
+ * 升降级订阅
2764
+ */
2765
+ async function changeSubscription(apiHost, authToken, params) {
2766
+ if (!authToken) {
2767
+ throw new Error('Authentication token not found');
2768
+ }
2769
+ const response = await fetch(`${apiHost}/api/v1/payment/sdk/subscription/change`, {
2770
+ method: 'POST',
2771
+ headers: {
2772
+ Authorization: `Bearer ${authToken}`,
2773
+ 'Content-Type': 'application/json',
2774
+ },
2775
+ body: JSON.stringify({
2776
+ product_id: params.productId,
2777
+ billing_period: params.billingPeriod,
2778
+ redirect_url: params.redirectUrl,
2779
+ }),
2780
+ });
2781
+ const result = await response.json();
2782
+ if (result.code !== 0) {
2783
+ throw {
2784
+ code: result.code,
2785
+ bizCode: result.code,
2786
+ message: result.msg || 'Change subscription failed',
2787
+ };
2788
+ }
2789
+ return result.data;
2790
+ }
2791
+ /**
2792
+ * 重新启动订阅
2793
+ * 用于已取消但仍在有效期内的订阅,重新恢复自动续费
2794
+ */
2795
+ async function restartSubscription(apiHost, authToken, params) {
2796
+ if (!authToken) {
2797
+ throw new Error('Authentication token not found');
2798
+ }
2799
+ const response = await fetch(`${apiHost}/api/v1/payment/sdk/subscription/restart`, {
2800
+ method: 'POST',
2801
+ headers: {
2802
+ Authorization: `Bearer ${authToken}`,
2803
+ 'Content-Type': 'application/json',
2804
+ },
2805
+ body: JSON.stringify({
2806
+ subscription_id: params.subscriptionId,
2807
+ }),
2808
+ });
2809
+ // 检查 HTTP 状态码
2810
+ if (!response.ok) {
2811
+ console.error(`Failed to restart subscription: HTTP ${response.status}`);
2812
+ throw new Error(`HTTP ${response.status}: Failed to restart subscription`);
2813
+ }
2814
+ const result = await response.json();
2815
+ if (result.code !== 0) {
2816
+ throw {
2817
+ code: result.code,
2818
+ bizCode: result.code,
2819
+ message: result.msg || 'Restart subscription failed',
2820
+ };
2821
+ }
2822
+ return result.data;
2823
+ }
2824
+ /**
2825
+ * 获取积分详情
2826
+ * @param apiHost 接口域名(如 https://wallet.sg.seaverse.dev)
2827
+ * @param authToken 认证令牌
2828
+ * @returns 积分详情
2829
+ */
2830
+ async function getCreditDetail(apiHost, authToken) {
2831
+ if (!authToken) {
2832
+ console.warn('[getCreditDetail] Auth token not provided');
2833
+ return null;
2834
+ }
2835
+ try {
2836
+ const response = await fetch(`${apiHost}/sdk/v1/credits/detail`, {
2837
+ method: 'GET',
2838
+ headers: {
2839
+ 'Content-Type': 'application/json',
2840
+ Authorization: `Bearer ${authToken}`,
2841
+ 'X-App-Id': 'seaverse',
2842
+ },
2843
+ });
2844
+ if (!response.ok) {
2845
+ console.error(`[getCreditDetail] HTTP error! status: ${response.status}`);
2846
+ return null;
2847
+ }
2848
+ const result = await response.json();
2849
+ if (result.code === 0 && result.data) {
2850
+ return result.data;
2851
+ }
2852
+ else {
2853
+ console.error('[getCreditDetail] API error:', result.message || 'Unknown error');
2854
+ return null;
2855
+ }
2856
+ }
2857
+ catch (error) {
2858
+ console.error('[getCreditDetail] Failed to fetch credit detail:', error);
2859
+ return null;
2860
+ }
2861
+ }
2862
+
2863
+ /**
2864
+ * 共享配置文件
2865
+ * Shared configuration for CreditPackageModal and GenericPackageModal
2866
+ */
2867
+ /**
2868
+ * 环境配置映射表
2869
+ * SDK 根据 environment 参数自动选择对应的配置
2870
+ */
2871
+ const ENVIRONMENT_CONFIGS = {
2872
+ development: {
2873
+ scriptUrl: 'https://seaart-publish.sc-api-release.saconsole.com/payment-component/client.js',
2874
+ clientId: 'XF49NOfyZ54O16GujB0ptio2',
2875
+ orderApiUrl: 'https://payment.sg.seaverse.dev',
2876
+ walletApiUrl: 'https://wallet.sg.seaverse.dev',
2877
+ cssUrl: 'https://seaart-publish.sc-api-release.saconsole.com/payment-component/public/style.css',
2878
+ },
2879
+ production: {
2880
+ scriptUrl: 'https://payment-static.seaverse.com/sdk/seaart-payment-component.js',
2881
+ clientId: 'prod_client_id',
2882
+ orderApiUrl: 'https://payment.seaverse.com',
2883
+ walletApiUrl: 'https://wallet.seaverse.com',
2884
+ cssUrl: 'https://payment-static.seaverse.com/sdk/seaart-payment-component.css',
2885
+ },
2886
+ };
2887
+ /**
2888
+ * 响应式断点配置
2889
+ * Responsive breakpoints for grid layout
2890
+ */
2891
+ const RESPONSIVE_BREAKPOINTS = {
2892
+ mobile: 768,
2893
+ tablet: 1200,
2894
+ laptop: 1400,
2895
+ desktop: Infinity,
2896
+ };
2897
+ /**
2898
+ * 网格列数配置
2899
+ * Grid columns for different screen sizes
2900
+ */
2901
+ const GRID_COLUMNS = {
2902
+ mobile: 1,
2903
+ tablet: 2,
2904
+ laptop: 2,
2905
+ desktop: 4,
2906
+ };
2907
+ /**
2908
+ * SDK 初始化配置
2909
+ */
2910
+ const SDK_CONFIG = {
2911
+ /** 默认脚本加载超时 (毫秒) */
2912
+ DEFAULT_SCRIPT_TIMEOUT: 10000,
2913
+ /** 默认 SDK 初始化超时 (毫秒) */
2914
+ DEFAULT_INIT_TIMEOUT: 30000,
2915
+ /** 默认最大重试次数 */
2916
+ DEFAULT_MAX_RETRIES: 1,
2917
+ /** 默认业务类型 (1=一次性购买, 2=订阅) */
2918
+ DEFAULT_BUSINESS_TYPE: 1,
2919
+ };
2920
+
2921
+ /**
2922
+ * UI 反馈工具类
2923
+ * 提供统一的用户界面反馈(错误提示、加载状态等)
2924
+ */
2925
+ /**
2926
+ * 显示错误消息
2927
+ * 替代浏览器原生 alert(),提供更好的用户体验
2928
+ *
2929
+ * @param message 错误消息
2930
+ * @param duration 显示时长(毫秒),默认 3000ms
2931
+ */
2932
+ function showErrorMessage(message, duration = 3000) {
2933
+ // 检查是否已存在错误提示,避免重复创建
2934
+ const existingError = document.getElementById('payment-sdk-error-toast');
2935
+ if (existingError) {
2936
+ existingError.remove();
2937
+ }
2938
+ const errorDiv = document.createElement('div');
2939
+ errorDiv.id = 'payment-sdk-error-toast';
2940
+ errorDiv.style.cssText = `
2941
+ position: fixed;
2942
+ top: 20px;
2943
+ right: 20px;
2944
+ background: linear-gradient(135deg, rgba(255, 59, 48, 0.95) 0%, rgba(220, 38, 38, 0.95) 100%);
2945
+ color: white;
2946
+ padding: 16px 24px;
2947
+ border-radius: 12px;
2948
+ box-shadow: 0 8px 32px rgba(255, 59, 48, 0.4), 0 2px 8px rgba(0, 0, 0, 0.2);
2949
+ z-index: 10001;
2950
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
2951
+ font-size: 14px;
2952
+ line-height: 1.5;
2953
+ max-width: 400px;
2954
+ animation: slideInFromRight 0.3s cubic-bezier(0.4, 0, 0.2, 1);
2955
+ backdrop-filter: blur(10px);
2956
+ border: 1px solid rgba(255, 255, 255, 0.1);
2957
+ `;
2958
+ // 添加图标和消息
2959
+ errorDiv.innerHTML = `
2960
+ <div style="display: flex; align-items: start; gap: 12px;">
2961
+ <svg style="flex-shrink: 0; width: 20px; height: 20px; margin-top: 2px;" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2962
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
2963
+ <path d="M12 8V12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
2964
+ <circle cx="12" cy="16" r="1" fill="currentColor"/>
2965
+ </svg>
2966
+ <div style="flex: 1; word-break: break-word;">${escapeHtml$1(message)}</div>
2967
+ </div>
2968
+ `;
2969
+ // 添加 CSS 动画
2970
+ const style = document.createElement('style');
2971
+ style.textContent = `
2972
+ @keyframes slideInFromRight {
2973
+ from {
2974
+ transform: translateX(400px);
2975
+ opacity: 0;
2976
+ }
2977
+ to {
2978
+ transform: translateX(0);
2979
+ opacity: 1;
2980
+ }
2981
+ }
2982
+ @keyframes slideOutToRight {
2983
+ from {
2984
+ transform: translateX(0);
2985
+ opacity: 1;
2986
+ }
2987
+ to {
2988
+ transform: translateX(400px);
2989
+ opacity: 0;
2990
+ }
2991
+ }
2992
+ `;
2993
+ document.head.appendChild(style);
2994
+ document.body.appendChild(errorDiv);
2995
+ // 自动移除
2996
+ setTimeout(() => {
2997
+ errorDiv.style.animation = 'slideOutToRight 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
2998
+ setTimeout(() => {
2999
+ errorDiv.remove();
3000
+ style.remove();
3001
+ }, 300);
3002
+ }, duration);
3003
+ }
3004
+ /**
3005
+ * 显示成功消息
3006
+ *
3007
+ * @param message 成功消息
3008
+ * @param duration 显示时长(毫秒),默认 3000ms
3009
+ */
3010
+ function showSuccessMessage(message, duration = 3000) {
3011
+ // 检查是否已存在成功提示
3012
+ const existingSuccess = document.getElementById('payment-sdk-success-toast');
3013
+ if (existingSuccess) {
3014
+ existingSuccess.remove();
3015
+ }
3016
+ const successDiv = document.createElement('div');
3017
+ successDiv.id = 'payment-sdk-success-toast';
3018
+ successDiv.style.cssText = `
3019
+ position: fixed;
3020
+ top: 20px;
3021
+ right: 20px;
3022
+ background: linear-gradient(135deg, rgba(0, 255, 136, 0.95) 0%, rgba(34, 206, 156, 0.95) 100%);
3023
+ color: #0a1a14;
3024
+ padding: 16px 24px;
3025
+ border-radius: 12px;
3026
+ box-shadow: 0 8px 32px rgba(0, 255, 136, 0.4), 0 2px 8px rgba(0, 0, 0, 0.2);
3027
+ z-index: 10001;
3028
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
3029
+ font-size: 14px;
3030
+ line-height: 1.5;
3031
+ font-weight: 500;
3032
+ max-width: 400px;
3033
+ animation: slideInFromRight 0.3s cubic-bezier(0.4, 0, 0.2, 1);
3034
+ backdrop-filter: blur(10px);
3035
+ border: 1px solid rgba(255, 255, 255, 0.2);
3036
+ `;
3037
+ successDiv.innerHTML = `
3038
+ <div style="display: flex; align-items: start; gap: 12px;">
3039
+ <svg style="flex-shrink: 0; width: 20px; height: 20px; margin-top: 2px;" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
3040
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
3041
+ <path d="M8 12L11 15L16 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
3042
+ </svg>
3043
+ <div style="flex: 1; word-break: break-word;">${escapeHtml$1(message)}</div>
3044
+ </div>
3045
+ `;
3046
+ document.body.appendChild(successDiv);
3047
+ setTimeout(() => {
3048
+ successDiv.style.animation = 'slideOutToRight 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
3049
+ setTimeout(() => successDiv.remove(), 300);
3050
+ }, duration);
3051
+ }
3052
+ /**
3053
+ * 显示加载指示器
3054
+ *
3055
+ * @param message 加载消息,默认 'Initializing...'
3056
+ * @returns 加载指示器的 HTML 元素(用于后续移除)
3057
+ */
3058
+ function showLoadingIndicator(message = 'Initializing payment system...') {
3059
+ // 移除已存在的加载指示器
3060
+ const existing = document.getElementById('payment-sdk-loading-indicator');
3061
+ if (existing) {
3062
+ existing.remove();
3063
+ }
3064
+ const loader = document.createElement('div');
3065
+ loader.id = 'payment-sdk-loading-indicator';
3066
+ loader.style.cssText = `
3067
+ position: fixed;
3068
+ top: 50%;
3069
+ left: 50%;
3070
+ transform: translate(-50%, -50%);
3071
+ background: rgba(10, 26, 20, 0.98);
3072
+ padding: 40px 56px;
3073
+ border-radius: 20px;
3074
+ z-index: 10002;
3075
+ text-align: center;
3076
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
3077
+ border: 1px solid rgba(34, 206, 156, 0.2);
3078
+ backdrop-filter: blur(20px);
3079
+ animation: fadeIn 0.2s ease-out;
3080
+ `;
3081
+ loader.innerHTML = `
3082
+ <style>
3083
+ @keyframes fadeIn {
3084
+ from { opacity: 0; transform: translate(-50%, -48%); }
3085
+ to { opacity: 1; transform: translate(-50%, -50%); }
3086
+ }
3087
+ @keyframes spin {
3088
+ to { transform: rotate(360deg); }
3089
+ }
3090
+ </style>
3091
+ <div style="
3092
+ width: 48px;
3093
+ height: 48px;
3094
+ border: 4px solid rgba(34, 206, 156, 0.2);
3095
+ border-top-color: #22ce9c;
3096
+ border-radius: 50%;
3097
+ animation: spin 0.8s linear infinite;
3098
+ margin: 0 auto 20px;
3099
+ "></div>
3100
+ <div style="
3101
+ color: rgba(255, 255, 255, 0.9);
3102
+ font-size: 15px;
3103
+ font-weight: 500;
3104
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
3105
+ letter-spacing: 0.01em;
3106
+ ">${escapeHtml$1(message)}</div>
3107
+ `;
3108
+ document.body.appendChild(loader);
3109
+ return loader;
3110
+ }
3111
+ /**
3112
+ * 移除加载指示器
3113
+ *
3114
+ * @param loader 加载指示器元素(可选)
3115
+ */
3116
+ function hideLoadingIndicator(loader) {
3117
+ if (loader && loader.parentNode) {
3118
+ loader.style.animation = 'fadeOut 0.2s ease-in';
3119
+ setTimeout(() => loader.remove(), 200);
3120
+ }
3121
+ else {
3122
+ const existing = document.getElementById('payment-sdk-loading-indicator');
3123
+ if (existing) {
3124
+ existing.style.animation = 'fadeOut 0.2s ease-in';
3125
+ setTimeout(() => existing.remove(), 200);
3126
+ }
3127
+ }
3128
+ }
3129
+ /**
3130
+ * HTML 转义工具函数
3131
+ * 防止 XSS 攻击
3132
+ */
3133
+ function escapeHtml$1(text) {
3134
+ const div = document.createElement('div');
3135
+ div.textContent = text;
3136
+ return div.innerHTML;
3137
+ }
3138
+ /**
3139
+ * 显示信息提示
3140
+ *
3141
+ * @param message 信息内容
3142
+ * @param duration 显示时长(毫秒),默认 3000ms
3143
+ */
3144
+ function showInfoMessage(message, duration = 3000) {
3145
+ const existing = document.getElementById('payment-sdk-info-toast');
3146
+ if (existing) {
3147
+ existing.remove();
3148
+ }
3149
+ const infoDiv = document.createElement('div');
3150
+ infoDiv.id = 'payment-sdk-info-toast';
3151
+ infoDiv.style.cssText = `
3152
+ position: fixed;
3153
+ top: 20px;
3154
+ right: 20px;
3155
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.95) 0%, rgba(37, 99, 235, 0.95) 100%);
3156
+ color: white;
3157
+ padding: 16px 24px;
3158
+ border-radius: 12px;
3159
+ box-shadow: 0 8px 32px rgba(59, 130, 246, 0.4), 0 2px 8px rgba(0, 0, 0, 0.2);
3160
+ z-index: 10001;
3161
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
3162
+ font-size: 14px;
3163
+ line-height: 1.5;
3164
+ max-width: 400px;
3165
+ animation: slideInFromRight 0.3s cubic-bezier(0.4, 0, 0.2, 1);
3166
+ backdrop-filter: blur(10px);
3167
+ border: 1px solid rgba(255, 255, 255, 0.1);
3168
+ `;
3169
+ infoDiv.innerHTML = `
3170
+ <div style="display: flex; align-items: start; gap: 12px;">
3171
+ <svg style="flex-shrink: 0; width: 20px; height: 20px; margin-top: 2px;" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
3172
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
3173
+ <path d="M12 11V16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
3174
+ <circle cx="12" cy="8" r="1" fill="currentColor"/>
3175
+ </svg>
3176
+ <div style="flex: 1; word-break: break-word;">${escapeHtml$1(message)}</div>
3177
+ </div>
3178
+ `;
3179
+ document.body.appendChild(infoDiv);
3180
+ setTimeout(() => {
3181
+ infoDiv.style.animation = 'slideOutToRight 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
3182
+ setTimeout(() => infoDiv.remove(), 300);
3183
+ }, duration);
3184
+ }
3185
+
3186
+ /**
3187
+ * PurchaseSuccessModal - 购买成功弹窗
3188
+ * 纯 JavaScript 实现,不依赖 React
3189
+ *
3190
+ * 参考设计: credit-pack-success-modal
3191
+ * - 邮件确认卡片风格
3192
+ * - 精美的动画效果(淡入淡出、缩放)
3193
+ * - 包含成功图标、套餐详情、积分数、支付金额
3194
+ */
3195
+ /**
3196
+ * 购买成功弹窗类
3197
+ */
3198
+ class PurchaseSuccessModal {
3199
+ constructor(options) {
3200
+ this.overlay = null;
3201
+ this.modal = null;
3202
+ this.isExiting = false;
3203
+ this.scrollY = 0;
3204
+ this.boundHandleEscKey = null;
3205
+ this.options = {
3206
+ language: 'en',
3207
+ ...options,
3208
+ };
3209
+ }
3210
+ /**
3211
+ * 打开弹窗
3212
+ */
3213
+ open() {
3214
+ if (this.overlay)
3215
+ return; // 防止重复打开
3216
+ // 保存滚动位置并阻止 body 滚动
3217
+ this.scrollY = window.scrollY;
3218
+ document.body.style.position = 'fixed';
3219
+ document.body.style.top = `-${this.scrollY}px`;
3220
+ document.body.style.width = '100%';
3221
+ // 创建遮罩层和弹窗
3222
+ this.createModal();
3223
+ // 添加事件监听
3224
+ this.attachEventListeners();
3225
+ }
3226
+ /**
3227
+ * 关闭弹窗
3228
+ */
3229
+ close() {
3230
+ if (this.isExiting || !this.overlay)
3231
+ return;
3232
+ this.isExiting = true;
3233
+ // 添加退出动画
3234
+ this.overlay.style.animation = 'fadeOut 0.3s ease-in';
3235
+ if (this.modal) {
3236
+ this.modal.style.animation = 'slideOutDown 0.3s ease-in';
3237
+ }
3238
+ // 300ms 后移除元素
3239
+ setTimeout(() => {
3240
+ this.cleanup();
3241
+ this.options.onClose?.();
3242
+ }, 300);
3243
+ }
3244
+ /**
3245
+ * 创建弹窗元素
3246
+ */
3247
+ createModal() {
3248
+ const { data, language } = this.options;
3249
+ const isZh = language === 'zh-CN';
3250
+ // 创建遮罩层
3251
+ this.overlay = document.createElement('div');
3252
+ this.overlay.id = 'purchase-success-modal-overlay';
3253
+ this.overlay.style.cssText = `
3254
+ position: fixed;
3255
+ inset: 0;
3256
+ z-index: 10001;
3257
+ display: flex;
3258
+ align-items: center;
3259
+ justify-content: center;
3260
+ padding: 20px;
3261
+ background: rgba(15, 23, 42, 0.75);
3262
+ backdrop-filter: blur(8px);
3263
+ animation: fadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
3264
+ `;
3265
+ // 创建弹窗卡片
3266
+ this.modal = document.createElement('div');
3267
+ this.modal.style.cssText = `
3268
+ position: relative;
3269
+ width: 90vw;
3270
+ max-width: 680px;
3271
+ border-radius: 24px;
3272
+ background: linear-gradient(to bottom, #0f172a, #1e293b);
3273
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
3274
+ animation: slideInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
3275
+ `;
3276
+ this.modal.innerHTML = this.getModalHTML(data, isZh);
3277
+ this.overlay.appendChild(this.modal);
3278
+ document.body.appendChild(this.overlay);
3279
+ // 添加 CSS 动画
3280
+ this.addStyles();
3281
+ }
3282
+ /**
3283
+ * 生成弹窗 HTML
3284
+ */
3285
+ getModalHTML(data, isZh) {
3286
+ const texts = isZh ? {
3287
+ title: '购买成功!',
3288
+ subtitle: '您的积分已到账',
3289
+ greeting: '亲爱的用户,',
3290
+ message: '感谢您的购买。您的积分已成功添加到账户中。',
3291
+ packLabel: '套餐名称',
3292
+ creditsLabel: '积分数量',
3293
+ amountLabel: '支付金额',
3294
+ badge: '已支付',
3295
+ } : {
3296
+ title: 'Purchase Successful!',
3297
+ subtitle: 'Your credits have been added',
3298
+ greeting: 'Hi there,',
3299
+ message: 'Thank you for your purchase. Your credits have been successfully added to your account.',
3300
+ packLabel: 'Package',
3301
+ creditsLabel: 'Credits',
3302
+ amountLabel: 'Amount',
3303
+ badge: 'PAID',
3304
+ };
3305
+ const currency = data.currency || '$';
3306
+ return `
3307
+ <!-- 关闭按钮 -->
3308
+ <button
3309
+ type="button"
3310
+ id="success-modal-close-btn"
3311
+ style="
3312
+ position: absolute;
3313
+ top: 20px;
3314
+ right: 20px;
3315
+ z-index: 10;
3316
+ display: flex;
3317
+ width: 40px;
3318
+ height: 40px;
3319
+ align-items: center;
3320
+ justify-content: center;
3321
+ border-radius: 50%;
3322
+ background: rgba(255, 255, 255, 0.1);
3323
+ color: rgba(255, 255, 255, 0.7);
3324
+ border: none;
3325
+ cursor: pointer;
3326
+ transition: all 0.2s;
3327
+ "
3328
+ >
3329
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
3330
+ <path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
3331
+ </svg>
3332
+ </button>
3333
+
3334
+ <!-- 头部 -->
3335
+ <div style="
3336
+ border-radius: 24px 24px 0 0;
3337
+ background: linear-gradient(to bottom, rgba(16, 185, 129, 0.1), transparent);
3338
+ padding: 48px 32px;
3339
+ text-align: center;
3340
+ ">
3341
+ <!-- Success Icon -->
3342
+ <div style="
3343
+ margin: 0 auto 16px;
3344
+ width: 80px;
3345
+ height: 80px;
3346
+ animation: scaleIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
3347
+ ">
3348
+ <svg width="80" height="80" viewBox="0 0 80 80" fill="none">
3349
+ <circle cx="40" cy="40" r="36" fill="#10B981" fill-opacity="0.1" />
3350
+ <circle cx="40" cy="40" r="28" fill="#10B981" fill-opacity="0.2" />
3351
+ <path d="M28 40L36 48L52 32" stroke="#10B981" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
3352
+ </svg>
3353
+ </div>
3354
+
3355
+ <!-- Title -->
3356
+ <h2 style="
3357
+ margin: 0 0 8px;
3358
+ font-size: 32px;
3359
+ font-weight: 700;
3360
+ color: white;
3361
+ animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1) 0.2s backwards;
3362
+ ">${texts.title}</h2>
3363
+
3364
+ <!-- Subtitle -->
3365
+ <p style="
3366
+ margin: 0;
3367
+ font-size: 15px;
3368
+ color: rgba(255, 255, 255, 0.6);
3369
+ animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1) 0.3s backwards;
3370
+ ">${texts.subtitle}</p>
3371
+ </div>
3372
+
3373
+ <!-- 内容区域 -->
3374
+ <div style="padding: 0 32px 48px;">
3375
+ <!-- 确认卡片 -->
3376
+ <div style="
3377
+ overflow: hidden;
3378
+ border-radius: 16px;
3379
+ border: 1px solid rgba(255, 255, 255, 0.1);
3380
+ background: #1e293b;
3381
+ animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1) 0.4s backwards;
3382
+ ">
3383
+ <!-- 卡片头部 -->
3384
+ <div style="
3385
+ display: flex;
3386
+ align-items: center;
3387
+ justify-content: space-between;
3388
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
3389
+ background: #0f172a;
3390
+ padding: 24px;
3391
+ ">
3392
+ <div style="
3393
+ display: flex;
3394
+ align-items: center;
3395
+ gap: 12px;
3396
+ color: #10B981;
3397
+ ">
3398
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
3399
+ <path d="M12 2L2 7L12 12L22 7L12 2Z" fill="currentColor" opacity="0.3" />
3400
+ <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
3401
+ <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
3402
+ </svg>
3403
+ <span style="
3404
+ font-size: 18px;
3405
+ font-weight: 600;
3406
+ ">SeaVerse</span>
3407
+ </div>
3408
+ <div style="
3409
+ border-radius: 9999px;
3410
+ border: 1px solid rgba(168, 85, 247, 0.3);
3411
+ background: rgba(168, 85, 247, 0.1);
3412
+ padding: 6px 16px;
3413
+ font-size: 13px;
3414
+ font-weight: 500;
3415
+ color: #a855f7;
3416
+ ">${texts.badge}</div>
3417
+ </div>
3418
+
3419
+ <!-- 问候语 -->
3420
+ <div style="
3421
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
3422
+ padding: 32px 24px;
3423
+ ">
3424
+ <p style="
3425
+ margin: 0 0 12px;
3426
+ font-size: 15px;
3427
+ font-weight: 600;
3428
+ color: rgba(255, 255, 255, 0.9);
3429
+ ">${texts.greeting}</p>
3430
+ <p style="
3431
+ margin: 0;
3432
+ font-size: 14px;
3433
+ line-height: 1.6;
3434
+ color: rgba(255, 255, 255, 0.7);
3435
+ ">${texts.message}</p>
3436
+ </div>
3437
+
3438
+ <!-- 详情列表 -->
3439
+ <div style="
3440
+ background: rgba(15, 23, 42, 0.5);
3441
+ padding: 24px;
3442
+ ">
3443
+ <div style="
3444
+ display: flex;
3445
+ align-items: center;
3446
+ justify-content: space-between;
3447
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
3448
+ padding: 16px 0;
3449
+ ">
3450
+ <span style="
3451
+ font-size: 13px;
3452
+ font-weight: 500;
3453
+ color: rgba(255, 255, 255, 0.6);
3454
+ ">${texts.packLabel}</span>
3455
+ <span style="
3456
+ font-size: 14px;
3457
+ font-weight: 600;
3458
+ color: rgba(255, 255, 255, 0.95);
3459
+ ">${this.escapeHtml(data.packName)}</span>
3460
+ </div>
3461
+ <div style="
3462
+ display: flex;
3463
+ align-items: center;
3464
+ justify-content: space-between;
3465
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
3466
+ padding: 16px 0;
3467
+ ">
3468
+ <span style="
3469
+ font-size: 13px;
3470
+ font-weight: 500;
3471
+ color: rgba(255, 255, 255, 0.6);
3472
+ ">${texts.creditsLabel}</span>
3473
+ <span style="
3474
+ font-size: 14px;
3475
+ font-weight: 600;
3476
+ color: rgba(255, 255, 255, 0.95);
3477
+ ">${data.credits.toLocaleString()} Credits</span>
3478
+ </div>
3479
+ <div style="
3480
+ display: flex;
3481
+ align-items: center;
3482
+ justify-content: space-between;
3483
+ background: linear-gradient(to right, rgba(16, 185, 129, 0.05), transparent);
3484
+ margin: 0 -24px;
3485
+ padding: 16px 24px;
3486
+ ">
3487
+ <span style="
3488
+ font-size: 13px;
3489
+ font-weight: 500;
3490
+ color: rgba(255, 255, 255, 0.6);
3491
+ ">${texts.amountLabel}</span>
3492
+ <span style="
3493
+ font-size: 20px;
3494
+ font-weight: 600;
3495
+ color: #10B981;
3496
+ ">${currency}${this.escapeHtml(data.amount)}</span>
3497
+ </div>
3498
+ </div>
3499
+ </div>
3500
+ </div>
3501
+ `;
3502
+ }
3503
+ /**
3504
+ * 添加 CSS 动画
3505
+ */
3506
+ addStyles() {
3507
+ // 检查是否已添加样式
3508
+ if (document.getElementById('purchase-success-modal-styles'))
3509
+ return;
3510
+ const style = document.createElement('style');
3511
+ style.id = 'purchase-success-modal-styles';
3512
+ style.textContent = `
3513
+ @keyframes fadeIn {
3514
+ from {
3515
+ opacity: 0;
3516
+ }
3517
+ to {
3518
+ opacity: 1;
3519
+ }
3520
+ }
3521
+
3522
+ @keyframes fadeOut {
3523
+ from {
3524
+ opacity: 1;
3525
+ }
3526
+ to {
3527
+ opacity: 0;
3528
+ }
3529
+ }
3530
+
3531
+ @keyframes slideInUp {
3532
+ from {
3533
+ opacity: 0;
3534
+ transform: translateY(16px);
3535
+ }
3536
+ to {
3537
+ opacity: 1;
3538
+ transform: translateY(0);
3539
+ }
3540
+ }
3541
+
3542
+ @keyframes slideOutDown {
3543
+ from {
3544
+ opacity: 1;
3545
+ transform: translateY(0);
3546
+ }
3547
+ to {
3548
+ opacity: 0;
3549
+ transform: translateY(16px);
3550
+ }
3551
+ }
3552
+
3553
+ @keyframes scaleIn {
3554
+ from {
3555
+ opacity: 0;
3556
+ transform: scale(0.8);
3557
+ }
3558
+ to {
3559
+ opacity: 1;
3560
+ transform: scale(1);
3561
+ }
3562
+ }
3563
+
3564
+ @keyframes fadeInUp {
3565
+ from {
3566
+ opacity: 0;
3567
+ transform: translateY(8px);
3568
+ }
3569
+ to {
3570
+ opacity: 1;
3571
+ transform: translateY(0);
3572
+ }
3573
+ }
3574
+
3575
+ #success-modal-close-btn:hover {
3576
+ background: rgba(255, 255, 255, 0.15) !important;
3577
+ color: white !important;
3578
+ transform: rotate(90deg);
3579
+ }
3580
+ `;
3581
+ document.head.appendChild(style);
3582
+ }
3583
+ /**
3584
+ * 添加事件监听
3585
+ */
3586
+ attachEventListeners() {
3587
+ if (!this.overlay)
3588
+ return;
3589
+ // 点击遮罩关闭
3590
+ this.overlay.addEventListener('click', (e) => {
3591
+ if (e.target === this.overlay) {
3592
+ this.close();
3593
+ }
3594
+ });
3595
+ // 点击关闭按钮
3596
+ const closeBtn = document.getElementById('success-modal-close-btn');
3597
+ if (closeBtn) {
3598
+ closeBtn.addEventListener('click', () => this.close());
3599
+ }
3600
+ // ESC 键关闭
3601
+ this.boundHandleEscKey = this.handleEscKey.bind(this);
3602
+ document.addEventListener('keydown', this.boundHandleEscKey);
3603
+ }
3604
+ /**
3605
+ * 处理 ESC 键
3606
+ */
3607
+ handleEscKey(e) {
3608
+ if (e.key === 'Escape') {
3609
+ this.close();
3610
+ }
3611
+ }
3612
+ /**
3613
+ * 清理资源
3614
+ */
3615
+ cleanup() {
3616
+ // 移除事件监听
3617
+ if (this.boundHandleEscKey) {
3618
+ document.removeEventListener('keydown', this.boundHandleEscKey);
3619
+ this.boundHandleEscKey = null;
3620
+ }
3621
+ // 移除元素
3622
+ if (this.overlay && this.overlay.parentNode) {
3623
+ this.overlay.parentNode.removeChild(this.overlay);
3624
+ }
3625
+ // 恢复 body 滚动
3626
+ document.body.style.position = '';
3627
+ document.body.style.top = '';
3628
+ document.body.style.width = '';
3629
+ window.scrollTo(0, this.scrollY);
3630
+ // 清空引用
3631
+ this.overlay = null;
3632
+ this.modal = null;
3633
+ this.isExiting = false;
3634
+ }
3635
+ /**
3636
+ * HTML 转义工具函数
3637
+ * 防止 XSS 攻击
3638
+ */
3639
+ escapeHtml(text) {
3640
+ const div = document.createElement('div');
3641
+ div.textContent = text;
3642
+ return div.innerHTML;
3643
+ }
3644
+ /**
3645
+ * 检查弹窗是否打开
3646
+ */
3647
+ isOpen() {
3648
+ return this.overlay !== null && !this.isExiting;
3649
+ }
3650
+ }
3651
+
3652
+ /**
3653
+ * CreditPackageModal - 积分套餐选择弹框
3654
+ * 展示不同的积分套餐供用户选择
3655
+ */
3656
+ class CreditPackageModal {
3657
+ constructor(options) {
3658
+ this.resizeHandler = null;
3659
+ this.isInitializingSDK = false;
3660
+ this.sdkInitialized = false;
3661
+ // 设计系统常量
3662
+ this.SPACING = {
3663
+ xs: '8px',
3664
+ sm: '16px',
3665
+ md: '24px',
3666
+ lg: '32px',
3667
+ xl: '48px',
3668
+ };
3669
+ this.COLORS = {
3670
+ text: {
3671
+ primary: 'rgba(255, 255, 255, 0.95)',
3672
+ secondary: 'rgba(255, 255, 255, 0.65)',
3673
+ tertiary: 'rgba(255, 255, 255, 0.5)',
3674
+ },
3675
+ green: {
3676
+ primary: '#00ff88',
3677
+ secondary: '#00f2fe',
3678
+ accent: '#22ce9c',
3679
+ },
3680
+ };
3681
+ this.options = options;
3682
+ this.language = options.language || 'en';
3683
+ // 创建弹框
3684
+ this.modal = new PaymentModal({
3685
+ title: this.language === 'zh-CN'
3686
+ ? (options.title_cn || '选择您的创作力量')
3687
+ : (options.title || 'Choose Your Creative Power'),
3688
+ showCloseButton: true,
3689
+ closeOnOverlayClick: false, // 禁用点击空白处关闭
3690
+ closeOnEsc: false, // 禁用ESC键关闭
3691
+ maxWidth: '1200px',
3692
+ onClose: () => {
3693
+ this.cleanup();
3694
+ options.onClose?.();
3695
+ },
3696
+ });
3697
+ console.log('[CreditPackageModal] Created');
3698
+ }
3699
+ /**
3700
+ * 打开弹框
3701
+ */
3702
+ async open() {
3703
+ console.log('[CreditPackageModal] Opening modal...');
3704
+ this.modal.open();
3705
+ // 修改弹框背景为深色
3706
+ const modalElement = document.querySelector('.payment-modal');
3707
+ if (modalElement) {
3708
+ modalElement.style.background = '#0a0a0f';
3709
+ modalElement.style.border = '1px solid rgba(255, 255, 255, 0.1)';
3710
+ }
3711
+ // 修改标题样式
3712
+ const titleElement = document.querySelector('.payment-modal-title');
3713
+ if (titleElement) {
3714
+ titleElement.style.color = 'white';
3715
+ titleElement.style.fontSize = '28px';
3716
+ titleElement.style.fontWeight = '700';
3717
+ titleElement.style.padding = '32px 32px 0 32px';
3718
+ titleElement.style.marginBottom = '16px';
3719
+ titleElement.style.letterSpacing = '-0.01em';
3720
+ }
3721
+ // 修改关闭按钮颜色并添加 hover 动画
3722
+ const closeButton = document.querySelector('.payment-modal-close');
3723
+ if (closeButton) {
3724
+ closeButton.style.background = 'rgba(255, 255, 255, 0.05)';
3725
+ closeButton.style.color = 'rgba(255, 255, 255, 0.7)';
3726
+ closeButton.style.transition = 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)';
3727
+ // 移除旧的事件监听器(如果有)
3728
+ const newCloseButton = closeButton.cloneNode(true);
3729
+ closeButton.parentNode?.replaceChild(newCloseButton, closeButton);
3730
+ // 添加 hover 动画
3731
+ newCloseButton.addEventListener('mouseenter', () => {
3732
+ newCloseButton.style.background = 'rgba(255, 255, 255, 0.1)';
3733
+ newCloseButton.style.color = 'white';
3734
+ newCloseButton.style.transform = 'rotate(90deg) scale(1.1)';
3735
+ });
3736
+ newCloseButton.addEventListener('mouseleave', () => {
3737
+ newCloseButton.style.background = 'rgba(255, 255, 255, 0.05)';
3738
+ newCloseButton.style.color = 'rgba(255, 255, 255, 0.7)';
3739
+ newCloseButton.style.transform = 'rotate(0deg) scale(1)';
3740
+ });
3741
+ newCloseButton.addEventListener('click', () => this.close());
3742
+ }
3743
+ // 渲染内容
3744
+ this.renderContent();
3745
+ // 添加resize监听器,窗口大小改变时重新渲染以应用响应式样式
3746
+ this.resizeHandler = () => {
3747
+ this.renderContent();
3748
+ };
3749
+ window.addEventListener('resize', this.resizeHandler);
3750
+ // 如果配置了 initPaymentSDK,在后台自动初始化 SDK
3751
+ if (!this.sdkInitialized && !this.isInitializingSDK) {
3752
+ this.initializeSDK();
3753
+ }
3754
+ console.log('[CreditPackageModal] Modal opened');
3755
+ }
3756
+ /**
3757
+ * 等待 SDK 初始化完成(支持超时和重试)
3758
+ * @param timeout 超时时间(毫秒),默认 30 秒
3759
+ * @param maxRetries 最大重试次数,默认 1 次
3760
+ * @returns 是否初始化成功
3761
+ */
3762
+ async waitForSDKInitialization(timeout = 30000, maxRetries = 1) {
3763
+ const startTime = Date.now();
3764
+ // 如果已经初始化完成,直接返回
3765
+ if (this.sdkInitialized) {
3766
+ console.log('[CreditPackageModal] SDK already initialized');
3767
+ return true;
3768
+ }
3769
+ // 如果还没开始初始化且有配置,主动触发
3770
+ if (!this.isInitializingSDK && this.options.sdkConfig) {
3771
+ console.log('[CreditPackageModal] Starting SDK initialization...');
3772
+ await this.initializeSDK();
3773
+ if (this.sdkInitialized) {
3774
+ return true;
3775
+ }
3776
+ }
3777
+ // 等待初始化完成
3778
+ console.log('[CreditPackageModal] Waiting for SDK initialization...');
3779
+ while (Date.now() - startTime < timeout) {
3780
+ if (this.sdkInitialized) {
3781
+ console.log('[CreditPackageModal] SDK initialization completed');
3782
+ return true;
3783
+ }
3784
+ if (!this.isInitializingSDK) {
3785
+ // 初始化已结束但未成功,尝试重试
3786
+ if (maxRetries > 0) {
3787
+ console.log(`[CreditPackageModal] SDK initialization failed, retrying... (${maxRetries} retries left)`);
3788
+ await this.initializeSDK();
3789
+ return this.waitForSDKInitialization(timeout - (Date.now() - startTime), maxRetries - 1);
3790
+ }
3791
+ else {
3792
+ console.error('[CreditPackageModal] SDK initialization failed after all retries');
3793
+ return false;
3794
+ }
3795
+ }
3796
+ // 等待 100ms 后重试
3797
+ await new Promise(resolve => setTimeout(resolve, 100));
3798
+ }
3799
+ // 超时
3800
+ console.error('[CreditPackageModal] SDK initialization timed out');
3801
+ return false;
3802
+ }
3803
+ /**
3804
+ * 初始化支付SDK(后台静默执行)
3805
+ */
3806
+ async initializeSDK() {
3807
+ if (this.isInitializingSDK || this.sdkInitialized) {
3808
+ return;
3809
+ }
3810
+ if (!this.options.sdkConfig) {
3811
+ console.log('[CreditPackageModal] No SDK configuration provided, skipping initialization');
3812
+ return;
3813
+ }
3814
+ this.isInitializingSDK = true;
3815
+ console.log('[CreditPackageModal] Initializing payment SDK...');
3816
+ // 显示加载指示器
3817
+ const loader = showLoadingIndicator('Initializing payment system...');
3818
+ try {
3819
+ const config = this.options.sdkConfig;
3820
+ // 1. 从环境配置中获取基础配置
3821
+ const envConfig = ENVIRONMENT_CONFIGS[config.environment];
3822
+ // 2. 合并配置(自定义配置优先级高于环境配置)
3823
+ const finalConfig = {
3824
+ scriptUrl: config.scriptUrl || envConfig.scriptUrl,
3825
+ clientId: config.clientId || envConfig.clientId,
3826
+ orderApiUrl: config.orderApiUrl || envConfig.orderApiUrl,
3827
+ cssUrl: config.cssUrl || envConfig.cssUrl,
3828
+ };
3829
+ console.log('[CreditPackageModal] Using environment:', config.environment);
3830
+ // 3. 初始化 SeaartPaymentSDK
3831
+ await SeaartPaymentSDK.getInstance().init({
3832
+ scriptUrl: finalConfig.scriptUrl,
3833
+ clientId: finalConfig.clientId,
3834
+ language: 'en',
3835
+ scriptTimeout: config.scriptTimeout,
3836
+ cssUrl: finalConfig.cssUrl,
3837
+ });
3838
+ // 4. 获取支付方式列表
3839
+ const paymentMethods = await SeaartPaymentSDK.getInstance().getPaymentMethods({
3840
+ country_code: config.countryCode,
3841
+ business_type: config.businessType ?? 1, // 默认为 1(一次性购买)
3842
+ });
3843
+ // 5. 查找匹配的支付方式
3844
+ const paymentMethod = config.paymentMethodType
3845
+ ? paymentMethods.find((m) => m.payment_method_type === config.paymentMethodType ||
3846
+ m.payment_method_name.toLowerCase().includes(config.paymentMethodType.toLowerCase()))
3847
+ : paymentMethods.find((m) => m.payment_type === 2); // 默认使用 dropin (payment_type === 2)
3848
+ if (!paymentMethod) {
3849
+ throw new Error(`Payment method "${config.paymentMethodType || 'dropin'}" not found`);
3850
+ }
3851
+ // 6. 存储到类成员变量(包括 finalConfig 以供后续使用)
3852
+ this.options.paymentMethod = paymentMethod;
3853
+ this.options.accountToken = config.accountToken;
3854
+ this.sdkInitialized = true;
3855
+ // 存储最终配置到 config 对象(用于 handlePaymentFlow)
3856
+ this.options.sdkConfig._resolvedOrderApiUrl = finalConfig.orderApiUrl;
3857
+ console.log('[CreditPackageModal] SDK initialized with environment config:', {
3858
+ environment: config.environment,
3859
+ paymentMethod: paymentMethod.payment_method_name,
3860
+ accountToken: config.accountToken ? 'provided' : 'not provided',
3861
+ });
3862
+ }
3863
+ catch (error) {
3864
+ console.error('[CreditPackageModal] Failed to initialize payment SDK:', error);
3865
+ // SDK 初始化失败不影响浏览积分包,只是无法进行支付
3866
+ }
3867
+ finally {
3868
+ this.isInitializingSDK = false;
3869
+ // 隐藏加载指示器
3870
+ hideLoadingIndicator(loader);
3871
+ }
3872
+ }
3873
+ /**
3874
+ * 关闭弹框
3875
+ */
3876
+ close() {
3877
+ console.log('[CreditPackageModal] Closing modal...');
3878
+ this.modal.close();
3879
+ }
3880
+ /**
3881
+ * 获取响应式样式配置
3882
+ */
3883
+ getResponsiveStyles() {
3884
+ const isMobile = window.matchMedia('(max-width: 768px)').matches;
3885
+ const isTablet = window.matchMedia('(max-width: 1200px)').matches;
3886
+ const isLaptop = window.matchMedia('(max-width: 1400px)').matches;
3887
+ let computeColumns = 5;
3888
+ let packColumns = 4;
3889
+ let padding = '0 60px 60px';
3890
+ if (isMobile) {
3891
+ computeColumns = 1;
3892
+ packColumns = 1;
3893
+ padding = '0 20px 20px';
3894
+ }
3895
+ else if (isTablet) {
3896
+ computeColumns = 2;
3897
+ packColumns = 2;
3898
+ padding = '0 30px 30px';
3899
+ }
3900
+ else if (isLaptop) {
3901
+ computeColumns = 3;
3902
+ packColumns = 2;
3903
+ padding = '0 40px 40px';
3904
+ }
3905
+ return {
3906
+ containerPadding: padding,
3907
+ computeGridColumns: `repeat(${computeColumns}, 1fr)`,
3908
+ packGridColumns: `repeat(${packColumns}, 1fr)`,
3909
+ };
3910
+ }
3911
+ /**
3912
+ * 渲染弹框内容
3913
+ */
3914
+ renderContent() {
3915
+ const container = this.modal.getContentContainer();
3916
+ if (!container) {
3917
+ throw new Error('Modal content container not found');
3918
+ }
3919
+ const isZh = this.language === 'zh-CN';
3920
+ const styles = this.getResponsiveStyles();
3921
+ container.innerHTML = `
3922
+ <div style="
3923
+ padding: ${styles.containerPadding};
3924
+ background: #0a0a0f;
3925
+ color: white;
3926
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
3927
+ ">
3928
+ <!-- 副标题 -->
3929
+ <p style="
3930
+ text-align: center;
3931
+ font-size: 16px;
3932
+ color: ${this.COLORS.text.secondary};
3933
+ margin: 0 0 ${this.SPACING.xl} 0;
3934
+ line-height: 1.6;
3935
+ font-weight: 400;
3936
+ ">
3937
+ ${isZh
3938
+ ? (this.options.subtitle_cn || '免费开始,随创作扩展。所有套餐都包含用于电影、游戏、音乐和世界的算力积分。')
3939
+ : (this.options.subtitle || 'Start free, scale with creation. All packages include credits for movies, games, music, and worlds.')}
3940
+ </p>
3941
+
3942
+ <!-- 算力积分说明区域 - 完全复刻 next-meta pricing -->
3943
+ <div style="margin-bottom: ${this.SPACING.xl};">
3944
+ <!-- Section Header -->
3945
+ <div style="margin-bottom: ${this.SPACING.xl}; text-align: center;">
3946
+ <h2 style="
3947
+ margin-bottom: ${this.SPACING.xs};
3948
+ font-size: 24px;
3949
+ font-weight: 700;
3950
+ color: ${this.COLORS.text.primary};
3951
+ letter-spacing: -0.02em;
3952
+ ">
3953
+ ${isZh ? '算力积分消耗' : 'Compute Credits'}
3954
+ </h2>
3955
+ <p style="
3956
+ margin: 0 auto;
3957
+ max-width: 48rem;
3958
+ font-size: 16px;
3959
+ color: ${this.COLORS.text.secondary};
3960
+ line-height: 1.6;
3961
+ ">
3962
+ ${isZh ? '创作所需的算力积分' : 'Credits needed for creation'}
3963
+ </p>
3964
+ </div>
3965
+
3966
+ <!-- Credits Grid - 完全复刻 next-meta 的 5 个卡片 -->
3967
+ <div style="
3968
+ display: grid;
3969
+ grid-template-columns: ${styles.computeGridColumns};
3970
+ gap: 12px;
3971
+ margin-bottom: 20px;
3972
+ ">
3973
+ <!-- Film Clips -->
3974
+ <article style="
3975
+ position: relative;
3976
+ overflow: hidden;
3977
+ border-radius: 16px;
3978
+ border: 1px solid rgba(255, 255, 255, 0.06);
3979
+ background: rgba(255, 255, 255, 0.05);
3980
+ padding: 20px;
3981
+ text-align: center;
3982
+ backdrop-filter: blur(40px);
3983
+ opacity: 0.7;
3984
+ transform: scale(0.98);
3985
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
3986
+ " onmouseover="this.style.transform='translateY(-4px) scale(1)'; this.style.opacity='1'; this.style.borderColor='rgba(255, 255, 255, 0.12)'; this.style.boxShadow='0 20px 60px rgba(0,0,0,0.4)';"
3987
+ onmouseout="this.style.transform='scale(0.98)'; this.style.opacity='0.7'; this.style.borderColor='rgba(255, 255, 255, 0.06)'; this.style.boxShadow='none';">
3988
+ <div style="
3989
+ margin: 0 auto 16px;
3990
+ display: flex;
3991
+ align-items: center;
3992
+ justify-content: center;
3993
+ height: 48px;
3994
+ width: 48px;
3995
+ border-radius: 12px;
3996
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
3997
+ box-shadow: 0 8px 20px rgba(0,0,0,0.2);
3998
+ ">
3999
+ <svg style="height: 24px; width: 24px; color: white;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4000
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>
4001
+ </svg>
4002
+ </div>
4003
+ <h3 style="margin-bottom: 8px; font-size: 16px; font-weight: 600; color: white;">
4004
+ ${isZh ? '电影片段' : 'Film Clips'}
4005
+ </h3>
4006
+ <p style="margin-bottom: 8px; font-family: 'Monaco', 'Menlo', monospace; font-size: 20px; font-weight: 800; color: #00ff88;">
4007
+ 100-300
4008
+ </p>
4009
+ <p style="font-size: 12px; line-height: 1.5; color: rgba(255, 255, 255, 0.5);">
4010
+ ${isZh ? '每次生成' : 'per generation'}
4011
+ </p>
4012
+ </article>
4013
+
4014
+ <!-- Game Scenes -->
4015
+ <article style="
4016
+ position: relative;
4017
+ overflow: hidden;
4018
+ border-radius: 16px;
4019
+ border: 1px solid rgba(255, 255, 255, 0.06);
4020
+ background: rgba(255, 255, 255, 0.05);
4021
+ padding: 20px;
4022
+ text-align: center;
4023
+ backdrop-filter: blur(40px);
4024
+ opacity: 0.7;
4025
+ transform: scale(0.98);
4026
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
4027
+ " onmouseover="this.style.transform='translateY(-4px) scale(1)'; this.style.opacity='1'; this.style.borderColor='rgba(255, 255, 255, 0.12)'; this.style.boxShadow='0 20px 60px rgba(0,0,0,0.4)';"
4028
+ onmouseout="this.style.transform='scale(0.98)'; this.style.opacity='0.7'; this.style.borderColor='rgba(255, 255, 255, 0.06)'; this.style.boxShadow='none';">
4029
+ <div style="
4030
+ margin: 0 auto 16px;
4031
+ display: flex;
4032
+ align-items: center;
4033
+ justify-content: center;
4034
+ height: 48px;
4035
+ width: 48px;
4036
+ border-radius: 12px;
4037
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
4038
+ box-shadow: 0 8px 20px rgba(0,0,0,0.2);
4039
+ ">
4040
+ <svg style="height: 24px; width: 24px; color: white;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4041
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>
4042
+ </svg>
4043
+ </div>
4044
+ <h3 style="margin-bottom: 8px; font-size: 16px; font-weight: 600; color: white;">
4045
+ ${isZh ? '游戏场景' : 'Game Scenes'}
4046
+ </h3>
4047
+ <p style="margin-bottom: 8px; font-family: 'Monaco', 'Menlo', monospace; font-size: 20px; font-weight: 800; color: #00ff88;">
4048
+ 50-200
4049
+ </p>
4050
+ <p style="font-size: 12px; line-height: 1.5; color: rgba(255, 255, 255, 0.5);">
4051
+ ${isZh ? '每个场景' : 'per scene'}
4052
+ </p>
4053
+ </article>
4054
+
4055
+ <!-- Music -->
4056
+ <article style="
4057
+ position: relative;
4058
+ overflow: hidden;
4059
+ border-radius: 16px;
4060
+ border: 1px solid rgba(255, 255, 255, 0.06);
4061
+ background: rgba(255, 255, 255, 0.05);
4062
+ padding: 20px;
4063
+ text-align: center;
4064
+ backdrop-filter: blur(40px);
4065
+ opacity: 0.7;
4066
+ transform: scale(0.98);
4067
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
4068
+ " onmouseover="this.style.transform='translateY(-4px) scale(1)'; this.style.opacity='1'; this.style.borderColor='rgba(255, 255, 255, 0.12)'; this.style.boxShadow='0 20px 60px rgba(0,0,0,0.4)';"
4069
+ onmouseout="this.style.transform='scale(0.98)'; this.style.opacity='0.7'; this.style.borderColor='rgba(255, 255, 255, 0.06)'; this.style.boxShadow='none';">
4070
+ <div style="
4071
+ margin: 0 auto 16px;
4072
+ display: flex;
4073
+ align-items: center;
4074
+ justify-content: center;
4075
+ height: 48px;
4076
+ width: 48px;
4077
+ border-radius: 12px;
4078
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
4079
+ box-shadow: 0 8px 20px rgba(0,0,0,0.2);
4080
+ ">
4081
+ <svg style="height: 24px; width: 24px; color: white;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4082
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"/>
4083
+ </svg>
4084
+ </div>
4085
+ <h3 style="margin-bottom: 8px; font-size: 16px; font-weight: 600; color: white;">
4086
+ ${isZh ? '音乐' : 'Music'}
4087
+ </h3>
4088
+ <p style="margin-bottom: 8px; font-family: 'Monaco', 'Menlo', monospace; font-size: 20px; font-weight: 800; color: #00ff88;">
4089
+ 30-100
4090
+ </p>
4091
+ <p style="font-size: 12px; line-height: 1.5; color: rgba(255, 255, 255, 0.5);">
4092
+ ${isZh ? '每首曲目' : 'per track'}
4093
+ </p>
4094
+ </article>
4095
+
4096
+ <!-- 3D Worlds -->
4097
+ <article style="
4098
+ position: relative;
4099
+ overflow: hidden;
4100
+ border-radius: 16px;
4101
+ border: 1px solid rgba(255, 255, 255, 0.06);
4102
+ background: rgba(255, 255, 255, 0.05);
4103
+ padding: 20px;
4104
+ text-align: center;
4105
+ backdrop-filter: blur(40px);
4106
+ opacity: 0.7;
4107
+ transform: scale(0.98);
4108
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
4109
+ " onmouseover="this.style.transform='translateY(-4px) scale(1)'; this.style.opacity='1'; this.style.borderColor='rgba(255, 255, 255, 0.12)'; this.style.boxShadow='0 20px 60px rgba(0,0,0,0.4)';"
4110
+ onmouseout="this.style.transform='scale(0.98)'; this.style.opacity='0.7'; this.style.borderColor='rgba(255, 255, 255, 0.06)'; this.style.boxShadow='none';">
4111
+ <div style="
4112
+ margin: 0 auto 16px;
4113
+ display: flex;
4114
+ align-items: center;
4115
+ justify-content: center;
4116
+ height: 48px;
4117
+ width: 48px;
4118
+ border-radius: 12px;
4119
+ background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
4120
+ box-shadow: 0 8px 20px rgba(0,0,0,0.2);
4121
+ ">
4122
+ <svg style="height: 24px; width: 24px; color: white;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4123
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
4124
+ </svg>
4125
+ </div>
4126
+ <h3 style="margin-bottom: 8px; font-size: 16px; font-weight: 600; color: white;">
4127
+ ${isZh ? '3D 世界' : '3D Worlds'}
4128
+ </h3>
4129
+ <p style="margin-bottom: 8px; font-family: 'Monaco', 'Menlo', monospace; font-size: 20px; font-weight: 800; color: #00ff88;">
4130
+ 150-500
4131
+ </p>
4132
+ <p style="font-size: 12px; line-height: 1.5; color: rgba(255, 255, 255, 0.5);">
4133
+ ${isZh ? '每个世界' : 'per world'}
4134
+ </p>
4135
+ </article>
4136
+
4137
+ <!-- Agent Sessions -->
4138
+ <article style="
4139
+ position: relative;
4140
+ overflow: hidden;
4141
+ border-radius: 16px;
4142
+ border: 1px solid rgba(255, 255, 255, 0.06);
4143
+ background: rgba(255, 255, 255, 0.05);
4144
+ padding: 20px;
4145
+ text-align: center;
4146
+ backdrop-filter: blur(40px);
4147
+ opacity: 0.7;
4148
+ transform: scale(0.98);
4149
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
4150
+ " onmouseover="this.style.transform='translateY(-4px) scale(1)'; this.style.opacity='1'; this.style.borderColor='rgba(255, 255, 255, 0.12)'; this.style.boxShadow='0 20px 60px rgba(0,0,0,0.4)';"
4151
+ onmouseout="this.style.transform='scale(0.98)'; this.style.opacity='0.7'; this.style.borderColor='rgba(255, 255, 255, 0.06)'; this.style.boxShadow='none';">
4152
+ <div style="
4153
+ margin: 0 auto 16px;
4154
+ display: flex;
4155
+ align-items: center;
4156
+ justify-content: center;
4157
+ height: 48px;
4158
+ width: 48px;
4159
+ border-radius: 12px;
4160
+ background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
4161
+ box-shadow: 0 8px 20px rgba(0,0,0,0.2);
4162
+ ">
4163
+ <svg style="height: 24px; width: 24px; color: white;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4164
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/>
4165
+ </svg>
4166
+ </div>
4167
+ <h3 style="margin-bottom: 8px; font-size: 16px; font-weight: 600; color: white;">
4168
+ ${isZh ? 'AI 会话' : 'Agent Sessions'}
4169
+ </h3>
4170
+ <p style="margin-bottom: 8px; font-family: 'Monaco', 'Menlo', monospace; font-size: 20px; font-weight: 800; color: #00ff88;">
4171
+ 10
4172
+ </p>
4173
+ <p style="font-size: 12px; line-height: 1.5; color: rgba(255, 255, 255, 0.5);">
4174
+ ${isZh ? '每次会话' : 'per session'}
4175
+ </p>
4176
+ </article>
4177
+ </div>
4178
+ </div>
4179
+
4180
+ <!-- Credit Packs - 积分包购买标题 -->
4181
+ <div style="margin: 0 auto ${this.SPACING.lg}; max-width: 80rem; text-align: center;">
4182
+ <h3 style="
4183
+ margin-bottom: ${this.SPACING.xs};
4184
+ font-size: 24px;
4185
+ font-weight: 700;
4186
+ color: ${this.COLORS.text.primary};
4187
+ letter-spacing: -0.02em;
4188
+ ">
4189
+ ${isZh ? '需要更多积分?' : 'Need More Credits?'}
4190
+ </h3>
4191
+ <p style="
4192
+ font-size: 16px;
4193
+ color: ${this.COLORS.text.secondary};
4194
+ line-height: 1.6;
4195
+ ">
4196
+ ${isZh ? '随时购买永久积分包' : 'Purchase unlimited credit packages anytime'}
4197
+ <span style="margin-left: 4px; font-size: 14px; color: ${this.COLORS.text.tertiary};">
4198
+ (${isZh ? '需订阅' : 'Subscription Required'})
4199
+ </span>
4200
+ </p>
4201
+ </div>
4202
+
4203
+ <!-- 套餐卡片 -->
4204
+ <div style="
4205
+ display: grid;
4206
+ grid-template-columns: ${styles.packGridColumns};
4207
+ gap: 16px;
4208
+ ">
4209
+ ${CREDIT_PACKAGES.map((pkg, index) => this.renderPackageCard(pkg, index)).join('')}
4210
+ </div>
4211
+ </div>
4212
+ `;
4213
+ // 添加点击事件监听
4214
+ this.attachEventListeners(container);
4215
+ }
4216
+ /**
4217
+ * 渲染套餐卡片
4218
+ */
4219
+ renderPackageCard(pkg, index) {
4220
+ const isZh = this.language === 'zh-CN';
4221
+ const isPopular = pkg.is_popular;
4222
+ const hasBonus = parseInt(pkg.bonus_credits) > 0;
4223
+ // Popular包的呼吸动画
4224
+ const pulseAnimation = isPopular ? `
4225
+ <style>
4226
+ @keyframes pulse-${pkg.id} {
4227
+ 0%, 100% {
4228
+ transform: translateY(0) scale(1);
4229
+ box-shadow: 0 8px 32px rgba(0, 255, 136, 0.3);
4230
+ }
4231
+ 50% {
4232
+ transform: translateY(-2px) scale(1.02);
4233
+ box-shadow: 0 12px 40px rgba(0, 255, 136, 0.4);
4234
+ }
4235
+ }
4236
+ </style>
4237
+ ` : '';
4238
+ return `
4239
+ ${pulseAnimation}
4240
+ <div
4241
+ data-package-id="${pkg.id}"
4242
+ style="
4243
+ position: relative;
4244
+ display: flex;
4245
+ flex-direction: column;
4246
+ gap: 8px;
4247
+ border-radius: 16px;
4248
+ border: 1px solid ${isPopular ? 'rgba(0, 255, 136, 0.3)' : 'rgba(255, 255, 255, 0.06)'};
4249
+ background: ${isPopular ? 'linear-gradient(to bottom, rgba(0, 255, 136, 0.05), rgba(255, 255, 255, 0.05))' : 'rgba(255, 255, 255, 0.05)'};
4250
+ padding: 20px;
4251
+ text-align: center;
4252
+ backdrop-filter: blur(40px);
4253
+ cursor: pointer;
4254
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
4255
+ ${isPopular ? `animation: pulse-${pkg.id} 2s ease-in-out infinite;` : ''}
4256
+ "
4257
+ onmouseover="this.style.transform='translateY(-4px)${isPopular ? ' scale(1.02)' : ''}'; this.style.borderColor='${isPopular ? 'rgba(0, 255, 136, 0.5)' : 'rgba(255, 255, 255, 0.12)'}'; this.style.animationPlayState='paused';"
4258
+ onmouseout="this.style.transform='translateY(0)${isPopular ? ' scale(1)' : ''}'; this.style.borderColor='${isPopular ? 'rgba(0, 255, 136, 0.3)' : 'rgba(255, 255, 255, 0.06)'}'; this.style.animationPlayState='running';"
4259
+ >
4260
+ ${pkg.day_limit > 0 ? `
4261
+ <div style="
4262
+ position: absolute;
4263
+ top: -14px;
4264
+ left: 50%;
4265
+ z-index: 10;
4266
+ transform: translateX(-50%);
4267
+ border-radius: 9999px;
4268
+ border: 1px solid rgba(34, 206, 156, 0.3);
4269
+ background: #0a1a14;
4270
+ padding: 6px 16px;
4271
+ font-size: 12px;
4272
+ font-weight: 700;
4273
+ letter-spacing: 0.05em;
4274
+ white-space: nowrap;
4275
+ text-transform: uppercase;
4276
+ color: #22ce9c;
4277
+ box-shadow: 0 0 12px rgba(34, 206, 156, 0.2);
4278
+ ">
4279
+ ${isZh ? `每日限购 ${pkg.day_limit}` : `DAILY LIMIT: ${pkg.day_limit}`}
4280
+ </div>
4281
+ ` : ''}
4282
+
4283
+ <!-- 积分总数 -->
4284
+ <div style="
4285
+ margin-bottom: 8px;
4286
+ font-size: 36px;
4287
+ line-height: 1;
4288
+ font-weight: 700;
4289
+ letter-spacing: -0.02em;
4290
+ color: white;
4291
+ ">
4292
+ ${this.formatNumber(pkg.credits)}
4293
+ </div>
4294
+
4295
+ <!-- 积分明细 -->
4296
+ <div style="
4297
+ margin-bottom: 12px;
4298
+ display: flex;
4299
+ flex-wrap: wrap;
4300
+ align-items: center;
4301
+ justify-content: center;
4302
+ gap: 4px;
4303
+ ">
4304
+ <span style="
4305
+ font-size: 16px;
4306
+ font-weight: 500;
4307
+ color: hsl(240, 5%, 65%);
4308
+ ">
4309
+ ${this.formatNumber(pkg.base_credits)} ${isZh ? '积分' : 'Credits'}
4310
+ </span>
4311
+ ${hasBonus ? `
4312
+ <span style="
4313
+ border-radius: 4px;
4314
+ background: rgba(0, 255, 136, 0.12);
4315
+ padding: 2px 8px;
4316
+ font-size: 16px;
4317
+ font-weight: 500;
4318
+ color: rgba(0, 255, 136, 0.9);
4319
+ ">
4320
+ +${this.formatNumber(pkg.bonus_credits)} ${isZh ? '奖励' : 'Bonus'}
4321
+ </span>
4322
+ ` : ''}
4323
+ </div>
4324
+
4325
+ <!-- 购买按钮 -->
4326
+ <button
4327
+ data-package-button="${pkg.id}"
4328
+ style="
4329
+ margin-top: auto;
4330
+ width: 100%;
4331
+ border-radius: 12px;
4332
+ border: ${isPopular ? 'none' : '1px solid rgba(255, 255, 255, 0.12)'};
4333
+ background: ${isPopular ? 'linear-gradient(135deg, #00ff88 0%, #00f2fe 100%)' : 'rgba(255, 255, 255, 0.08)'};
4334
+ padding: 10px 20px;
4335
+ font-size: 14px;
4336
+ font-weight: ${isPopular ? '700' : '600'};
4337
+ color: ${isPopular ? '#0a0a0f' : 'white'};
4338
+ cursor: pointer;
4339
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
4340
+ box-shadow: ${isPopular ? '0 4px 16px rgba(0, 255, 136, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.3)' : 'none'};
4341
+ "
4342
+ onmouseover="this.style.borderColor='${isPopular ? 'none' : 'rgba(255, 255, 255, 0.25)'}'; this.style.background='${isPopular ? 'linear-gradient(135deg, #00ff88 0%, #00f2fe 100%)' : 'rgba(255, 255, 255, 0.15)'}'; this.style.boxShadow='${isPopular ? '0 6px 20px rgba(0, 255, 136, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.4)' : 'none'}'; this.style.transform='translateY(-2px)';"
4343
+ onmouseout="this.style.borderColor='${isPopular ? 'none' : 'rgba(255, 255, 255, 0.12)'}'; this.style.background='${isPopular ? 'linear-gradient(135deg, #00ff88 0%, #00f2fe 100%)' : 'rgba(255, 255, 255, 0.08)'}'; this.style.boxShadow='${isPopular ? '0 4px 16px rgba(0, 255, 136, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.3)' : 'none'}'; this.style.transform='translateY(0)';"
4344
+ >
4345
+ ${isZh ? '购买' : 'Buy'} $${pkg.price}
4346
+ </button>
4347
+ </div>
4348
+ `;
4349
+ }
4350
+ /**
4351
+ * 格式化数字(添加逗号)
4352
+ */
4353
+ formatNumber(num) {
4354
+ return parseInt(num).toLocaleString();
4355
+ }
4356
+ /**
4357
+ * 获取加载按钮的 HTML(带旋转动画)
4358
+ * @param text 加载文本
4359
+ * @param isPopular 是否为 Popular 套餐(用于调整颜色)
4360
+ */
4361
+ getLoadingButtonHTML(text, isPopular = false) {
4362
+ return `
4363
+ <svg style="
4364
+ display: inline-block;
4365
+ width: 16px;
4366
+ height: 16px;
4367
+ border: 2px solid ${isPopular ? 'rgba(10, 10, 15, 0.3)' : 'rgba(255, 255, 255, 0.3)'};
4368
+ border-top-color: ${isPopular ? '#0a0a0f' : 'white'};
4369
+ border-radius: 50%;
4370
+ animation: spin 0.6s linear infinite;
4371
+ " viewBox="0 0 24 24"></svg>
4372
+ <style>@keyframes spin { to { transform: rotate(360deg); } }</style>
4373
+ ${text ? `<span style="margin-left: 8px;">${text}</span>` : ''}
4374
+ `;
4375
+ }
4376
+ /**
4377
+ * 添加事件监听
4378
+ */
4379
+ attachEventListeners(container) {
4380
+ // 为每个套餐按钮添加点击事件
4381
+ CREDIT_PACKAGES.forEach(pkg => {
4382
+ const button = container.querySelector(`[data-package-button="${pkg.id}"]`);
4383
+ if (button) {
4384
+ button.addEventListener('click', async (e) => {
4385
+ e.preventDefault();
4386
+ e.stopPropagation();
4387
+ console.log('[CreditPackageModal] Package selected:', pkg.id);
4388
+ // 保存原始按钮文本
4389
+ const originalText = button.innerHTML;
4390
+ const originalDisabled = button.disabled;
4391
+ try {
4392
+ // 禁用按钮,显示初始化状态
4393
+ button.disabled = true;
4394
+ const isZh = this.language === 'zh-CN';
4395
+ button.innerHTML = this.getLoadingButtonHTML(isZh ? '初始化中...' : 'Initializing...', pkg.is_popular);
4396
+ // 等待 SDK 初始化(支持重试)
4397
+ const initialized = await this.waitForSDKInitialization(30000, 1);
4398
+ if (!initialized) {
4399
+ throw new Error('SDK initialization failed or timed out. Please try again.');
4400
+ }
4401
+ // SDK 初始化成功,执行支付流程
4402
+ await this.handlePaymentFlow(pkg, button, originalText);
4403
+ }
4404
+ catch (error) {
4405
+ console.error('[CreditPackageModal] Failed to process payment:', error);
4406
+ // 恢复按钮状态
4407
+ button.disabled = originalDisabled;
4408
+ button.innerHTML = originalText;
4409
+ // 显示错误提示(使用自定义 UI 替代 alert)
4410
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
4411
+ showErrorMessage(`Payment failed: ${errorMessage}`);
4412
+ // 触发失败回调
4413
+ this.options.onPaymentFailed?.(error instanceof Error ? error : new Error(String(error)));
4414
+ }
4415
+ });
4416
+ }
4417
+ });
4418
+ }
4419
+ /**
4420
+ * 处理支付流程
4421
+ */
4422
+ async handlePaymentFlow(pkg, button, originalHTML) {
4423
+ try {
4424
+ // 更新按钮状态为"创建订单中"
4425
+ button.innerHTML = this.getLoadingButtonHTML('Creating order...', pkg.is_popular);
4426
+ console.log('[CreditPackageModal] Creating order for package:', pkg.id);
4427
+ // 使用默认实现:调用解析后的 orderApiUrl
4428
+ const resolvedOrderApiUrl = this.options.sdkConfig._resolvedOrderApiUrl || ENVIRONMENT_CONFIGS[this.options.sdkConfig.environment].orderApiUrl;
4429
+ const response = await createOrder(resolvedOrderApiUrl, this.options.sdkConfig.accountToken || '', {
4430
+ product_id: pkg.id,
4431
+ purchase_type: this.options.sdkConfig.businessType ?? 1, // 默认为 1
4432
+ });
4433
+ console.log('[CreditPackageModal] Create order response:', response);
4434
+ if (!response || !response.transaction_id) {
4435
+ throw new Error('Failed to create order: Invalid response from API');
4436
+ }
4437
+ const orderId = response.transaction_id;
4438
+ console.log('[CreditPackageModal] Order created:', orderId);
4439
+ // 恢复按钮状态
4440
+ button.disabled = false;
4441
+ button.innerHTML = originalHTML;
4442
+ // 创建并打开支付弹框
4443
+ await this.openPaymentModal(orderId, pkg);
4444
+ }
4445
+ catch (error) {
4446
+ console.error('[CreditPackageModal] Payment flow failed:', error);
4447
+ // 恢复按钮状态
4448
+ button.disabled = false;
4449
+ button.innerHTML = originalHTML;
4450
+ // 显示错误提示(使用自定义 UI 替代 alert)
4451
+ showErrorMessage(`Payment failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
4452
+ // 触发失败回调
4453
+ this.options.onPaymentFailed?.(error instanceof Error ? error : new Error(String(error)));
4454
+ }
4455
+ }
4456
+ /**
4457
+ * 打开支付弹框
4458
+ */
4459
+ async openPaymentModal(orderId, pkg) {
4460
+ if (!this.options.paymentMethod) {
4461
+ throw new Error('Payment method not configured');
4462
+ }
4463
+ const isZh = this.language === 'zh-CN';
4464
+ // 创建支付实例(使用 orderId)
4465
+ let paymentInstance;
4466
+ if (this.options.sdkConfig || this.sdkInitialized) {
4467
+ // 方案A:使用 sdkConfig 初始化后,创建支付实例
4468
+ paymentInstance = window.SeaartPaymentComponent.createPayment({
4469
+ sys_order_id: orderId,
4470
+ account_token: this.options.accountToken,
4471
+ });
4472
+ }
4473
+ else {
4474
+ throw new Error('Payment SDK not initialized. Please provide sdkConfig or paymentSDK.');
4475
+ }
4476
+ const dropinModal = new DropinPaymentModal(paymentInstance, orderId, this.options.accountToken, this.options.paymentMethod, {
4477
+ modalTitle: isZh ? `购买 ${pkg.credits} 积分` : `Purchase ${pkg.credits} Credits`,
4478
+ onCompleted: (payload) => {
4479
+ console.log('[CreditPackageModal] Payment completed:', payload);
4480
+ this.close();
4481
+ // 显示购买成功弹窗
4482
+ const successModal = new PurchaseSuccessModal({
4483
+ data: {
4484
+ packName: isZh ? `${pkg.credits} 积分套餐` : `${pkg.credits} Credits Package`,
4485
+ credits: parseInt(pkg.credits),
4486
+ amount: pkg.price,
4487
+ currency: '$',
4488
+ orderId: orderId,
4489
+ transactionId: payload.transaction_id,
4490
+ },
4491
+ language: this.options.language,
4492
+ onClose: () => {
4493
+ // 弹窗关闭后刷新积分
4494
+ (async () => {
4495
+ try {
4496
+ const envConfig = ENVIRONMENT_CONFIGS[this.options.sdkConfig.environment];
4497
+ const walletApiUrl = envConfig.walletApiUrl;
4498
+ console.log('[CreditPackageModal] Refreshing credits from:', walletApiUrl);
4499
+ const creditDetail = await getCreditDetail(walletApiUrl, this.options.sdkConfig.accountToken);
4500
+ if (creditDetail) {
4501
+ console.log('[CreditPackageModal] Credits refreshed, total balance:', creditDetail.total_balance);
4502
+ }
4503
+ else {
4504
+ console.warn('[CreditPackageModal] Failed to refresh credits');
4505
+ }
4506
+ }
4507
+ catch (error) {
4508
+ console.error('[CreditPackageModal] Failed to refresh credits:', error);
4509
+ }
4510
+ })();
4511
+ // 触发用户回调
4512
+ this.options.onPaymentSuccess?.(orderId, payload.transaction_id);
4513
+ },
4514
+ });
4515
+ successModal.open();
4516
+ },
4517
+ onFailed: (payload) => {
4518
+ console.error('[CreditPackageModal] Payment failed:', payload);
4519
+ const error = new Error(payload.message || 'Payment failed');
4520
+ this.options.onPaymentFailed?.(error);
4521
+ },
4522
+ onError: (payload, error) => {
4523
+ console.error('[CreditPackageModal] Payment error:', error);
4524
+ this.options.onPaymentFailed?.(error);
4525
+ },
4526
+ });
4527
+ await dropinModal.open();
4528
+ }
4529
+ /**
4530
+ * 清理资源
4531
+ */
4532
+ cleanup() {
4533
+ console.log('[CreditPackageModal] Cleaning up...');
4534
+ // 移除resize监听器
4535
+ if (this.resizeHandler) {
4536
+ window.removeEventListener('resize', this.resizeHandler);
4537
+ this.resizeHandler = null;
4538
+ }
4539
+ }
4540
+ /**
4541
+ * 检查弹框是否打开
4542
+ */
4543
+ isOpen() {
4544
+ return this.modal.isModalOpen();
4545
+ }
4546
+ }
4547
+
4548
+ /**
4549
+ * GenericPackageModal - 通用套餐选择弹框
4550
+ * 支持多种套餐类型(破冰包、告急包、首充包等)
4551
+ * 套餐数据从外部配置传入,无硬编码
4552
+ */
4553
+ class GenericPackageModal {
4554
+ constructor(options) {
4555
+ this.resizeHandler = null;
4556
+ this.isInitializingSDK = false;
4557
+ this.sdkInitialized = false;
4558
+ // 验证必填字段
4559
+ if (!options.packages || options.packages.length === 0) {
4560
+ throw new Error('GenericPackageModal: packages array is required and cannot be empty');
4561
+ }
4562
+ this.options = options;
4563
+ this.language = options.language || 'en';
4564
+ // 创建弹框 - 根据套餐数量动态调整宽度
4565
+ const packageCount = options.packages.length;
4566
+ let maxWidth = '680px'; // 单个套餐默认宽度(优化宽高比,确保积分文本完整显示)
4567
+ if (packageCount === 2) {
4568
+ maxWidth = '1100px';
4569
+ }
4570
+ else if (packageCount >= 3) {
4571
+ maxWidth = '1200px';
4572
+ }
4573
+ this.modal = new PaymentModal({
4574
+ title: '', // 不显示标题
4575
+ showCloseButton: false, // 不显示弹框关闭按钮,使用卡片内的关闭按钮
4576
+ closeOnOverlayClick: false, // 禁用点击空白处关闭
4577
+ closeOnEsc: true, // 允许ESC键关闭
4578
+ maxWidth: maxWidth,
4579
+ onClose: () => {
4580
+ this.cleanup();
4581
+ options.onClose?.();
4582
+ },
4583
+ });
4584
+ console.log('[GenericPackageModal] Created with', options.packages.length, 'packages');
4585
+ }
4586
+ /**
4587
+ * 打开弹框
4588
+ */
4589
+ async open() {
4590
+ console.log('[GenericPackageModal] Opening modal...');
4591
+ this.modal.open();
4592
+ // 修改弹框样式 - 让卡片完全填充
4593
+ const modalElement = document.querySelector('.payment-modal');
4594
+ if (modalElement) {
4595
+ modalElement.style.background = 'transparent';
4596
+ modalElement.style.border = 'none';
4597
+ modalElement.style.padding = '0';
4598
+ modalElement.style.borderRadius = '16px';
4599
+ modalElement.style.overflow = 'hidden';
4600
+ }
4601
+ // 修改内容容器样式 - 移除padding
4602
+ const contentElement = document.querySelector('.payment-modal-content');
4603
+ if (contentElement) {
4604
+ contentElement.style.padding = '0';
4605
+ contentElement.style.margin = '0';
4606
+ }
4607
+ // 监听卡片内的关闭按钮事件
4608
+ const container = this.modal.getContentContainer();
4609
+ if (container) {
4610
+ container.addEventListener('close-modal', () => {
4611
+ this.close();
4612
+ });
4613
+ }
4614
+ // 渲染内容
4615
+ this.renderContent();
4616
+ // 添加resize监听器,窗口大小改变时重新渲染以应用响应式样式
4617
+ this.resizeHandler = () => {
4618
+ this.renderContent();
4619
+ };
4620
+ window.addEventListener('resize', this.resizeHandler);
4621
+ // 如果配置了 sdkConfig,在后台自动初始化 SDK
4622
+ if (!this.sdkInitialized && !this.isInitializingSDK) {
4623
+ this.initializeSDK();
4624
+ }
4625
+ console.log('[GenericPackageModal] Modal opened');
4626
+ }
4627
+ /**
4628
+ * 等待 SDK 初始化完成(支持超时和重试)
4629
+ * @param timeout 超时时间(毫秒),默认 30 秒
4630
+ * @param maxRetries 最大重试次数,默认 1 次
4631
+ * @returns 是否初始化成功
4632
+ */
4633
+ async waitForSDKInitialization(timeout = 30000, maxRetries = 1) {
4634
+ const startTime = Date.now();
4635
+ // 如果已经初始化完成,直接返回
4636
+ if (this.sdkInitialized) {
4637
+ console.log('[GenericPackageModal] SDK already initialized');
4638
+ return true;
4639
+ }
4640
+ // 如果还没开始初始化且有配置,主动触发
4641
+ if (!this.isInitializingSDK && this.options.sdkConfig) {
4642
+ console.log('[GenericPackageModal] Starting SDK initialization...');
4643
+ await this.initializeSDK();
4644
+ if (this.sdkInitialized) {
4645
+ return true;
4646
+ }
4647
+ }
4648
+ // 等待初始化完成
4649
+ console.log('[GenericPackageModal] Waiting for SDK initialization...');
4650
+ while (Date.now() - startTime < timeout) {
4651
+ if (this.sdkInitialized) {
4652
+ console.log('[GenericPackageModal] SDK initialization completed');
4653
+ return true;
4654
+ }
4655
+ if (!this.isInitializingSDK) {
4656
+ // 初始化已结束但未成功,尝试重试
4657
+ if (maxRetries > 0) {
4658
+ console.log(`[GenericPackageModal] SDK initialization failed, retrying... (${maxRetries} retries left)`);
4659
+ await this.initializeSDK();
4660
+ return this.waitForSDKInitialization(timeout - (Date.now() - startTime), maxRetries - 1);
4661
+ }
4662
+ else {
4663
+ console.error('[GenericPackageModal] SDK initialization failed after all retries');
4664
+ return false;
4665
+ }
4666
+ }
4667
+ // 等待 100ms 后重试
4668
+ await new Promise(resolve => setTimeout(resolve, 100));
4669
+ }
4670
+ // 超时
4671
+ console.error('[GenericPackageModal] SDK initialization timed out');
4672
+ return false;
4673
+ }
4674
+ /**
4675
+ * 初始化支付SDK(后台静默执行)
4676
+ */
4677
+ async initializeSDK() {
4678
+ if (this.isInitializingSDK || this.sdkInitialized) {
4679
+ return;
4680
+ }
4681
+ if (!this.options.sdkConfig) {
4682
+ console.log('[GenericPackageModal] No SDK configuration provided, skipping initialization');
4683
+ return;
4684
+ }
4685
+ this.isInitializingSDK = true;
4686
+ console.log('[GenericPackageModal] Initializing payment SDK...');
4687
+ // 显示加载指示器
4688
+ const loader = showLoadingIndicator('Initializing payment system...');
4689
+ try {
4690
+ const config = this.options.sdkConfig;
4691
+ // 1. 从环境配置中获取基础配置
4692
+ const envConfig = ENVIRONMENT_CONFIGS[config.environment];
4693
+ // 2. 合并配置(自定义配置优先级高于环境配置)
4694
+ const finalConfig = {
4695
+ scriptUrl: config.scriptUrl || envConfig.scriptUrl,
4696
+ clientId: config.clientId || envConfig.clientId,
4697
+ orderApiUrl: config.orderApiUrl || envConfig.orderApiUrl,
4698
+ cssUrl: config.cssUrl || envConfig.cssUrl,
4699
+ };
4700
+ console.log('[GenericPackageModal] Using environment:', config.environment);
4701
+ // 3. 初始化 SeaartPaymentSDK
4702
+ await SeaartPaymentSDK.getInstance().init({
4703
+ scriptUrl: finalConfig.scriptUrl,
4704
+ clientId: finalConfig.clientId,
4705
+ language: 'en',
4706
+ scriptTimeout: config.scriptTimeout,
4707
+ cssUrl: finalConfig.cssUrl,
4708
+ });
4709
+ // 4. 获取支付方式列表
4710
+ const paymentMethods = await SeaartPaymentSDK.getInstance().getPaymentMethods({
4711
+ country_code: config.countryCode,
4712
+ business_type: config.businessType ?? 1, // 默认为 1(一次性购买)
4713
+ });
4714
+ // 5. 查找匹配的支付方式
4715
+ const paymentMethod = config.paymentMethodType
4716
+ ? paymentMethods.find((m) => m.payment_method_type === config.paymentMethodType ||
4717
+ m.payment_method_name.toLowerCase().includes(config.paymentMethodType.toLowerCase()))
4718
+ : paymentMethods.find((m) => m.payment_type === 2); // 默认使用 dropin (payment_type === 2)
4719
+ if (!paymentMethod) {
4720
+ throw new Error(`Payment method "${config.paymentMethodType || 'dropin'}" not found`);
4721
+ }
4722
+ // 6. 存储到类成员变量(包括 finalConfig 以供后续使用)
4723
+ this.paymentMethod = paymentMethod;
4724
+ this.accountToken = config.accountToken;
4725
+ this.sdkInitialized = true;
4726
+ // 存储最终配置到 config 对象(用于 handlePaymentFlow)
4727
+ this.options.sdkConfig._resolvedOrderApiUrl = finalConfig.orderApiUrl;
4728
+ console.log('[GenericPackageModal] SDK initialized with environment config:', {
4729
+ environment: config.environment,
4730
+ paymentMethod: paymentMethod.payment_method_name,
4731
+ accountToken: config.accountToken ? 'provided' : 'not provided',
4732
+ });
4733
+ }
4734
+ catch (error) {
4735
+ console.error('[GenericPackageModal] Failed to initialize payment SDK:', error);
4736
+ // SDK 初始化失败不影响浏览套餐,只是无法进行支付
4737
+ }
4738
+ finally {
4739
+ this.isInitializingSDK = false;
4740
+ // 隐藏加载指示器
4741
+ hideLoadingIndicator(loader);
4742
+ }
4743
+ }
4744
+ /**
4745
+ * 关闭弹框
4746
+ */
4747
+ close() {
4748
+ console.log('[GenericPackageModal] Closing modal...');
4749
+ this.modal.close();
4750
+ }
4751
+ /**
4752
+ * 渲染弹框内容
4753
+ */
4754
+ renderContent() {
4755
+ const container = this.modal.getContentContainer();
4756
+ if (!container) {
4757
+ throw new Error('Modal content container not found');
4758
+ }
4759
+ // 直接渲染卡片内容,不添加任何外层包装
4760
+ container.innerHTML = this.options.packages.map((pkg, index) => this.renderPackageCard(pkg, index)).join('');
4761
+ // 添加点击事件监听
4762
+ this.attachEventListeners(container);
4763
+ }
4764
+ /**
4765
+ * 渲染套餐卡片
4766
+ */
4767
+ renderPackageCard(pkg, index) {
4768
+ const hasBonus = pkg.bonus_credits && parseInt(pkg.bonus_credits) > 0;
4769
+ const hasBonusPercentage = pkg.bonus_percentage && pkg.bonus_percentage > 0;
4770
+ // 根据套餐类型决定显示的标题
4771
+ let packageTitle = '';
4772
+ if (pkg.package_type === 'iceBreaker' || pkg.package_type === 'firstCharge') {
4773
+ packageTitle = 'One-time Only';
4774
+ }
4775
+ return `
4776
+ <div
4777
+ data-package-id="${pkg.id}"
4778
+ style="
4779
+ position: relative;
4780
+ display: flex;
4781
+ flex-direction: column;
4782
+ justify-content: center;
4783
+ align-items: center;
4784
+ gap: 48px;
4785
+ border-radius: 16px;
4786
+ border: 1px solid rgba(255, 255, 255, 0.1);
4787
+ background: #000000;
4788
+ padding: 80px 60px;
4789
+ text-align: center;
4790
+ cursor: pointer;
4791
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
4792
+ width: 100%;
4793
+ height: 100%;
4794
+ box-sizing: border-box;
4795
+ "
4796
+ onmouseover="this.style.borderColor='rgba(255, 255, 255, 0.2)';"
4797
+ onmouseout="this.style.borderColor='rgba(255, 255, 255, 0.1)';"
4798
+ >
4799
+ <!-- 套餐标题 - 卡片内顶部居中 -->
4800
+ ${packageTitle ? `
4801
+ <div style="
4802
+ position: absolute;
4803
+ top: 32px;
4804
+ left: 50%;
4805
+ transform: translateX(-50%);
4806
+ font-size: 44px;
4807
+ line-height: 1;
4808
+ font-weight: 400;
4809
+ color: white;
4810
+ letter-spacing: -0.02em;
4811
+ white-space: nowrap;
4812
+ ">
4813
+ ${packageTitle}
4814
+ </div>
4815
+ ` : ''}
4816
+
4817
+ <!-- 关闭按钮 - 右上角 -->
4818
+ <button
4819
+ onclick="this.closest('[data-package-id]').dispatchEvent(new CustomEvent('close-modal', { bubbles: true }));"
4820
+ style="
4821
+ position: absolute;
4822
+ top: 24px;
4823
+ right: 24px;
4824
+ width: 32px;
4825
+ height: 32px;
4826
+ border-radius: 50%;
4827
+ border: none;
4828
+ background: rgba(255, 255, 255, 0.1);
4829
+ color: rgba(255, 255, 255, 0.7);
4830
+ cursor: pointer;
4831
+ display: flex;
4832
+ align-items: center;
4833
+ justify-content: center;
4834
+ font-size: 20px;
4835
+ line-height: 1;
4836
+ transition: all 0.2s;
4837
+ z-index: 100;
4838
+ "
4839
+ onmouseover="this.style.background='rgba(255, 255, 255, 0.2)'; this.style.color='white';"
4840
+ onmouseout="this.style.background='rgba(255, 255, 255, 0.1)'; this.style.color='rgba(255, 255, 255, 0.7)';"
4841
+ >
4842
+ ×
4843
+ </button>
4844
+
4845
+ <!-- 折扣标签 - 关闭按钮下方 -->
4846
+ ${hasBonusPercentage ? `
4847
+ <div style="
4848
+ position: absolute;
4849
+ top: 92px;
4850
+ right: 30px;
4851
+ background: linear-gradient(135deg, #fa8c42 0%, #ff5a00 100%);
4852
+ color: white;
4853
+ padding: 8px 16px;
4854
+ border-radius: 8px;
4855
+ font-size: 20px;
4856
+ font-weight: 800;
4857
+ box-shadow: 0 4px 16px rgba(255, 90, 0, 0.5);
4858
+ white-space: nowrap;
4859
+ z-index: 10;
4860
+ ">
4861
+ +${pkg.bonus_percentage}%
4862
+ </div>
4863
+ ` : ''}
4864
+
4865
+ <!-- 积分显示区域 - 确保不换行 -->
4866
+ <div style="
4867
+ display: flex;
4868
+ flex-direction: column;
4869
+ align-items: center;
4870
+ justify-content: center;
4871
+ gap: 0;
4872
+ width: 100%;
4873
+ margin-top: 60px;
4874
+ ">
4875
+ <div style="
4876
+ display: flex;
4877
+ align-items: baseline;
4878
+ justify-content: center;
4879
+ gap: 12px;
4880
+ flex-wrap: nowrap;
4881
+ ">
4882
+ <!-- 基础积分 -->
4883
+ <span style="
4884
+ font-size: 80px;
4885
+ line-height: 1;
4886
+ font-weight: 400;
4887
+ color: white;
4888
+ letter-spacing: -0.04em;
4889
+ white-space: nowrap;
4890
+ ">
4891
+ ${this.formatNumber(pkg.base_credits || pkg.credits)}
4892
+ </span>
4893
+
4894
+ <!-- credits 文本 -->
4895
+ <span style="
4896
+ font-size: 52px;
4897
+ line-height: 1;
4898
+ font-weight: 400;
4899
+ color: white;
4900
+ letter-spacing: -0.04em;
4901
+ white-space: nowrap;
4902
+ ">
4903
+ credits
4904
+ </span>
4905
+
4906
+ <!-- 奖励积分 -->
4907
+ ${hasBonus ? `
4908
+ <span style="
4909
+ font-size: 52px;
4910
+ line-height: 1;
4911
+ font-weight: 400;
4912
+ color: #00ff88;
4913
+ letter-spacing: -0.04em;
4914
+ white-space: nowrap;
4915
+ ">
4916
+ +${this.formatNumber(pkg.bonus_credits)} bonus
4917
+ </span>
4918
+ ` : ''}
4919
+ </div>
4920
+ </div>
4921
+
4922
+ <!-- 副标题 - Valid for all platform products -->
4923
+ <div style="
4924
+ font-size: 22px;
4925
+ line-height: 1.4;
4926
+ font-weight: 300;
4927
+ color: rgba(255, 255, 255, 0.75);
4928
+ letter-spacing: -0.015em;
4929
+ ">
4930
+ Valid for all platform products
4931
+ </div>
4932
+
4933
+ <!-- 购买按钮 -->
4934
+ <button
4935
+ data-package-button="${pkg.id}"
4936
+ style="
4937
+ margin-top: 0;
4938
+ width: auto;
4939
+ min-width: 280px;
4940
+ max-width: 400px;
4941
+ border-radius: 50px;
4942
+ border: none;
4943
+ background: linear-gradient(135deg, #00ff88 0%, #00f2fe 100%);
4944
+ padding: 16px 48px;
4945
+ font-size: 20px;
4946
+ font-weight: 600;
4947
+ color: #000000;
4948
+ cursor: pointer;
4949
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
4950
+ box-shadow: 0 4px 20px rgba(0, 255, 136, 0.35);
4951
+ letter-spacing: 0.01em;
4952
+ "
4953
+ onmouseover="this.style.background='linear-gradient(135deg, #00ff88 0%, #00f2fe 100%)'; this.style.boxShadow='0 6px 28px rgba(0, 255, 136, 0.5)'; this.style.transform='translateY(-2px) scale(1.02)';"
4954
+ onmouseout="this.style.background='linear-gradient(135deg, #00ff88 0%, #00f2fe 100%)'; this.style.boxShadow='0 4px 20px rgba(0, 255, 136, 0.35)'; this.style.transform='translateY(0) scale(1)';"
4955
+ >
4956
+ Buy $ ${pkg.price}
4957
+ </button>
4958
+ </div>
4959
+ `;
4960
+ }
4961
+ /**
4962
+ * 格式化数字(添加逗号)
4963
+ */
4964
+ formatNumber(num) {
4965
+ return parseInt(num).toLocaleString();
4966
+ }
4967
+ /**
4968
+ * 获取加载按钮的 HTML(带旋转动画)
4969
+ */
4970
+ getLoadingButtonHTML(text) {
4971
+ return `
4972
+ <svg style="
4973
+ display: inline-block;
4974
+ width: 16px;
4975
+ height: 16px;
4976
+ border: 2px solid rgba(255, 255, 255, 0.3);
4977
+ border-top-color: white;
4978
+ border-radius: 50%;
4979
+ animation: spin 0.6s linear infinite;
4980
+ " viewBox="0 0 24 24"></svg>
4981
+ <style>@keyframes spin { to { transform: rotate(360deg); } }</style>
4982
+ <span style="margin-left: 8px;">${text}</span>
4983
+ `;
4984
+ }
4985
+ /**
4986
+ * 添加事件监听
4987
+ */
4988
+ attachEventListeners(container) {
4989
+ // 为每个套餐按钮添加点击事件
4990
+ this.options.packages.forEach(pkg => {
4991
+ const button = container.querySelector(`[data-package-button="${pkg.id}"]`);
4992
+ if (button) {
4993
+ button.addEventListener('click', async (e) => {
4994
+ e.preventDefault();
4995
+ e.stopPropagation();
4996
+ console.log('[GenericPackageModal] Package selected:', pkg.id);
4997
+ // 保存原始按钮文本
4998
+ const originalText = button.innerHTML;
4999
+ const originalDisabled = button.disabled;
5000
+ try {
5001
+ // 禁用按钮,显示初始化状态
5002
+ button.disabled = true;
5003
+ const isZh = this.language === 'zh-CN';
5004
+ button.innerHTML = this.getLoadingButtonHTML(isZh ? '初始化中...' : 'Initializing...');
5005
+ // 等待 SDK 初始化(支持重试)
5006
+ const initialized = await this.waitForSDKInitialization(30000, 1);
5007
+ if (!initialized) {
5008
+ throw new Error('SDK initialization failed or timed out. Please try again.');
5009
+ }
5010
+ // SDK 初始化成功,执行支付流程
5011
+ await this.handlePaymentFlow(pkg, button, originalText);
5012
+ }
5013
+ catch (error) {
5014
+ console.error('[GenericPackageModal] Failed to process payment:', error);
5015
+ // 恢复按钮状态
5016
+ button.disabled = originalDisabled;
5017
+ button.innerHTML = originalText;
5018
+ // 显示错误提示(使用自定义 UI 替代 alert)
5019
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
5020
+ showErrorMessage(`Payment failed: ${errorMessage}`);
5021
+ // 触发失败回调
5022
+ this.options.onPaymentFailed?.(error instanceof Error ? error : new Error(String(error)), pkg);
5023
+ }
5024
+ });
5025
+ }
5026
+ });
5027
+ }
5028
+ /**
5029
+ * 处理支付流程
5030
+ */
5031
+ async handlePaymentFlow(pkg, button, originalHTML) {
5032
+ try {
5033
+ // 更新按钮状态为"创建订单中"
5034
+ button.innerHTML = this.getLoadingButtonHTML('Creating order...');
5035
+ console.log('[GenericPackageModal] Creating order for package:', pkg.id);
5036
+ // 调用回调创建订单,或使用默认实现
5037
+ let orderId;
5038
+ // 使用默认实现:调用解析后的 orderApiUrl
5039
+ const resolvedOrderApiUrl = this.options.sdkConfig._resolvedOrderApiUrl || ENVIRONMENT_CONFIGS[this.options.sdkConfig.environment].orderApiUrl;
5040
+ const response = await createOrder(resolvedOrderApiUrl, this.options.sdkConfig.accountToken || '', {
5041
+ product_id: pkg.id,
5042
+ purchase_type: this.options.sdkConfig.businessType ?? 1, // 默认为 1
5043
+ });
5044
+ console.log('[GenericPackageModal] Create order response:', response);
5045
+ if (!response || !response.transaction_id) {
5046
+ throw new Error('Failed to create order: Invalid response from API');
5047
+ }
5048
+ orderId = response.transaction_id;
5049
+ console.log('[GenericPackageModal] Order created:', orderId);
5050
+ if (!orderId) {
5051
+ throw new Error('Order ID not returned');
5052
+ }
5053
+ // 创建并打开支付弹框(按钮状态将在支付完成后处理)
5054
+ await this.openPaymentModal(orderId, pkg);
5055
+ // 支付弹框打开后,恢复按钮状态
5056
+ button.disabled = false;
5057
+ button.innerHTML = originalHTML;
5058
+ }
5059
+ catch (error) {
5060
+ console.error('[GenericPackageModal] Payment flow failed:', error);
5061
+ // 恢复按钮状态
5062
+ button.disabled = false;
5063
+ button.innerHTML = originalHTML;
5064
+ // 显示错误提示(使用自定义 UI 替代 alert)
5065
+ showErrorMessage(`Payment failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
5066
+ // 触发失败回调
5067
+ this.options.onPaymentFailed?.(error instanceof Error ? error : new Error(String(error)), pkg);
5068
+ }
5069
+ }
5070
+ /**
5071
+ * 打开支付弹框
5072
+ */
5073
+ async openPaymentModal(orderId, pkg) {
5074
+ if (!this.paymentMethod) {
5075
+ throw new Error('Payment method not configured');
5076
+ }
5077
+ const pkgName = pkg.name;
5078
+ // 创建支付实例(使用 orderId)
5079
+ if (!this.sdkInitialized) {
5080
+ throw new Error('Payment SDK not initialized. Please provide sdkConfig.');
5081
+ }
5082
+ const paymentInstance = window.SeaartPaymentComponent.createPayment({
5083
+ sys_order_id: orderId,
5084
+ account_token: this.accountToken,
5085
+ });
5086
+ const dropinModal = new DropinPaymentModal(paymentInstance, orderId, this.accountToken, this.paymentMethod, {
5087
+ modalTitle: `Purchase ${pkgName}`,
5088
+ onCompleted: (payload) => {
5089
+ console.log('[GenericPackageModal] Payment completed:', payload);
5090
+ this.close();
5091
+ // 显示购买成功弹窗
5092
+ const successModal = new PurchaseSuccessModal({
5093
+ data: {
5094
+ packName: pkg.name,
5095
+ credits: parseInt(pkg.credits),
5096
+ amount: pkg.price,
5097
+ currency: pkg.currency === 'USD' ? '$' : pkg.currency,
5098
+ orderId: orderId,
5099
+ transactionId: payload.transaction_id,
5100
+ },
5101
+ language: this.language,
5102
+ onClose: () => {
5103
+ // 弹窗关闭后刷新积分
5104
+ (async () => {
5105
+ try {
5106
+ const envConfig = ENVIRONMENT_CONFIGS[this.options.sdkConfig.environment];
5107
+ const walletApiUrl = envConfig.walletApiUrl;
5108
+ console.log('[GenericPackageModal] Refreshing credits from:', walletApiUrl);
5109
+ const creditDetail = await getCreditDetail(walletApiUrl, this.options.sdkConfig.accountToken);
5110
+ if (creditDetail) {
5111
+ console.log('[GenericPackageModal] Credits refreshed, total balance:', creditDetail.total_balance);
5112
+ }
5113
+ else {
5114
+ console.warn('[GenericPackageModal] Failed to refresh credits');
5115
+ }
5116
+ }
5117
+ catch (error) {
5118
+ console.error('[GenericPackageModal] Failed to refresh credits:', error);
5119
+ }
5120
+ })();
5121
+ // 触发用户回调
5122
+ this.options.onPaymentSuccess?.(orderId, payload.transaction_id, pkg);
5123
+ },
5124
+ });
5125
+ successModal.open();
5126
+ },
5127
+ onFailed: (payload) => {
5128
+ console.error('[GenericPackageModal] Payment failed:', payload);
5129
+ const error = new Error(payload.message || 'Payment failed');
5130
+ this.options.onPaymentFailed?.(error, pkg);
5131
+ },
5132
+ onError: (payload, error) => {
5133
+ console.error('[GenericPackageModal] Payment error:', error);
5134
+ this.options.onPaymentFailed?.(error, pkg);
5135
+ },
5136
+ });
5137
+ await dropinModal.open();
5138
+ }
5139
+ /**
5140
+ * 清理资源
5141
+ */
5142
+ cleanup() {
5143
+ console.log('[GenericPackageModal] Cleaning up...');
5144
+ // 移除resize监听器
5145
+ if (this.resizeHandler) {
5146
+ window.removeEventListener('resize', this.resizeHandler);
5147
+ this.resizeHandler = null;
5148
+ }
5149
+ }
5150
+ /**
5151
+ * 检查弹框是否打开
5152
+ */
5153
+ isOpen() {
5154
+ return this.modal.isModalOpen();
5155
+ }
5156
+ }
5157
+
1096
5158
  /**
1097
5159
  * @seaverse/payment-sdk
1098
5160
  *
@@ -1145,7 +5207,7 @@ function getSDKLocale(locale) {
1145
5207
  /**
1146
5208
  * SDK version
1147
5209
  */
1148
- const VERSION$2 = '0.7.0';
5210
+ const VERSION$2 = '0.8.0';
1149
5211
 
1150
5212
  var __defProp = Object.defineProperty;
1151
5213
  var __defProps = Object.defineProperties;
@@ -17705,29 +21767,63 @@ var iconYuebuzuBaakZaa_ = /*#__PURE__*/Object.freeze({
17705
21767
 
17706
21768
  exports.API_ENDPOINTS = API_ENDPOINTS$1;
17707
21769
  exports.BIZ_CODE = BIZ_CODE;
21770
+ exports.BindCardPaymentComponent = BindCardPaymentComponent;
17708
21771
  exports.COMPONENT_LOAD_TIMEOUT = COMPONENT_LOAD_TIMEOUT;
21772
+ exports.CREATIVE_POWER_TYPES = CREATIVE_POWER_TYPES;
21773
+ exports.CREDIT_PACKAGES = CREDIT_PACKAGES;
17709
21774
  exports.CheckoutAPI = CheckoutAPI;
21775
+ exports.CreditPackageModal = CreditPackageModal;
17710
21776
  exports.DEFAULT_CHECKOUT_CONFIG = DEFAULT_CHECKOUT_CONFIG;
21777
+ exports.DropinPaymentComponent = DropinPaymentComponent;
21778
+ exports.DropinPaymentModal = DropinPaymentModal;
21779
+ exports.ENVIRONMENT_CONFIGS = ENVIRONMENT_CONFIGS;
17711
21780
  exports.ENV_CONFIG = ENV_CONFIG;
21781
+ exports.ErrorHandler = ErrorHandler;
21782
+ exports.GRID_COLUMNS = GRID_COLUMNS;
21783
+ exports.GenericPackageModal = GenericPackageModal;
17712
21784
  exports.HTTP_STATUS = HTTP_STATUS;
21785
+ exports.LinkPaymentComponent = LinkPaymentComponent;
21786
+ exports.OrderPayment = OrderPayment;
17713
21787
  exports.PAYMENT_ELEMENT_NAME = PAYMENT_ELEMENT_NAME;
17714
21788
  exports.PaymentAPIError = PaymentAPIError;
17715
21789
  exports.PaymentCheckoutClient = PaymentCheckoutClient;
17716
21790
  exports.PaymentClient = PaymentClient;
21791
+ exports.PaymentError = PaymentError;
21792
+ exports.PaymentModal = PaymentModal;
21793
+ exports.PaymentStorage = PaymentStorage;
21794
+ exports.PurchaseSuccessModal = PurchaseSuccessModal;
21795
+ exports.RESPONSIVE_BREAKPOINTS = RESPONSIVE_BREAKPOINTS;
21796
+ exports.SDK_CONFIG = SDK_CONFIG;
21797
+ exports.ScriptLoader = ScriptLoader;
17717
21798
  exports.SeaArtPayLoader = SeaArtPayLoader;
21799
+ exports.SeaartPaymentSDK = SeaartPaymentSDK;
21800
+ exports.StylesheetLoader = StylesheetLoader;
17718
21801
  exports.VERSION = VERSION$2;
17719
21802
  exports.centsToDollars = centsToDollars;
21803
+ exports.changeSubscription = changeSubscription;
21804
+ exports.checkOrderStatus = checkOrderStatus;
17720
21805
  exports.createCheckoutPaymentError = createCheckoutPaymentError;
21806
+ exports.createOrder = createOrder;
17721
21807
  exports.delay = delay;
17722
21808
  exports.dollarsToCents = dollarsToCents;
17723
21809
  exports.formatPrice = formatPrice;
17724
21810
  exports.generateOrderReference = generateOrderReference;
21811
+ exports.getActiveSubscription = getActiveSubscription;
21812
+ exports.getCreditDetail = getCreditDetail;
21813
+ exports.getCurrentSubscription = getCurrentSubscription;
17725
21814
  exports.getCurrentUrl = getCurrentUrl;
17726
21815
  exports.getGlobalLoader = getGlobalLoader;
17727
21816
  exports.getSDKLocale = getSDKLocale;
21817
+ exports.hideLoadingIndicator = hideLoadingIndicator;
17728
21818
  exports.isBrowser = isBrowser;
17729
21819
  exports.isCheckoutPaymentError = isCheckoutPaymentError;
21820
+ exports.pollOrderStatus = pollOrderStatus;
17730
21821
  exports.resetGlobalLoader = resetGlobalLoader;
21822
+ exports.restartSubscription = restartSubscription;
17731
21823
  exports.safeJsonParse = safeJsonParse;
21824
+ exports.showErrorMessage = showErrorMessage;
21825
+ exports.showInfoMessage = showInfoMessage;
21826
+ exports.showLoadingIndicator = showLoadingIndicator;
21827
+ exports.showSuccessMessage = showSuccessMessage;
17732
21828
  exports.withTimeout = withTimeout;
17733
21829
  //# sourceMappingURL=index.cjs.map