@seaverse/payment-sdk 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -986,11 +986,33 @@ function centsToDollars(cents) {
986
986
  function formatPrice(amount, currency = 'USD') {
987
987
  const symbols = {
988
988
  USD: '$',
989
- CNY: '¥',
990
989
  EUR: '€',
991
990
  GBP: '£',
992
991
  JPY: '¥',
992
+ CNY: '¥',
993
993
  KRW: '₩',
994
+ AUD: 'A$',
995
+ CAD: 'C$',
996
+ CHF: 'CHF',
997
+ HKD: 'HK$',
998
+ SGD: 'S$',
999
+ INR: '₹',
1000
+ RUB: '₽',
1001
+ BRL: 'R$',
1002
+ MXN: 'MX$',
1003
+ ZAR: 'R',
1004
+ SEK: 'kr',
1005
+ NOK: 'kr',
1006
+ DKK: 'kr',
1007
+ PLN: 'zł',
1008
+ THB: '฿',
1009
+ IDR: 'Rp',
1010
+ MYR: 'RM',
1011
+ PHP: '₱',
1012
+ VND: '₫',
1013
+ TRY: '₺',
1014
+ AED: 'د.إ',
1015
+ SAR: 'SR',
994
1016
  };
995
1017
  const symbol = symbols[currency] || currency + ' ';
996
1018
  return `${symbol}${amount.toFixed(2)}`;
@@ -2143,222 +2165,771 @@ class PaymentModal {
2143
2165
  }
2144
2166
 
2145
2167
  /**
2146
- * DropinPaymentModal - Dropin 支付弹框
2147
- * 封装 DropinPaymentComponent 并在弹框中展示
2168
+ * RetentionModal - 支付挽留弹框
2169
+ * 当用户尝试取消支付时弹出,提供优惠信息以挽留用户
2170
+ * 参考图片中的深色卡片设计风格
2148
2171
  */
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
- });
2172
+ /**
2173
+ * 支付挽留弹框类
2174
+ */
2175
+ class RetentionModal {
2176
+ constructor(options) {
2177
+ this.overlay = null;
2178
+ this.modal = null;
2179
+ this.isExiting = false;
2180
+ this.scrollY = 0;
2181
+ this.boundHandleEscKey = null;
2182
+ this.countdownInterval = null;
2183
+ this.remainingSeconds = 60;
2184
+ this.options = {
2185
+ language: 'en',
2186
+ bonusAmount: 0,
2187
+ onClose: () => { },
2188
+ ...options,
2189
+ };
2174
2190
  }
2175
2191
  /**
2176
- * 打开弹框并渲染支付组件
2192
+ * 打开弹框
2177
2193
  */
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
- }
2194
+ open() {
2195
+ if (this.overlay)
2196
+ return; // 防止重复打开
2197
+ // 保存滚动位置并阻止 body 滚动
2198
+ this.scrollY = window.scrollY;
2199
+ document.body.style.position = 'fixed';
2200
+ document.body.style.top = `-${this.scrollY}px`;
2201
+ document.body.style.width = '100%';
2202
+ // 创建遮罩层和弹窗
2203
+ this.createModal();
2204
+ // 添加事件监听
2205
+ this.attachEventListeners();
2206
+ // 启动倒计时
2207
+ this.startCountdown();
2266
2208
  }
2267
2209
  /**
2268
2210
  * 关闭弹框
2269
2211
  */
2270
2212
  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;
2213
+ if (this.isExiting || !this.overlay)
2214
+ return;
2215
+ this.isExiting = true;
2216
+ // 停止倒计时
2217
+ this.stopCountdown();
2218
+ // 添加退出动画
2219
+ this.overlay.style.animation = 'fadeOut 0.3s ease-in';
2220
+ if (this.modal) {
2221
+ this.modal.style.animation = 'slideOutDown 0.3s ease-in';
2282
2222
  }
2283
- this.containerElement = null;
2223
+ // 300ms 后移除元素
2224
+ setTimeout(() => {
2225
+ this.cleanup();
2226
+ this.options.onClose?.();
2227
+ }, 300);
2284
2228
  }
2285
2229
  /**
2286
- * 显示加载状态
2230
+ * 创建弹框元素
2287
2231
  */
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;
2232
+ createModal() {
2233
+ const { language, productName, purchaseAmount, bonusAmount, discountPrice } = this.options;
2234
+ const isZh = language === 'zh-CN';
2235
+ const texts = isZh ? {
2236
+ title: '你有一笔订单待支付',
2237
+ timePrefix: '请在',
2238
+ timeSuffix: '内支付,超时自动取消',
2239
+ productLabel: '商品:',
2240
+ bonusLabel: '活动赠送:',
2241
+ discountLabel: '优惠价:',
2242
+ cancelButton: '取消',
2243
+ continueButton: '继续支付',
2244
+ } : {
2245
+ title: 'You have a pending order',
2246
+ timePrefix: 'Please pay within',
2247
+ timeSuffix: 'or the order will be automatically cancelled',
2248
+ productLabel: 'Product:',
2249
+ bonusLabel: 'Bonus:',
2250
+ discountLabel: 'Discount:',
2251
+ cancelButton: 'Cancel',
2252
+ continueButton: 'Continue Payment',
2253
+ };
2254
+ // 创建遮罩层
2255
+ this.overlay = document.createElement('div');
2256
+ this.overlay.id = 'retention-modal-overlay';
2257
+ this.overlay.style.cssText = `
2258
+ position: fixed;
2259
+ inset: 0;
2260
+ z-index: 10002;
2302
2261
  display: flex;
2303
2262
  align-items: center;
2304
2263
  justify-content: center;
2305
- background: rgba(0, 0, 0, 0.3);
2306
- backdrop-filter: blur(2px);
2307
- z-index: 10;
2264
+ padding: 20px;
2265
+ background: rgba(0, 0, 0, 0.85);
2266
+ backdrop-filter: blur(8px);
2267
+ animation: fadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
2308
2268
  `;
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>
2269
+ // 创建弹框卡片
2270
+ this.modal = document.createElement('div');
2271
+ this.modal.style.cssText = `
2272
+ position: relative;
2273
+ width: 90vw;
2274
+ max-width: 654px;
2275
+ border-radius: 24px;
2276
+ background: linear-gradient(180deg, #2d3748 0%, #1a202c 100%);
2277
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.8);
2278
+ animation: slideInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
2279
+ overflow: hidden;
2318
2280
  `;
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
- /**
2281
+ this.modal.innerHTML = `
2282
+ <!-- 关闭按钮 -->
2283
+ <button
2284
+ type="button"
2285
+ id="retention-modal-close-btn"
2286
+ style="
2287
+ position: absolute;
2288
+ top: 20px;
2289
+ right: 20px;
2290
+ z-index: 10;
2291
+ display: flex;
2292
+ width: 32px;
2293
+ height: 32px;
2294
+ align-items: center;
2295
+ justify-content: center;
2296
+ border-radius: 50%;
2297
+ background: rgba(255, 255, 255, 0.1);
2298
+ color: rgba(255, 255, 255, 0.7);
2299
+ border: none;
2300
+ cursor: pointer;
2301
+ transition: all 0.2s;
2302
+ "
2303
+ >
2304
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
2305
+ <path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
2306
+ </svg>
2307
+ </button>
2308
+
2309
+ <!-- 头部标题 -->
2310
+ <div style="
2311
+ padding: 40px 32px 24px;
2312
+ text-align: center;
2313
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
2314
+ ">
2315
+ <h2 style="
2316
+ margin: 0 0 16px;
2317
+ font-size: 24px;
2318
+ font-weight: 600;
2319
+ color: white;
2320
+ ">${texts.title}</h2>
2321
+
2322
+ <!-- 倒计时 -->
2323
+ <p style="
2324
+ margin: 0;
2325
+ font-size: 14px;
2326
+ color: rgba(255, 255, 255, 0.6);
2327
+ ">
2328
+ ${texts.timePrefix} <span id="countdown-display" style="color: #fc8181; font-weight: 600;">00:15:00</span> ${texts.timeSuffix}
2329
+ </p>
2330
+ </div>
2331
+
2332
+ <!-- 内容区域 -->
2333
+ <div style="padding: 32px;">
2334
+ <!-- 订单详情卡片 -->
2335
+ <div style="
2336
+ border-radius: 16px;
2337
+ border: 1px solid rgba(255, 255, 255, 0.1);
2338
+ background: rgba(0, 0, 0, 0.3);
2339
+ padding: 24px;
2340
+ ">
2341
+ <!-- 商品信息 -->
2342
+ <div style="
2343
+ display: flex;
2344
+ align-items: center;
2345
+ justify-content: space-between;
2346
+ padding-bottom: 16px;
2347
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
2348
+ margin-bottom: 16px;
2349
+ ">
2350
+ <span style="
2351
+ font-size: 14px;
2352
+ color: rgba(255, 255, 255, 0.6);
2353
+ ">${texts.productLabel}</span>
2354
+ <span style="
2355
+ font-size: 14px;
2356
+ font-weight: 600;
2357
+ color: white;
2358
+ ">${this.escapeHtml(productName)}</span>
2359
+ </div>
2360
+
2361
+ <!-- 活动赠送(可选) -->
2362
+ ${bonusAmount ? `
2363
+ <div style="
2364
+ display: flex;
2365
+ align-items: center;
2366
+ justify-content: space-between;
2367
+ padding-bottom: 16px;
2368
+ margin-bottom: 16px;
2369
+ ">
2370
+ <span style="
2371
+ font-size: 14px;
2372
+ color: rgba(255, 255, 255, 0.6);
2373
+ ">${texts.bonusLabel}</span>
2374
+ <span style="
2375
+ font-size: 14px;
2376
+ font-weight: 600;
2377
+ color: white;
2378
+ ">${bonusAmount.toLocaleString()}</span>
2379
+ </div>
2380
+ ` : ''}
2381
+
2382
+ <!-- 优惠价 -->
2383
+ <div style="
2384
+ display: flex;
2385
+ align-items: center;
2386
+ justify-content: space-between;
2387
+ background: linear-gradient(90deg, rgba(168, 85, 247, 0.1) 0%, transparent 100%);
2388
+ padding: 16px;
2389
+ margin: 0 -24px -24px;
2390
+ border-radius: 0 0 16px 16px;
2391
+ ">
2392
+ <span style="
2393
+ font-size: 14px;
2394
+ color: rgba(255, 255, 255, 0.6);
2395
+ ">${texts.discountLabel}</span>
2396
+ <span style="
2397
+ font-size: 18px;
2398
+ font-weight: 700;
2399
+ color: #a855f7;
2400
+ ">${this.escapeHtml(discountPrice)}</span>
2401
+ </div>
2402
+ </div>
2403
+ </div>
2404
+
2405
+ <!-- 底部按钮组 -->
2406
+ <div style="
2407
+ display: flex;
2408
+ gap: 12px;
2409
+ padding: 0 32px 32px;
2410
+ ">
2411
+ <!-- 取消按钮 -->
2412
+ <button
2413
+ type="button"
2414
+ id="retention-cancel-btn"
2415
+ style="
2416
+ flex: 1;
2417
+ padding: 14px 24px;
2418
+ border-radius: 12px;
2419
+ border: 1px solid rgba(255, 255, 255, 0.2);
2420
+ background: transparent;
2421
+ color: white;
2422
+ font-size: 16px;
2423
+ font-weight: 600;
2424
+ cursor: pointer;
2425
+ transition: all 0.2s;
2426
+ "
2427
+ >
2428
+ ${texts.cancelButton}
2429
+ </button>
2430
+
2431
+ <!-- 继续支付按钮 -->
2432
+ <button
2433
+ type="button"
2434
+ id="retention-continue-btn"
2435
+ style="
2436
+ flex: 1;
2437
+ padding: 14px 24px;
2438
+ border-radius: 12px;
2439
+ border: none;
2440
+ background: linear-gradient(135deg, #00ff88 0%, #00f2fe 100%);
2441
+ color: white;
2442
+ font-size: 16px;
2443
+ font-weight: 700;
2444
+ cursor: pointer;
2445
+ transition: all 0.2s;
2446
+ box-shadow: 0 4px 16px rgba(0, 255, 136, 0.4);
2447
+ "
2448
+ >
2449
+ ${texts.continueButton}
2450
+ </button>
2451
+ </div>
2452
+ `;
2453
+ this.overlay.appendChild(this.modal);
2454
+ document.body.appendChild(this.overlay);
2455
+ // 添加 CSS 动画
2456
+ this.addStyles();
2457
+ }
2458
+ /**
2459
+ * 添加 CSS 动画
2460
+ */
2461
+ addStyles() {
2462
+ // 检查是否已添加样式
2463
+ if (document.getElementById('retention-modal-styles'))
2464
+ return;
2465
+ const style = document.createElement('style');
2466
+ style.id = 'retention-modal-styles';
2467
+ style.textContent = `
2468
+ @keyframes fadeIn {
2469
+ from { opacity: 0; }
2470
+ to { opacity: 1; }
2471
+ }
2472
+
2473
+ @keyframes fadeOut {
2474
+ from { opacity: 1; }
2475
+ to { opacity: 0; }
2476
+ }
2477
+
2478
+ @keyframes slideInUp {
2479
+ from {
2480
+ opacity: 0;
2481
+ transform: translateY(16px);
2482
+ }
2483
+ to {
2484
+ opacity: 1;
2485
+ transform: translateY(0);
2486
+ }
2487
+ }
2488
+
2489
+ @keyframes slideOutDown {
2490
+ from {
2491
+ opacity: 1;
2492
+ transform: translateY(0);
2493
+ }
2494
+ to {
2495
+ opacity: 0;
2496
+ transform: translateY(16px);
2497
+ }
2498
+ }
2499
+
2500
+ #retention-modal-close-btn:hover {
2501
+ background: rgba(255, 255, 255, 0.2) !important;
2502
+ color: white !important;
2503
+ }
2504
+
2505
+ #retention-cancel-btn:hover {
2506
+ background: rgba(255, 255, 255, 0.1) !important;
2507
+ border-color: rgba(255, 255, 255, 0.3) !important;
2508
+ }
2509
+
2510
+ #retention-continue-btn:hover {
2511
+ box-shadow: 0 6px 20px rgba(0, 255, 136, 0.6) !important;
2512
+ transform: translateY(-1px);
2513
+ }
2514
+ `;
2515
+ document.head.appendChild(style);
2516
+ }
2517
+ /**
2518
+ * 添加事件监听
2519
+ */
2520
+ attachEventListeners() {
2521
+ if (!this.overlay)
2522
+ return;
2523
+ // 点击遮罩不关闭(挽留弹框需要强制选择)
2524
+ // this.overlay.addEventListener('click', (e) => {
2525
+ // if (e.target === this.overlay) {
2526
+ // this.close();
2527
+ // }
2528
+ // });
2529
+ // 点击关闭按钮
2530
+ const closeBtn = document.getElementById('retention-modal-close-btn');
2531
+ if (closeBtn) {
2532
+ closeBtn.addEventListener('click', () => {
2533
+ this.options.onCancel();
2534
+ this.close();
2535
+ });
2536
+ }
2537
+ // 点击取消按钮
2538
+ const cancelBtn = document.getElementById('retention-cancel-btn');
2539
+ if (cancelBtn) {
2540
+ cancelBtn.addEventListener('click', () => {
2541
+ this.options.onCancel();
2542
+ this.close();
2543
+ });
2544
+ }
2545
+ // 点击继续支付按钮
2546
+ const continueBtn = document.getElementById('retention-continue-btn');
2547
+ if (continueBtn) {
2548
+ continueBtn.addEventListener('click', () => {
2549
+ this.options.onContinue();
2550
+ this.close();
2551
+ });
2552
+ }
2553
+ // ESC 键关闭
2554
+ this.boundHandleEscKey = this.handleEscKey.bind(this);
2555
+ document.addEventListener('keydown', this.boundHandleEscKey);
2556
+ }
2557
+ /**
2558
+ * 处理 ESC 键
2559
+ */
2560
+ handleEscKey(e) {
2561
+ if (e.key === 'Escape') {
2562
+ this.options.onCancel();
2563
+ this.close();
2564
+ }
2565
+ }
2566
+ /**
2567
+ * 启动倒计时
2568
+ */
2569
+ startCountdown() {
2570
+ this.remainingSeconds = 15 * 60; // 从15分钟开始倒计时
2571
+ this.updateCountdownDisplay();
2572
+ this.countdownInterval = window.setInterval(() => {
2573
+ this.remainingSeconds--;
2574
+ this.updateCountdownDisplay();
2575
+ if (this.remainingSeconds <= 0) {
2576
+ this.stopCountdown();
2577
+ // 倒计时结束,自动取消订单
2578
+ this.options.onCancel();
2579
+ this.close();
2580
+ }
2581
+ }, 1000);
2582
+ }
2583
+ /**
2584
+ * 停止倒计时
2585
+ */
2586
+ stopCountdown() {
2587
+ if (this.countdownInterval) {
2588
+ clearInterval(this.countdownInterval);
2589
+ this.countdownInterval = null;
2590
+ }
2591
+ }
2592
+ /**
2593
+ * 更新倒计时显示
2594
+ */
2595
+ updateCountdownDisplay() {
2596
+ const display = document.getElementById('countdown-display');
2597
+ if (!display)
2598
+ return;
2599
+ const hours = Math.floor(this.remainingSeconds / 3600);
2600
+ const minutes = Math.floor((this.remainingSeconds % 3600) / 60);
2601
+ const seconds = this.remainingSeconds % 60;
2602
+ display.textContent = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
2603
+ }
2604
+ /**
2605
+ * 清理资源
2606
+ */
2607
+ cleanup() {
2608
+ // 停止倒计时
2609
+ this.stopCountdown();
2610
+ // 移除事件监听
2611
+ if (this.boundHandleEscKey) {
2612
+ document.removeEventListener('keydown', this.boundHandleEscKey);
2613
+ this.boundHandleEscKey = null;
2614
+ }
2615
+ // 移除元素
2616
+ if (this.overlay && this.overlay.parentNode) {
2617
+ this.overlay.parentNode.removeChild(this.overlay);
2618
+ }
2619
+ // 恢复 body 滚动
2620
+ document.body.style.position = '';
2621
+ document.body.style.top = '';
2622
+ document.body.style.width = '';
2623
+ window.scrollTo(0, this.scrollY);
2624
+ // 清空引用
2625
+ this.overlay = null;
2626
+ this.modal = null;
2627
+ this.isExiting = false;
2628
+ }
2629
+ /**
2630
+ * HTML 转义工具函数
2631
+ * 防止 XSS 攻击
2632
+ */
2633
+ escapeHtml(text) {
2634
+ const div = document.createElement('div');
2635
+ div.textContent = text;
2636
+ return div.innerHTML;
2637
+ }
2638
+ /**
2639
+ * 检查弹框是否打开
2640
+ */
2641
+ isOpen() {
2642
+ return this.overlay !== null && !this.isExiting;
2643
+ }
2644
+ }
2645
+
2646
+ /**
2647
+ * DropinPaymentModal - Dropin 支付弹框
2648
+ * 封装 DropinPaymentComponent 并在弹框中展示
2649
+ */
2650
+ class DropinPaymentModal {
2651
+ constructor(paymentInstance, orderId, accountToken, paymentMethod, options) {
2652
+ this.paymentInstance = paymentInstance;
2653
+ this.orderId = orderId;
2654
+ this.accountToken = accountToken;
2655
+ this.paymentMethod = paymentMethod;
2656
+ this.options = options;
2657
+ this.dropinPayment = null;
2658
+ this.containerElement = null;
2659
+ this.retentionModal = null;
2660
+ this.shouldShowRetention = true; // 控制是否显示挽留弹框
2661
+ this.paymentCompleted = false; // 标记支付是否已完成
2662
+ // 创建弹框
2663
+ this.modal = new PaymentModal({
2664
+ title: options.modalTitle ?? `Pay with ${paymentMethod.payment_method_name}`,
2665
+ showCloseButton: true,
2666
+ closeOnOverlayClick: false, // Dropin 支付中不允许点击遮罩关闭
2667
+ closeOnEsc: false, // 禁用 ESC 键关闭,改为触发挽留弹框
2668
+ maxWidth: '600px',
2669
+ onClose: () => {
2670
+ // 当弹框真正关闭时触发挽留弹框
2671
+ if (this.shouldShowRetention && this.options.enableRetention !== false && !this.paymentCompleted) {
2672
+ this.showRetentionModal();
2673
+ }
2674
+ else {
2675
+ this.cleanup();
2676
+ }
2677
+ },
2678
+ ...options.modalOptions,
2679
+ });
2680
+ console.log('[DropinPaymentModal] Created:', {
2681
+ orderId: this.orderId,
2682
+ paymentMethod: this.paymentMethod.payment_method_name,
2683
+ });
2684
+ }
2685
+ /**
2686
+ * 打开弹框并渲染支付组件
2687
+ */
2688
+ async open() {
2689
+ console.log('[DropinPaymentModal] Opening modal...');
2690
+ // 打开弹框
2691
+ this.modal.open();
2692
+ // 创建容器元素
2693
+ this.containerElement = document.createElement('div');
2694
+ this.containerElement.id = 'dropin-payment-container';
2695
+ this.containerElement.style.cssText = `
2696
+ min-height: 500px;
2697
+ width: 100%;
2698
+ `;
2699
+ // 设置弹框内容 - 必须先添加到 DOM,再创建 Dropin 组件
2700
+ const contentContainer = this.modal.getContentContainer();
2701
+ if (!contentContainer) {
2702
+ throw new Error('Modal content container not found');
2703
+ }
2704
+ contentContainer.appendChild(this.containerElement);
2705
+ // 验证订单信息(关键:确保后端订单已创建)
2706
+ try {
2707
+ console.log('[DropinPaymentModal] Validating order info...');
2708
+ const response = await this.paymentInstance.getOrderInfo();
2709
+ if (response.err || !response.data?.order_info) {
2710
+ throw new Error(response.message || 'Order information not found');
2711
+ }
2712
+ const orderInfo = response.data.order_info;
2713
+ if (!orderInfo.sys_order_id) {
2714
+ throw new Error('Order ID (sys_order_id) is missing');
2715
+ }
2716
+ console.log('[DropinPaymentModal] Order validated:', {
2717
+ orderInfo,
2718
+ orderId: orderInfo.sys_order_id,
2719
+ status: orderInfo.order_status,
2720
+ });
2721
+ }
2722
+ catch (error) {
2723
+ console.error('[DropinPaymentModal] Order validation failed:', error);
2724
+ this.close();
2725
+ throw new Error(`Order validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
2726
+ }
2727
+ // 创建并渲染 Dropin 支付组件
2728
+ try {
2729
+ this.dropinPayment = new DropinPaymentComponent({
2730
+ paymentInstance: this.paymentInstance,
2731
+ orderId: this.orderId,
2732
+ accountToken: this.accountToken,
2733
+ paymentMethod: this.paymentMethod,
2734
+ containerId: '#dropin-payment-container',
2735
+ onSubmit: (payload) => {
2736
+ console.log('[DropinPaymentModal] onSubmit:', payload);
2737
+ this.options.onSubmit?.(payload);
2738
+ },
2739
+ onError: (payload, error) => {
2740
+ console.error('[DropinPaymentModal] onError:', error);
2741
+ this.options.onError?.(payload, error);
2742
+ },
2743
+ onCreateOrder: (payload) => {
2744
+ console.log('[DropinPaymentModal] onCreateOrder:', payload);
2745
+ this.options.onCreateOrder?.(payload);
2746
+ },
2747
+ onCompleted: (payload) => {
2748
+ console.log('[DropinPaymentModal] onCompleted:', payload);
2749
+ this.paymentCompleted = true; // 标记支付已完成
2750
+ this.shouldShowRetention = false; // 支付完成后不显示挽留弹框
2751
+ this.options.onCompleted?.(payload);
2752
+ // 支付成功后自动关闭弹框(可选)
2753
+ setTimeout(() => this.close(), 2000);
2754
+ },
2755
+ onFailed: (payload) => {
2756
+ console.error('[DropinPaymentModal] onFailed:', payload);
2757
+ this.options.onFailed?.(payload);
2758
+ },
2759
+ onLoading: (loading) => {
2760
+ this.options.onLoading?.(loading);
2761
+ // 可以在这里显示加载状态
2762
+ if (loading) {
2763
+ this.showLoading();
2764
+ }
2765
+ else {
2766
+ this.hideLoading();
2767
+ }
2768
+ },
2769
+ });
2770
+ await this.dropinPayment.render();
2771
+ console.log('[DropinPaymentModal] Dropin component rendered');
2772
+ }
2773
+ catch (error) {
2774
+ console.error('[DropinPaymentModal] Failed to render dropin:', error);
2775
+ this.close();
2776
+ throw error;
2777
+ }
2778
+ }
2779
+ /**
2780
+ * 关闭弹框
2781
+ */
2782
+ close() {
2783
+ console.log('[DropinPaymentModal] Closing modal...');
2784
+ // 关闭挽留弹框(如果存在)
2785
+ if (this.retentionModal && this.retentionModal.isOpen()) {
2786
+ this.retentionModal.close();
2787
+ }
2788
+ this.modal.close();
2789
+ }
2790
+ /**
2791
+ * 清理资源
2792
+ */
2793
+ cleanup() {
2794
+ console.log('[DropinPaymentModal] Cleaning up...');
2795
+ // 清理 Dropin 支付组件
2796
+ if (this.dropinPayment) {
2797
+ this.dropinPayment.destroy();
2798
+ this.dropinPayment = null;
2799
+ }
2800
+ // 清理挽留弹框
2801
+ if (this.retentionModal) {
2802
+ if (this.retentionModal.isOpen()) {
2803
+ this.retentionModal.close();
2804
+ }
2805
+ this.retentionModal = null;
2806
+ }
2807
+ this.containerElement = null;
2808
+ this.shouldShowRetention = true; // 重置状态
2809
+ this.paymentCompleted = false; // 重置状态
2810
+ }
2811
+ /**
2812
+ * 显示加载状态
2813
+ */
2814
+ showLoading() {
2815
+ if (!this.containerElement)
2816
+ return;
2817
+ const loadingEl = document.getElementById('dropin-loading');
2818
+ if (loadingEl)
2819
+ return; // 已经存在
2820
+ const loading = document.createElement('div');
2821
+ loading.id = 'dropin-loading';
2822
+ loading.style.cssText = `
2823
+ position: absolute;
2824
+ top: 0;
2825
+ left: 0;
2826
+ right: 0;
2827
+ bottom: 0;
2828
+ display: flex;
2829
+ align-items: center;
2830
+ justify-content: center;
2831
+ background: rgba(0, 0, 0, 0.3);
2832
+ backdrop-filter: blur(2px);
2833
+ z-index: 10;
2834
+ `;
2835
+ loading.innerHTML = `
2836
+ <div style="
2837
+ width: 40px;
2838
+ height: 40px;
2839
+ border: 3px solid rgba(255, 255, 255, 0.2);
2840
+ border-top-color: #ffffff;
2841
+ border-radius: 50%;
2842
+ animation: spin 0.8s linear infinite;
2843
+ "></div>
2844
+ `;
2845
+ // 添加旋转动画
2846
+ if (!document.getElementById('dropin-loading-styles')) {
2847
+ const style = document.createElement('style');
2848
+ style.id = 'dropin-loading-styles';
2849
+ style.textContent = `
2850
+ @keyframes spin {
2851
+ to { transform: rotate(360deg); }
2852
+ }
2853
+ `;
2854
+ document.head.appendChild(style);
2855
+ }
2856
+ this.containerElement.style.position = 'relative';
2857
+ this.containerElement.appendChild(loading);
2858
+ }
2859
+ /**
2860
+ * 隐藏加载状态
2861
+ */
2862
+ hideLoading() {
2863
+ const loadingEl = document.getElementById('dropin-loading');
2864
+ if (loadingEl && loadingEl.parentNode) {
2865
+ loadingEl.parentNode.removeChild(loadingEl);
2866
+ }
2867
+ }
2868
+ /**
2869
+ * 显示挽留弹框
2870
+ */
2871
+ async showRetentionModal() {
2872
+ console.log('[DropinPaymentModal] Showing retention modal...');
2873
+ try {
2874
+ // 获取订单信息用于挽留弹框展示
2875
+ const response = await this.paymentInstance.getOrderInfo();
2876
+ if (response.err || !response.data?.order_info) {
2877
+ console.warn('[DropinPaymentModal] Failed to get order info for retention modal');
2878
+ this.cleanup();
2879
+ return;
2880
+ }
2881
+ const orderInfo = response.data.order_info;
2882
+ // 创建挽留弹框
2883
+ this.retentionModal = new RetentionModal({
2884
+ language: this.options.retentionOptions?.language ?? 'en',
2885
+ productName: orderInfo.product_name || this.paymentMethod.payment_method_name,
2886
+ purchaseAmount: Number(orderInfo.quantity) || 0,
2887
+ bonusAmount: this.options.retentionOptions?.bonusAmount,
2888
+ discountPrice: formatPrice(orderInfo.price || orderInfo.mark_price || 0, orderInfo.currency || 'USD'),
2889
+ onContinue: () => {
2890
+ // 继续支付:重新打开支付弹框
2891
+ console.log('[DropinPaymentModal] User chose to continue payment');
2892
+ this.shouldShowRetention = false; // 防止再次触发挽留弹框
2893
+ this.open(); // 重新打开支付弹框
2894
+ },
2895
+ onCancel: () => {
2896
+ // 确认取消:关闭所有弹框
2897
+ console.log('[DropinPaymentModal] User confirmed cancellation');
2898
+ this.cleanup();
2899
+ },
2900
+ onClose: () => {
2901
+ // 挽留弹框关闭时清理
2902
+ this.retentionModal = null;
2903
+ },
2904
+ ...this.options.retentionOptions,
2905
+ });
2906
+ this.retentionModal.open();
2907
+ }
2908
+ catch (error) {
2909
+ console.error('[DropinPaymentModal] Failed to show retention modal:', error);
2910
+ this.cleanup();
2911
+ }
2912
+ }
2913
+ /**
2914
+ * 检查弹框是否打开
2915
+ */
2916
+ isOpen() {
2917
+ return this.modal.isModalOpen();
2918
+ }
2919
+ }
2920
+
2921
+ /**
2922
+ * SeaartPaymentSDK
2923
+ * 基于 SeaartPaymentComponent 的支付 SDK 封装
2924
+ *
2925
+ * 核心职责:
2926
+ * 1. 动态加载 SeaartPaymentComponent 脚本
2927
+ * 2. 全局初始化(仅一次)
2928
+ * 3. 获取支付方式列表
2929
+ * 4. 创建订单支付实例
2930
+ */
2931
+ class SeaartPaymentSDK {
2932
+ /**
2362
2933
  * 私有构造函数(单例模式)
2363
2934
  */
2364
2935
  constructor() {
@@ -3581,71 +4152,423 @@ class PurchaseSuccessModal {
3581
4152
  document.head.appendChild(style);
3582
4153
  }
3583
4154
  /**
3584
- * 添加事件监听
4155
+ * 添加事件监听
4156
+ */
4157
+ attachEventListeners() {
4158
+ if (!this.overlay)
4159
+ return;
4160
+ // 点击遮罩关闭
4161
+ this.overlay.addEventListener('click', (e) => {
4162
+ if (e.target === this.overlay) {
4163
+ this.close();
4164
+ }
4165
+ });
4166
+ // 点击关闭按钮
4167
+ const closeBtn = document.getElementById('success-modal-close-btn');
4168
+ if (closeBtn) {
4169
+ closeBtn.addEventListener('click', () => this.close());
4170
+ }
4171
+ // ESC 键关闭
4172
+ this.boundHandleEscKey = this.handleEscKey.bind(this);
4173
+ document.addEventListener('keydown', this.boundHandleEscKey);
4174
+ }
4175
+ /**
4176
+ * 处理 ESC 键
4177
+ */
4178
+ handleEscKey(e) {
4179
+ if (e.key === 'Escape') {
4180
+ this.close();
4181
+ }
4182
+ }
4183
+ /**
4184
+ * 清理资源
4185
+ */
4186
+ cleanup() {
4187
+ // 移除事件监听
4188
+ if (this.boundHandleEscKey) {
4189
+ document.removeEventListener('keydown', this.boundHandleEscKey);
4190
+ this.boundHandleEscKey = null;
4191
+ }
4192
+ // 移除元素
4193
+ if (this.overlay && this.overlay.parentNode) {
4194
+ this.overlay.parentNode.removeChild(this.overlay);
4195
+ }
4196
+ // 恢复 body 滚动
4197
+ document.body.style.position = '';
4198
+ document.body.style.top = '';
4199
+ document.body.style.width = '';
4200
+ window.scrollTo(0, this.scrollY);
4201
+ // 清空引用
4202
+ this.overlay = null;
4203
+ this.modal = null;
4204
+ this.isExiting = false;
4205
+ }
4206
+ /**
4207
+ * HTML 转义工具函数
4208
+ * 防止 XSS 攻击
4209
+ */
4210
+ escapeHtml(text) {
4211
+ const div = document.createElement('div');
4212
+ div.textContent = text;
4213
+ return div.innerHTML;
4214
+ }
4215
+ /**
4216
+ * 检查弹窗是否打开
4217
+ */
4218
+ isOpen() {
4219
+ return this.overlay !== null && !this.isExiting;
4220
+ }
4221
+ }
4222
+
4223
+ /**
4224
+ * BasePackageModal - Abstract base class for package modals
4225
+ * Provides shared logic for SDK initialization, payment flow, and event handling
4226
+ */
4227
+ /**
4228
+ * Abstract base class for package modals
4229
+ * Provides common functionality for SDK initialization, payment flow, and event handling
4230
+ */
4231
+ class BasePackageModal {
4232
+ // === Constructor ===
4233
+ constructor(options) {
4234
+ this.resizeHandler = null;
4235
+ this.isInitializingSDK = false;
4236
+ this.sdkInitialized = false;
4237
+ this.options = options;
4238
+ this.language = options.language || 'en';
4239
+ this.modal = this.createModal();
4240
+ console.log(`[${this.constructor.name}] Created`);
4241
+ }
4242
+ // === Public Methods ===
4243
+ /**
4244
+ * Open the modal
4245
+ */
4246
+ async open() {
4247
+ console.log(`[${this.constructor.name}] Opening modal...`);
4248
+ this.modal.open();
4249
+ // Hook for subclass-specific styling
4250
+ this.applyModalStyling();
4251
+ // Render content (abstract method)
4252
+ this.renderContent();
4253
+ // Add resize listener
4254
+ this.resizeHandler = () => this.renderContent();
4255
+ window.addEventListener('resize', this.resizeHandler);
4256
+ // Initialize SDK in background
4257
+ if (!this.sdkInitialized && !this.isInitializingSDK) {
4258
+ this.initializeSDK();
4259
+ }
4260
+ console.log(`[${this.constructor.name}] Modal opened`);
4261
+ }
4262
+ /**
4263
+ * Close the modal
4264
+ */
4265
+ close() {
4266
+ console.log(`[${this.constructor.name}] Closing modal...`);
4267
+ this.modal.close();
4268
+ }
4269
+ /**
4270
+ * Check if modal is open
4271
+ */
4272
+ isOpen() {
4273
+ return this.modal.isModalOpen();
4274
+ }
4275
+ // === Protected Hook Methods (can be overridden by subclasses) ===
4276
+ /**
4277
+ * Apply modal styling (hook method)
4278
+ * Subclasses can override to customize modal appearance
4279
+ */
4280
+ applyModalStyling() {
4281
+ // Default: no custom styling
4282
+ }
4283
+ // === Protected Shared Methods ===
4284
+ /**
4285
+ * Initialize payment SDK (identical in both classes)
4286
+ */
4287
+ async initializeSDK() {
4288
+ if (this.isInitializingSDK || this.sdkInitialized) {
4289
+ return;
4290
+ }
4291
+ if (!this.options.sdkConfig) {
4292
+ console.log(`[${this.constructor.name}] No SDK configuration provided, skipping initialization`);
4293
+ return;
4294
+ }
4295
+ this.isInitializingSDK = true;
4296
+ console.log(`[${this.constructor.name}] Initializing payment SDK...`);
4297
+ // Show loading indicator
4298
+ const loader = showLoadingIndicator('Initializing payment system...');
4299
+ try {
4300
+ const config = this.options.sdkConfig;
4301
+ // 1. Get base configuration from environment
4302
+ const envConfig = ENVIRONMENT_CONFIGS[config.environment];
4303
+ // 2. Merge configuration (custom config has higher priority)
4304
+ const finalConfig = {
4305
+ scriptUrl: config.scriptUrl || envConfig.scriptUrl,
4306
+ clientId: config.clientId || envConfig.clientId,
4307
+ orderApiUrl: config.orderApiUrl || envConfig.orderApiUrl,
4308
+ cssUrl: config.cssUrl || envConfig.cssUrl,
4309
+ };
4310
+ console.log(`[${this.constructor.name}] Using environment:`, config.environment);
4311
+ // 3. Initialize SeaartPaymentSDK
4312
+ await SeaartPaymentSDK.getInstance().init({
4313
+ scriptUrl: finalConfig.scriptUrl,
4314
+ clientId: finalConfig.clientId,
4315
+ language: 'en',
4316
+ scriptTimeout: config.scriptTimeout,
4317
+ cssUrl: finalConfig.cssUrl,
4318
+ });
4319
+ // 4. Get payment methods list
4320
+ const paymentMethods = await SeaartPaymentSDK.getInstance().getPaymentMethods({
4321
+ country_code: config.countryCode,
4322
+ business_type: config.businessType ?? 1, // Default to 1 (one-time purchase)
4323
+ });
4324
+ // 5. Find matching payment method
4325
+ const paymentMethod = config.paymentMethodType
4326
+ ? paymentMethods.find((m) => m.payment_method_type === config.paymentMethodType ||
4327
+ m.payment_method_name.toLowerCase().includes(config.paymentMethodType.toLowerCase()))
4328
+ : paymentMethods.find((m) => m.payment_type === 2); // Default to dropin (payment_type === 2)
4329
+ if (!paymentMethod) {
4330
+ throw new Error(`Payment method "${config.paymentMethodType || 'dropin'}" not found`);
4331
+ }
4332
+ // 6. Store to class members (including finalConfig for later use)
4333
+ this.paymentMethod = paymentMethod;
4334
+ this.accountToken = config.accountToken;
4335
+ this.sdkInitialized = true;
4336
+ // Store final config to config object (for handlePaymentFlow)
4337
+ this.options.sdkConfig._resolvedOrderApiUrl = finalConfig.orderApiUrl;
4338
+ console.log(`[${this.constructor.name}] SDK initialized with environment config:`, {
4339
+ environment: config.environment,
4340
+ paymentMethod: paymentMethod.payment_method_name,
4341
+ accountToken: config.accountToken ? 'provided' : 'not provided',
4342
+ });
4343
+ }
4344
+ catch (error) {
4345
+ console.error(`[${this.constructor.name}] Failed to initialize payment SDK:`, error);
4346
+ // SDK initialization failure does not affect browsing packages, just cannot make payments
4347
+ }
4348
+ finally {
4349
+ this.isInitializingSDK = false;
4350
+ // Hide loading indicator
4351
+ hideLoadingIndicator(loader);
4352
+ }
4353
+ }
4354
+ /**
4355
+ * Wait for SDK initialization with timeout and retry
4356
+ * @param timeout Timeout in milliseconds (default 30 seconds)
4357
+ * @param maxRetries Maximum retry count (default 1)
4358
+ * @returns Whether initialization succeeded
4359
+ */
4360
+ async waitForSDKInitialization(timeout = 30000, maxRetries = 1) {
4361
+ const startTime = Date.now();
4362
+ // If already initialized, return immediately
4363
+ if (this.sdkInitialized) {
4364
+ console.log(`[${this.constructor.name}] SDK already initialized`);
4365
+ return true;
4366
+ }
4367
+ // If not started initializing and has config, trigger initialization
4368
+ if (!this.isInitializingSDK && this.options.sdkConfig) {
4369
+ console.log(`[${this.constructor.name}] Starting SDK initialization...`);
4370
+ await this.initializeSDK();
4371
+ if (this.sdkInitialized) {
4372
+ return true;
4373
+ }
4374
+ }
4375
+ // Wait for initialization to complete
4376
+ console.log(`[${this.constructor.name}] Waiting for SDK initialization...`);
4377
+ while (Date.now() - startTime < timeout) {
4378
+ if (this.sdkInitialized) {
4379
+ console.log(`[${this.constructor.name}] SDK initialization completed`);
4380
+ return true;
4381
+ }
4382
+ if (!this.isInitializingSDK) {
4383
+ // Initialization ended but not successful, try retry
4384
+ if (maxRetries > 0) {
4385
+ console.log(`[${this.constructor.name}] SDK initialization failed, retrying... (${maxRetries} retries left)`);
4386
+ await this.initializeSDK();
4387
+ return this.waitForSDKInitialization(timeout - (Date.now() - startTime), maxRetries - 1);
4388
+ }
4389
+ else {
4390
+ console.error(`[${this.constructor.name}] SDK initialization failed after all retries`);
4391
+ return false;
4392
+ }
4393
+ }
4394
+ // Wait 100ms before retry
4395
+ await new Promise(resolve => setTimeout(resolve, 100));
4396
+ }
4397
+ // Timeout
4398
+ console.error(`[${this.constructor.name}] SDK initialization timed out`);
4399
+ return false;
4400
+ }
4401
+ /**
4402
+ * Handle payment flow (order creation + payment modal)
4403
+ */
4404
+ async handlePaymentFlow(pkg, button, originalHTML) {
4405
+ try {
4406
+ // Update button state to "Creating order"
4407
+ button.innerHTML = this.getLoadingButtonHTML('Creating order...');
4408
+ console.log(`[${this.constructor.name}] Creating order for package:`, pkg.id);
4409
+ // Use default implementation: call resolved orderApiUrl
4410
+ const resolvedOrderApiUrl = this.options.sdkConfig._resolvedOrderApiUrl || ENVIRONMENT_CONFIGS[this.options.sdkConfig.environment].orderApiUrl;
4411
+ const response = await createOrder(resolvedOrderApiUrl, this.options.sdkConfig.accountToken || '', {
4412
+ product_id: pkg.id,
4413
+ purchase_type: this.options.sdkConfig.businessType ?? 1, // Default to 1
4414
+ });
4415
+ console.log(`[${this.constructor.name}] Create order response:`, response);
4416
+ if (!response || !response.transaction_id) {
4417
+ throw new Error('Failed to create order: Invalid response from API');
4418
+ }
4419
+ const orderId = response.transaction_id;
4420
+ console.log(`[${this.constructor.name}] Order created:`, orderId);
4421
+ // Restore button state
4422
+ button.disabled = false;
4423
+ button.innerHTML = originalHTML;
4424
+ // Create and open payment modal
4425
+ await this.openPaymentModal(orderId, pkg);
4426
+ }
4427
+ catch (error) {
4428
+ console.error(`[${this.constructor.name}] Payment flow failed:`, error);
4429
+ // Restore button state
4430
+ button.disabled = false;
4431
+ button.innerHTML = originalHTML;
4432
+ // Show error message (use custom UI instead of alert)
4433
+ showErrorMessage(`Payment failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
4434
+ // Trigger failure callback
4435
+ this.options.onPaymentFailed?.(error instanceof Error ? error : new Error(String(error)), pkg);
4436
+ }
4437
+ }
4438
+ /**
4439
+ * Open payment modal (DropinPaymentModal + PurchaseSuccessModal)
4440
+ */
4441
+ async openPaymentModal(orderId, pkg) {
4442
+ if (!this.paymentMethod) {
4443
+ throw new Error('Payment method not configured');
4444
+ }
4445
+ const pkgName = this.getPackageDisplayName(pkg);
4446
+ // Create payment instance (using orderId)
4447
+ if (!this.sdkInitialized) {
4448
+ throw new Error('Payment SDK not initialized. Please provide sdkConfig.');
4449
+ }
4450
+ const paymentInstance = window.SeaartPaymentComponent.createPayment({
4451
+ sys_order_id: orderId,
4452
+ account_token: this.accountToken,
4453
+ });
4454
+ const dropinModal = new DropinPaymentModal(paymentInstance, orderId, this.accountToken, this.paymentMethod, {
4455
+ modalTitle: `Purchase ${pkgName}`,
4456
+ onCompleted: (payload) => {
4457
+ console.log(`[${this.constructor.name}] Payment completed:`, payload);
4458
+ this.close();
4459
+ // Show purchase success modal
4460
+ const successModal = new PurchaseSuccessModal({
4461
+ data: {
4462
+ packName: pkgName,
4463
+ credits: parseInt(pkg.credits),
4464
+ amount: pkg.price,
4465
+ currency: pkg.currency === 'USD' ? '$' : pkg.currency,
4466
+ orderId: orderId,
4467
+ transactionId: payload.transaction_id,
4468
+ },
4469
+ language: this.language,
4470
+ onClose: () => {
4471
+ // Refresh credits after modal closes
4472
+ (async () => {
4473
+ try {
4474
+ const envConfig = ENVIRONMENT_CONFIGS[this.options.sdkConfig.environment];
4475
+ const walletApiUrl = envConfig.walletApiUrl;
4476
+ console.log(`[${this.constructor.name}] Refreshing credits from:`, walletApiUrl);
4477
+ const creditDetail = await getCreditDetail(walletApiUrl, this.options.sdkConfig.accountToken);
4478
+ if (creditDetail) {
4479
+ console.log(`[${this.constructor.name}] Credits refreshed, total balance:`, creditDetail.total_balance);
4480
+ }
4481
+ else {
4482
+ console.warn(`[${this.constructor.name}] Failed to refresh credits`);
4483
+ }
4484
+ }
4485
+ catch (error) {
4486
+ console.error(`[${this.constructor.name}] Failed to refresh credits:`, error);
4487
+ }
4488
+ })();
4489
+ // Trigger user callback
4490
+ this.options.onPaymentSuccess?.(orderId, payload.transaction_id, pkg);
4491
+ },
4492
+ });
4493
+ successModal.open();
4494
+ },
4495
+ onFailed: (payload) => {
4496
+ console.error(`[${this.constructor.name}] Payment failed:`, payload);
4497
+ const error = new Error(payload.message || 'Payment failed');
4498
+ this.options.onPaymentFailed?.(error, pkg);
4499
+ },
4500
+ onError: (payload, error) => {
4501
+ console.error(`[${this.constructor.name}] Payment error:`, error);
4502
+ this.options.onPaymentFailed?.(error, pkg);
4503
+ },
4504
+ });
4505
+ await dropinModal.open();
4506
+ }
4507
+ /**
4508
+ * Attach event listeners to package buttons
3585
4509
  */
3586
- attachEventListeners() {
3587
- if (!this.overlay)
3588
- return;
3589
- // 点击遮罩关闭
3590
- this.overlay.addEventListener('click', (e) => {
3591
- if (e.target === this.overlay) {
3592
- this.close();
4510
+ attachEventListeners(container) {
4511
+ const packages = this.getPackages();
4512
+ packages.forEach(pkg => {
4513
+ const button = container.querySelector(`[data-package-button="${pkg.id}"]`);
4514
+ if (button) {
4515
+ button.addEventListener('click', async (e) => {
4516
+ e.preventDefault();
4517
+ e.stopPropagation();
4518
+ console.log(`[${this.constructor.name}] Package selected:`, pkg.id);
4519
+ const originalText = button.innerHTML;
4520
+ const originalDisabled = button.disabled;
4521
+ try {
4522
+ // Disable button, show initializing state
4523
+ button.disabled = true;
4524
+ const isZh = this.language === 'zh-CN';
4525
+ button.innerHTML = this.getLoadingButtonHTML(isZh ? '初始化中...' : 'Initializing...');
4526
+ // Wait for SDK initialization (with retry)
4527
+ const initialized = await this.waitForSDKInitialization(30000, 1);
4528
+ if (!initialized) {
4529
+ throw new Error('SDK initialization failed or timed out. Please try again.');
4530
+ }
4531
+ // SDK initialization successful, execute payment flow
4532
+ await this.handlePaymentFlow(pkg, button, originalText);
4533
+ }
4534
+ catch (error) {
4535
+ console.error(`[${this.constructor.name}] Failed to process payment:`, error);
4536
+ // Restore button state
4537
+ button.disabled = originalDisabled;
4538
+ button.innerHTML = originalText;
4539
+ // Show error message (use custom UI instead of alert)
4540
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
4541
+ showErrorMessage(`Payment failed: ${errorMessage}`);
4542
+ // Trigger failure callback
4543
+ this.options.onPaymentFailed?.(error instanceof Error ? error : new Error(String(error)), pkg);
4544
+ }
4545
+ });
3593
4546
  }
3594
4547
  });
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
4548
  }
3612
4549
  /**
3613
- * 清理资源
4550
+ * Cleanup resources
3614
4551
  */
3615
4552
  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);
4553
+ console.log(`[${this.constructor.name}] Cleaning up...`);
4554
+ // Remove resize listener
4555
+ if (this.resizeHandler) {
4556
+ window.removeEventListener('resize', this.resizeHandler);
4557
+ this.resizeHandler = null;
3624
4558
  }
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
4559
  }
4560
+ // === Utility Methods ===
3635
4561
  /**
3636
- * HTML 转义工具函数
3637
- * 防止 XSS 攻击
4562
+ * Format number with commas
3638
4563
  */
3639
- escapeHtml(text) {
3640
- const div = document.createElement('div');
3641
- div.textContent = text;
3642
- return div.innerHTML;
4564
+ formatNumber(num) {
4565
+ return parseInt(num).toLocaleString();
3643
4566
  }
3644
4567
  /**
3645
- * 检查弹窗是否打开
4568
+ * Get content container from modal
3646
4569
  */
3647
- isOpen() {
3648
- return this.overlay !== null && !this.isExiting;
4570
+ getContentContainer() {
4571
+ return this.modal.getContentContainer();
3649
4572
  }
3650
4573
  }
3651
4574
 
@@ -3653,12 +4576,10 @@ class PurchaseSuccessModal {
3653
4576
  * CreditPackageModal - 积分套餐选择弹框
3654
4577
  * 展示不同的积分套餐供用户选择
3655
4578
  */
3656
- class CreditPackageModal {
3657
- constructor(options) {
3658
- this.resizeHandler = null;
3659
- this.isInitializingSDK = false;
3660
- this.sdkInitialized = false;
3661
- // 设计系统常量
4579
+ class CreditPackageModal extends BasePackageModal {
4580
+ constructor() {
4581
+ super(...arguments);
4582
+ // Design system constants
3662
4583
  this.SPACING = {
3663
4584
  xs: '8px',
3664
4585
  sm: '16px',
@@ -3678,241 +4599,105 @@ class CreditPackageModal {
3678
4599
  accent: '#22ce9c',
3679
4600
  },
3680
4601
  };
3681
- this.options = options;
3682
- this.language = options.language || 'en';
3683
- // 创建弹框
3684
- this.modal = new PaymentModal({
4602
+ }
4603
+ // === Abstract Method Implementations ===
4604
+ /**
4605
+ * Create and configure the PaymentModal instance
4606
+ */
4607
+ createModal() {
4608
+ return new PaymentModal({
3685
4609
  title: this.language === 'zh-CN'
3686
- ? (options.title_cn || '选择您的创作力量')
3687
- : (options.title || 'Choose Your Creative Power'),
4610
+ ? (this.options.title_cn || '选择您的创作力量')
4611
+ : (this.options.title || 'Choose Your Creative Power'),
3688
4612
  showCloseButton: true,
3689
- closeOnOverlayClick: false, // 禁用点击空白处关闭
3690
- closeOnEsc: false, // 禁用ESC键关闭
4613
+ closeOnOverlayClick: false, // Disable click overlay to close
4614
+ closeOnEsc: false, // Disable ESC key to close
3691
4615
  maxWidth: '1200px',
3692
4616
  onClose: () => {
3693
4617
  this.cleanup();
3694
- options.onClose?.();
4618
+ this.options.onClose?.();
3695
4619
  },
3696
4620
  });
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
4621
  }
3756
4622
  /**
3757
- * 等待 SDK 初始化完成(支持超时和重试)
3758
- * @param timeout 超时时间(毫秒),默认 30 秒
3759
- * @param maxRetries 最大重试次数,默认 1 次
3760
- * @returns 是否初始化成功
4623
+ * Get packages to display
3761
4624
  */
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;
4625
+ getPackages() {
4626
+ return CREDIT_PACKAGES;
3802
4627
  }
3803
4628
  /**
3804
- * 初始化支付SDK(后台静默执行)
4629
+ * Get package display name for payment modal title
3805
4630
  */
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
- }
4631
+ getPackageDisplayName(pkg) {
4632
+ const isZh = this.language === 'zh-CN';
4633
+ return isZh ? `${pkg.credits} 积分套餐` : `${pkg.credits} Credits Package`;
3872
4634
  }
3873
4635
  /**
3874
- * 关闭弹框
4636
+ * Get loading button HTML with spinner
3875
4637
  */
3876
- close() {
3877
- console.log('[CreditPackageModal] Closing modal...');
3878
- this.modal.close();
4638
+ getLoadingButtonHTML(text, isPopular = false) {
4639
+ return `
4640
+ <svg style="
4641
+ display: inline-block;
4642
+ width: 16px;
4643
+ height: 16px;
4644
+ border: 2px solid ${isPopular ? 'rgba(10, 10, 15, 0.3)' : 'rgba(255, 255, 255, 0.3)'};
4645
+ border-top-color: ${isPopular ? '#0a0a0f' : 'white'};
4646
+ border-radius: 50%;
4647
+ animation: spin 0.6s linear infinite;
4648
+ " viewBox="0 0 24 24"></svg>
4649
+ <style>@keyframes spin { to { transform: rotate(360deg); } }</style>
4650
+ ${text ? `<span style="margin-left: 8px;">${text}</span>` : ''}
4651
+ `;
3879
4652
  }
3880
4653
  /**
3881
- * 获取响应式样式配置
4654
+ * Apply modal styling (hook method override)
3882
4655
  */
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';
4656
+ applyModalStyling() {
4657
+ // Modify modal background to dark
4658
+ const modalElement = document.querySelector('.payment-modal');
4659
+ if (modalElement) {
4660
+ modalElement.style.background = '#0a0a0f';
4661
+ modalElement.style.border = '1px solid rgba(255, 255, 255, 0.1)';
3894
4662
  }
3895
- else if (isTablet) {
3896
- computeColumns = 2;
3897
- packColumns = 2;
3898
- padding = '0 30px 30px';
4663
+ // Modify title style
4664
+ const titleElement = document.querySelector('.payment-modal-title');
4665
+ if (titleElement) {
4666
+ titleElement.style.color = 'white';
4667
+ titleElement.style.fontSize = '28px';
4668
+ titleElement.style.fontWeight = '700';
4669
+ titleElement.style.padding = '32px 32px 0 32px';
4670
+ titleElement.style.marginBottom = '16px';
4671
+ titleElement.style.letterSpacing = '-0.01em';
3899
4672
  }
3900
- else if (isLaptop) {
3901
- computeColumns = 3;
3902
- packColumns = 2;
3903
- padding = '0 40px 40px';
4673
+ // Modify close button color and add hover animation
4674
+ const closeButton = document.querySelector('.payment-modal-close');
4675
+ if (closeButton) {
4676
+ closeButton.style.background = 'rgba(255, 255, 255, 0.05)';
4677
+ closeButton.style.color = 'rgba(255, 255, 255, 0.7)';
4678
+ closeButton.style.transition = 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)';
4679
+ // Remove old event listeners (if any)
4680
+ const newCloseButton = closeButton.cloneNode(true);
4681
+ closeButton.parentNode?.replaceChild(newCloseButton, closeButton);
4682
+ // Add hover animation
4683
+ newCloseButton.addEventListener('mouseenter', () => {
4684
+ newCloseButton.style.background = 'rgba(255, 255, 255, 0.1)';
4685
+ newCloseButton.style.color = 'white';
4686
+ newCloseButton.style.transform = 'rotate(90deg) scale(1.1)';
4687
+ });
4688
+ newCloseButton.addEventListener('mouseleave', () => {
4689
+ newCloseButton.style.background = 'rgba(255, 255, 255, 0.05)';
4690
+ newCloseButton.style.color = 'rgba(255, 255, 255, 0.7)';
4691
+ newCloseButton.style.transform = 'rotate(0deg) scale(1)';
4692
+ });
4693
+ newCloseButton.addEventListener('click', () => this.close());
3904
4694
  }
3905
- return {
3906
- containerPadding: padding,
3907
- computeGridColumns: `repeat(${computeColumns}, 1fr)`,
3908
- packGridColumns: `repeat(${packColumns}, 1fr)`,
3909
- };
3910
4695
  }
3911
4696
  /**
3912
- * 渲染弹框内容
4697
+ * Render modal content
3913
4698
  */
3914
4699
  renderContent() {
3915
- const container = this.modal.getContentContainer();
4700
+ const container = this.getContentContainer();
3916
4701
  if (!container) {
3917
4702
  throw new Error('Modal content container not found');
3918
4703
  }
@@ -3925,7 +4710,7 @@ class CreditPackageModal {
3925
4710
  color: white;
3926
4711
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
3927
4712
  ">
3928
- <!-- 副标题 -->
4713
+ <!-- Subtitle -->
3929
4714
  <p style="
3930
4715
  text-align: center;
3931
4716
  font-size: 16px;
@@ -3935,11 +4720,11 @@ class CreditPackageModal {
3935
4720
  font-weight: 400;
3936
4721
  ">
3937
4722
  ${isZh
3938
- ? (this.options.subtitle_cn || '免费开始,随创作扩展。所有套餐都包含用于电影、游戏、音乐和世界的算力积分。')
4723
+ ? (this.options.subtitle_cn || '免费开始,随创作扩展。所有套餐都包含用于电影、游戏、音乐和世界的算力积分。')
3939
4724
  : (this.options.subtitle || 'Start free, scale with creation. All packages include credits for movies, games, music, and worlds.')}
3940
4725
  </p>
3941
4726
 
3942
- <!-- 算力积分说明区域 - 完全复刻 next-meta pricing -->
4727
+ <!-- Compute credits section - fully replicate next-meta pricing -->
3943
4728
  <div style="margin-bottom: ${this.SPACING.xl};">
3944
4729
  <!-- Section Header -->
3945
4730
  <div style="margin-bottom: ${this.SPACING.xl}; text-align: center;">
@@ -3963,221 +4748,18 @@ class CreditPackageModal {
3963
4748
  </p>
3964
4749
  </div>
3965
4750
 
3966
- <!-- Credits Grid - 完全复刻 next-meta 5 个卡片 -->
4751
+ <!-- Credits Grid - fully replicate next-meta 5 cards -->
3967
4752
  <div style="
3968
4753
  display: grid;
3969
4754
  grid-template-columns: ${styles.computeGridColumns};
3970
4755
  gap: 12px;
3971
4756
  margin-bottom: 20px;
3972
4757
  ">
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>
4758
+ ${this.renderComputeCreditsCards()}
4177
4759
  </div>
4178
4760
  </div>
4179
4761
 
4180
- <!-- Credit Packs - 积分包购买标题 -->
4762
+ <!-- Credit Packs - credit package purchase title -->
4181
4763
  <div style="margin: 0 auto ${this.SPACING.lg}; max-width: 80rem; text-align: center;">
4182
4764
  <h3 style="
4183
4765
  margin-bottom: ${this.SPACING.xs};
@@ -4200,7 +4782,7 @@ class CreditPackageModal {
4200
4782
  </p>
4201
4783
  </div>
4202
4784
 
4203
- <!-- 套餐卡片 -->
4785
+ <!-- Package cards -->
4204
4786
  <div style="
4205
4787
  display: grid;
4206
4788
  grid-template-columns: ${styles.packGridColumns};
@@ -4210,17 +4792,133 @@ class CreditPackageModal {
4210
4792
  </div>
4211
4793
  </div>
4212
4794
  `;
4213
- // 添加点击事件监听
4795
+ // Attach event listeners
4214
4796
  this.attachEventListeners(container);
4215
4797
  }
4798
+ // === Private Helper Methods ===
4799
+ /**
4800
+ * Get responsive style configuration
4801
+ */
4802
+ getResponsiveStyles() {
4803
+ const isMobile = window.matchMedia('(max-width: 768px)').matches;
4804
+ const isTablet = window.matchMedia('(max-width: 1200px)').matches;
4805
+ const isLaptop = window.matchMedia('(max-width: 1400px)').matches;
4806
+ let computeColumns = 5;
4807
+ let packColumns = 4;
4808
+ let padding = '0 60px 60px';
4809
+ if (isMobile) {
4810
+ computeColumns = 1;
4811
+ packColumns = 1;
4812
+ padding = '0 20px 20px';
4813
+ }
4814
+ else if (isTablet) {
4815
+ computeColumns = 2;
4816
+ packColumns = 2;
4817
+ padding = '0 30px 30px';
4818
+ }
4819
+ else if (isLaptop) {
4820
+ computeColumns = 3;
4821
+ packColumns = 2;
4822
+ padding = '0 40px 40px';
4823
+ }
4824
+ return {
4825
+ containerPadding: padding,
4826
+ computeGridColumns: `repeat(${computeColumns}, 1fr)`,
4827
+ packGridColumns: `repeat(${packColumns}, 1fr)`,
4828
+ };
4829
+ }
4830
+ /**
4831
+ * Render compute credits cards
4832
+ */
4833
+ renderComputeCreditsCards() {
4834
+ const isZh = this.language === 'zh-CN';
4835
+ const cards = [
4836
+ {
4837
+ icon: '<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"/>',
4838
+ gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
4839
+ title: isZh ? '电影片段' : 'Film Clips',
4840
+ credits: '100-300',
4841
+ description: isZh ? '每次生成' : 'per generation',
4842
+ },
4843
+ {
4844
+ icon: '<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"/>',
4845
+ gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
4846
+ title: isZh ? '游戏场景' : 'Game Scenes',
4847
+ credits: '50-200',
4848
+ description: isZh ? '每个场景' : 'per scene',
4849
+ },
4850
+ {
4851
+ icon: '<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"/>',
4852
+ gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
4853
+ title: isZh ? '音乐' : 'Music',
4854
+ credits: '30-100',
4855
+ description: isZh ? '每首曲目' : 'per track',
4856
+ },
4857
+ {
4858
+ icon: '<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"/>',
4859
+ gradient: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
4860
+ title: isZh ? '3D 世界' : '3D Worlds',
4861
+ credits: '150-500',
4862
+ description: isZh ? '每个世界' : 'per world',
4863
+ },
4864
+ {
4865
+ icon: '<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"/>',
4866
+ gradient: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
4867
+ title: isZh ? 'AI 会话' : 'Agent Sessions',
4868
+ credits: '10',
4869
+ description: isZh ? '每次会话' : 'per session',
4870
+ },
4871
+ ];
4872
+ return cards.map(card => `
4873
+ <article style="
4874
+ position: relative;
4875
+ overflow: hidden;
4876
+ border-radius: 16px;
4877
+ border: 1px solid rgba(255, 255, 255, 0.06);
4878
+ background: rgba(255, 255, 255, 0.05);
4879
+ padding: 20px;
4880
+ text-align: center;
4881
+ backdrop-filter: blur(40px);
4882
+ opacity: 0.7;
4883
+ transform: scale(0.98);
4884
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
4885
+ " 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)';"
4886
+ 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';">
4887
+ <div style="
4888
+ margin: 0 auto 16px;
4889
+ display: flex;
4890
+ align-items: center;
4891
+ justify-content: center;
4892
+ height: 48px;
4893
+ width: 48px;
4894
+ border-radius: 12px;
4895
+ background: ${card.gradient};
4896
+ box-shadow: 0 8px 20px rgba(0,0,0,0.2);
4897
+ ">
4898
+ <svg style="height: 24px; width: 24px; color: white;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4899
+ ${card.icon}
4900
+ </svg>
4901
+ </div>
4902
+ <h3 style="margin-bottom: 8px; font-size: 16px; font-weight: 600; color: white;">
4903
+ ${card.title}
4904
+ </h3>
4905
+ <p style="margin-bottom: 8px; font-family: 'Monaco', 'Menlo', monospace; font-size: 20px; font-weight: 800; color: #00ff88;">
4906
+ ${card.credits}
4907
+ </p>
4908
+ <p style="font-size: 12px; line-height: 1.5; color: rgba(255, 255, 255, 0.5);">
4909
+ ${card.description}
4910
+ </p>
4911
+ </article>
4912
+ `).join('');
4913
+ }
4216
4914
  /**
4217
- * 渲染套餐卡片
4915
+ * Render package card
4218
4916
  */
4219
4917
  renderPackageCard(pkg, index) {
4220
4918
  const isZh = this.language === 'zh-CN';
4221
4919
  const isPopular = pkg.is_popular;
4222
- const hasBonus = parseInt(pkg.bonus_credits) > 0;
4223
- // Popular包的呼吸动画
4920
+ const hasBonus = pkg.bonus_credits && parseInt(pkg.bonus_credits) > 0;
4921
+ // Popular package breathing animation
4224
4922
  const pulseAnimation = isPopular ? `
4225
4923
  <style>
4226
4924
  @keyframes pulse-${pkg.id} {
@@ -4280,7 +4978,7 @@ class CreditPackageModal {
4280
4978
  </div>
4281
4979
  ` : ''}
4282
4980
 
4283
- <!-- 积分总数 -->
4981
+ <!-- Total credits -->
4284
4982
  <div style="
4285
4983
  margin-bottom: 8px;
4286
4984
  font-size: 36px;
@@ -4292,7 +4990,7 @@ class CreditPackageModal {
4292
4990
  ${this.formatNumber(pkg.credits)}
4293
4991
  </div>
4294
4992
 
4295
- <!-- 积分明细 -->
4993
+ <!-- Credits breakdown -->
4296
4994
  <div style="
4297
4995
  margin-bottom: 12px;
4298
4996
  display: flex;
@@ -4306,7 +5004,7 @@ class CreditPackageModal {
4306
5004
  font-weight: 500;
4307
5005
  color: hsl(240, 5%, 65%);
4308
5006
  ">
4309
- ${this.formatNumber(pkg.base_credits)} ${isZh ? '积分' : 'Credits'}
5007
+ ${this.formatNumber(pkg.base_credits || pkg.credits)} ${isZh ? '积分' : 'Credits'}
4310
5008
  </span>
4311
5009
  ${hasBonus ? `
4312
5010
  <span style="
@@ -4322,7 +5020,7 @@ class CreditPackageModal {
4322
5020
  ` : ''}
4323
5021
  </div>
4324
5022
 
4325
- <!-- 购买按钮 -->
5023
+ <!-- Buy button -->
4326
5024
  <button
4327
5025
  data-package-button="${pkg.id}"
4328
5026
  style="
@@ -4347,202 +5045,6 @@ class CreditPackageModal {
4347
5045
  </div>
4348
5046
  `;
4349
5047
  }
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
5048
  }
4547
5049
 
4548
5050
  /**
@@ -4550,46 +5052,75 @@ class CreditPackageModal {
4550
5052
  * 支持多种套餐类型(破冰包、告急包、首充包等)
4551
5053
  * 套餐数据从外部配置传入,无硬编码
4552
5054
  */
4553
- class GenericPackageModal {
5055
+ class GenericPackageModal extends BasePackageModal {
4554
5056
  constructor(options) {
4555
- this.resizeHandler = null;
4556
- this.isInitializingSDK = false;
4557
- this.sdkInitialized = false;
4558
- // 验证必填字段
5057
+ // Validate required fields
4559
5058
  if (!options.packages || options.packages.length === 0) {
4560
5059
  throw new Error('GenericPackageModal: packages array is required and cannot be empty');
4561
5060
  }
4562
- this.options = options;
4563
- this.language = options.language || 'en';
4564
- // 创建弹框 - 根据套餐数量动态调整宽度
4565
- const packageCount = options.packages.length;
4566
- let maxWidth = '680px'; // 单个套餐默认宽度(优化宽高比,确保积分文本完整显示)
5061
+ super(options);
5062
+ }
5063
+ // === Abstract Method Implementations ===
5064
+ /**
5065
+ * Create and configure the PaymentModal instance
5066
+ */
5067
+ createModal() {
5068
+ // Dynamically adjust width based on package count
5069
+ const packageCount = this.options.packages.length;
5070
+ let maxWidth = '680px'; // Single package default width (optimized aspect ratio, ensure credit text is fully displayed)
4567
5071
  if (packageCount === 2) {
4568
5072
  maxWidth = '1100px';
4569
5073
  }
4570
5074
  else if (packageCount >= 3) {
4571
5075
  maxWidth = '1200px';
4572
5076
  }
4573
- this.modal = new PaymentModal({
4574
- title: '', // 不显示标题
4575
- showCloseButton: false, // 不显示弹框关闭按钮,使用卡片内的关闭按钮
4576
- closeOnOverlayClick: false, // 禁用点击空白处关闭
4577
- closeOnEsc: true, // 允许ESC键关闭
5077
+ return new PaymentModal({
5078
+ title: '', // No title
5079
+ showCloseButton: false, // No modal close button, use card close button
5080
+ closeOnOverlayClick: false, // Disable click overlay to close
5081
+ closeOnEsc: true, // Allow ESC key to close
4578
5082
  maxWidth: maxWidth,
4579
5083
  onClose: () => {
4580
5084
  this.cleanup();
4581
- options.onClose?.();
5085
+ this.options.onClose?.();
4582
5086
  },
4583
5087
  });
4584
- console.log('[GenericPackageModal] Created with', options.packages.length, 'packages');
4585
5088
  }
4586
5089
  /**
4587
- * 打开弹框
5090
+ * Get packages to display
5091
+ */
5092
+ getPackages() {
5093
+ return this.options.packages;
5094
+ }
5095
+ /**
5096
+ * Get package display name for payment modal title
5097
+ */
5098
+ getPackageDisplayName(pkg) {
5099
+ return pkg.name;
5100
+ }
5101
+ /**
5102
+ * Get loading button HTML with spinner
5103
+ */
5104
+ getLoadingButtonHTML(text) {
5105
+ return `
5106
+ <svg style="
5107
+ display: inline-block;
5108
+ width: 16px;
5109
+ height: 16px;
5110
+ border: 2px solid rgba(255, 255, 255, 0.3);
5111
+ border-top-color: white;
5112
+ border-radius: 50%;
5113
+ animation: spin 0.6s linear infinite;
5114
+ " viewBox="0 0 24 24"></svg>
5115
+ <style>@keyframes spin { to { transform: rotate(360deg); } }</style>
5116
+ <span style="margin-left: 8px;">${text}</span>
5117
+ `;
5118
+ }
5119
+ /**
5120
+ * Apply modal styling (hook method override)
4588
5121
  */
4589
- async open() {
4590
- console.log('[GenericPackageModal] Opening modal...');
4591
- this.modal.open();
4592
- // 修改弹框样式 - 让卡片完全填充
5122
+ applyModalStyling() {
5123
+ // Modify modal style - make card fully fill
4593
5124
  const modalElement = document.querySelector('.payment-modal');
4594
5125
  if (modalElement) {
4595
5126
  modalElement.style.background = 'transparent';
@@ -4598,176 +5129,41 @@ class GenericPackageModal {
4598
5129
  modalElement.style.borderRadius = '16px';
4599
5130
  modalElement.style.overflow = 'hidden';
4600
5131
  }
4601
- // 修改内容容器样式 - 移除padding
5132
+ // Modify content container style - remove padding
4602
5133
  const contentElement = document.querySelector('.payment-modal-content');
4603
5134
  if (contentElement) {
4604
5135
  contentElement.style.padding = '0';
4605
5136
  contentElement.style.margin = '0';
4606
5137
  }
4607
- // 监听卡片内的关闭按钮事件
4608
- const container = this.modal.getContentContainer();
5138
+ // Listen for close button event from card
5139
+ const container = this.getContentContainer();
4609
5140
  if (container) {
4610
5141
  container.addEventListener('close-modal', () => {
4611
5142
  this.close();
4612
5143
  });
4613
5144
  }
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
5145
  }
4751
5146
  /**
4752
- * 渲染弹框内容
5147
+ * Render modal content
4753
5148
  */
4754
5149
  renderContent() {
4755
- const container = this.modal.getContentContainer();
5150
+ const container = this.getContentContainer();
4756
5151
  if (!container) {
4757
5152
  throw new Error('Modal content container not found');
4758
5153
  }
4759
- // 直接渲染卡片内容,不添加任何外层包装
5154
+ // Directly render card content without any outer wrapper
4760
5155
  container.innerHTML = this.options.packages.map((pkg, index) => this.renderPackageCard(pkg, index)).join('');
4761
- // 添加点击事件监听
5156
+ // Attach event listeners
4762
5157
  this.attachEventListeners(container);
4763
5158
  }
5159
+ // === Private Helper Methods ===
4764
5160
  /**
4765
- * 渲染套餐卡片
5161
+ * Render package card
4766
5162
  */
4767
5163
  renderPackageCard(pkg, index) {
4768
5164
  const hasBonus = pkg.bonus_credits && parseInt(pkg.bonus_credits) > 0;
4769
5165
  const hasBonusPercentage = pkg.bonus_percentage && pkg.bonus_percentage > 0;
4770
- // 根据套餐类型决定显示的标题
5166
+ // Determine title based on package type
4771
5167
  let packageTitle = '';
4772
5168
  if (pkg.package_type === 'iceBreaker' || pkg.package_type === 'firstCharge') {
4773
5169
  packageTitle = 'One-time Only';
@@ -4796,7 +5192,7 @@ class GenericPackageModal {
4796
5192
  onmouseover="this.style.borderColor='rgba(255, 255, 255, 0.2)';"
4797
5193
  onmouseout="this.style.borderColor='rgba(255, 255, 255, 0.1)';"
4798
5194
  >
4799
- <!-- 套餐标题 - 卡片内顶部居中 -->
5195
+ <!-- Package title - top center of card -->
4800
5196
  ${packageTitle ? `
4801
5197
  <div style="
4802
5198
  position: absolute;
@@ -4814,7 +5210,7 @@ class GenericPackageModal {
4814
5210
  </div>
4815
5211
  ` : ''}
4816
5212
 
4817
- <!-- 关闭按钮 - 右上角 -->
5213
+ <!-- Close button - top right -->
4818
5214
  <button
4819
5215
  onclick="this.closest('[data-package-id]').dispatchEvent(new CustomEvent('close-modal', { bubbles: true }));"
4820
5216
  style="
@@ -4842,7 +5238,7 @@ class GenericPackageModal {
4842
5238
  ×
4843
5239
  </button>
4844
5240
 
4845
- <!-- 折扣标签 - 关闭按钮下方 -->
5241
+ <!-- Discount tag - below close button -->
4846
5242
  ${hasBonusPercentage ? `
4847
5243
  <div style="
4848
5244
  position: absolute;
@@ -4862,7 +5258,7 @@ class GenericPackageModal {
4862
5258
  </div>
4863
5259
  ` : ''}
4864
5260
 
4865
- <!-- 积分显示区域 - 确保不换行 -->
5261
+ <!-- Credits display area - ensure no line break -->
4866
5262
  <div style="
4867
5263
  display: flex;
4868
5264
  flex-direction: column;
@@ -4879,7 +5275,7 @@ class GenericPackageModal {
4879
5275
  gap: 12px;
4880
5276
  flex-wrap: nowrap;
4881
5277
  ">
4882
- <!-- 基础积分 -->
5278
+ <!-- Base credits -->
4883
5279
  <span style="
4884
5280
  font-size: 80px;
4885
5281
  line-height: 1;
@@ -4891,7 +5287,7 @@ class GenericPackageModal {
4891
5287
  ${this.formatNumber(pkg.base_credits || pkg.credits)}
4892
5288
  </span>
4893
5289
 
4894
- <!-- credits 文本 -->
5290
+ <!-- credits text -->
4895
5291
  <span style="
4896
5292
  font-size: 52px;
4897
5293
  line-height: 1;
@@ -4903,7 +5299,7 @@ class GenericPackageModal {
4903
5299
  credits
4904
5300
  </span>
4905
5301
 
4906
- <!-- 奖励积分 -->
5302
+ <!-- Bonus credits -->
4907
5303
  ${hasBonus ? `
4908
5304
  <span style="
4909
5305
  font-size: 52px;
@@ -4919,7 +5315,7 @@ class GenericPackageModal {
4919
5315
  </div>
4920
5316
  </div>
4921
5317
 
4922
- <!-- 副标题 - Valid for all platform products -->
5318
+ <!-- Subtitle - Valid for all platform products -->
4923
5319
  <div style="
4924
5320
  font-size: 22px;
4925
5321
  line-height: 1.4;
@@ -4930,7 +5326,7 @@ class GenericPackageModal {
4930
5326
  Valid for all platform products
4931
5327
  </div>
4932
5328
 
4933
- <!-- 购买按钮 -->
5329
+ <!-- Buy button -->
4934
5330
  <button
4935
5331
  data-package-button="${pkg.id}"
4936
5332
  style="
@@ -4958,201 +5354,6 @@ class GenericPackageModal {
4958
5354
  </div>
4959
5355
  `;
4960
5356
  }
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
5357
  }
5157
5358
 
5158
5359
  /**
@@ -21793,6 +21994,7 @@ exports.PaymentModal = PaymentModal;
21793
21994
  exports.PaymentStorage = PaymentStorage;
21794
21995
  exports.PurchaseSuccessModal = PurchaseSuccessModal;
21795
21996
  exports.RESPONSIVE_BREAKPOINTS = RESPONSIVE_BREAKPOINTS;
21997
+ exports.RetentionModal = RetentionModal;
21796
21998
  exports.SDK_CONFIG = SDK_CONFIG;
21797
21999
  exports.ScriptLoader = ScriptLoader;
21798
22000
  exports.SeaArtPayLoader = SeaArtPayLoader;