@seaverse/payment-sdk 0.8.1 → 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
@@ -4221,184 +4221,94 @@ class PurchaseSuccessModal {
4221
4221
  }
4222
4222
 
4223
4223
  /**
4224
- * CreditPackageModal - 积分套餐选择弹框
4225
- * 展示不同的积分套餐供用户选择
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
4226
4230
  */
4227
- class CreditPackageModal {
4231
+ class BasePackageModal {
4232
+ // === Constructor ===
4228
4233
  constructor(options) {
4229
4234
  this.resizeHandler = null;
4230
4235
  this.isInitializingSDK = false;
4231
4236
  this.sdkInitialized = false;
4232
- // 设计系统常量
4233
- this.SPACING = {
4234
- xs: '8px',
4235
- sm: '16px',
4236
- md: '24px',
4237
- lg: '32px',
4238
- xl: '48px',
4239
- };
4240
- this.COLORS = {
4241
- text: {
4242
- primary: 'rgba(255, 255, 255, 0.95)',
4243
- secondary: 'rgba(255, 255, 255, 0.65)',
4244
- tertiary: 'rgba(255, 255, 255, 0.5)',
4245
- },
4246
- green: {
4247
- primary: '#00ff88',
4248
- secondary: '#00f2fe',
4249
- accent: '#22ce9c',
4250
- },
4251
- };
4252
4237
  this.options = options;
4253
4238
  this.language = options.language || 'en';
4254
- // 创建弹框
4255
- this.modal = new PaymentModal({
4256
- title: this.language === 'zh-CN'
4257
- ? (options.title_cn || '选择您的创作力量')
4258
- : (options.title || 'Choose Your Creative Power'),
4259
- showCloseButton: true,
4260
- closeOnOverlayClick: false, // 禁用点击空白处关闭
4261
- closeOnEsc: false, // 禁用ESC键关闭
4262
- maxWidth: '1200px',
4263
- onClose: () => {
4264
- this.cleanup();
4265
- options.onClose?.();
4266
- },
4267
- });
4268
- console.log('[CreditPackageModal] Created');
4239
+ this.modal = this.createModal();
4240
+ console.log(`[${this.constructor.name}] Created`);
4269
4241
  }
4242
+ // === Public Methods ===
4270
4243
  /**
4271
- * 打开弹框
4244
+ * Open the modal
4272
4245
  */
4273
4246
  async open() {
4274
- console.log('[CreditPackageModal] Opening modal...');
4247
+ console.log(`[${this.constructor.name}] Opening modal...`);
4275
4248
  this.modal.open();
4276
- // 修改弹框背景为深色
4277
- const modalElement = document.querySelector('.payment-modal');
4278
- if (modalElement) {
4279
- modalElement.style.background = '#0a0a0f';
4280
- modalElement.style.border = '1px solid rgba(255, 255, 255, 0.1)';
4281
- }
4282
- // 修改标题样式
4283
- const titleElement = document.querySelector('.payment-modal-title');
4284
- if (titleElement) {
4285
- titleElement.style.color = 'white';
4286
- titleElement.style.fontSize = '28px';
4287
- titleElement.style.fontWeight = '700';
4288
- titleElement.style.padding = '32px 32px 0 32px';
4289
- titleElement.style.marginBottom = '16px';
4290
- titleElement.style.letterSpacing = '-0.01em';
4291
- }
4292
- // 修改关闭按钮颜色并添加 hover 动画
4293
- const closeButton = document.querySelector('.payment-modal-close');
4294
- if (closeButton) {
4295
- closeButton.style.background = 'rgba(255, 255, 255, 0.05)';
4296
- closeButton.style.color = 'rgba(255, 255, 255, 0.7)';
4297
- closeButton.style.transition = 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)';
4298
- // 移除旧的事件监听器(如果有)
4299
- const newCloseButton = closeButton.cloneNode(true);
4300
- closeButton.parentNode?.replaceChild(newCloseButton, closeButton);
4301
- // 添加 hover 动画
4302
- newCloseButton.addEventListener('mouseenter', () => {
4303
- newCloseButton.style.background = 'rgba(255, 255, 255, 0.1)';
4304
- newCloseButton.style.color = 'white';
4305
- newCloseButton.style.transform = 'rotate(90deg) scale(1.1)';
4306
- });
4307
- newCloseButton.addEventListener('mouseleave', () => {
4308
- newCloseButton.style.background = 'rgba(255, 255, 255, 0.05)';
4309
- newCloseButton.style.color = 'rgba(255, 255, 255, 0.7)';
4310
- newCloseButton.style.transform = 'rotate(0deg) scale(1)';
4311
- });
4312
- newCloseButton.addEventListener('click', () => this.close());
4313
- }
4314
- // 渲染内容
4249
+ // Hook for subclass-specific styling
4250
+ this.applyModalStyling();
4251
+ // Render content (abstract method)
4315
4252
  this.renderContent();
4316
- // 添加resize监听器,窗口大小改变时重新渲染以应用响应式样式
4317
- this.resizeHandler = () => {
4318
- this.renderContent();
4319
- };
4253
+ // Add resize listener
4254
+ this.resizeHandler = () => this.renderContent();
4320
4255
  window.addEventListener('resize', this.resizeHandler);
4321
- // 如果配置了 initPaymentSDK,在后台自动初始化 SDK
4256
+ // Initialize SDK in background
4322
4257
  if (!this.sdkInitialized && !this.isInitializingSDK) {
4323
4258
  this.initializeSDK();
4324
4259
  }
4325
- console.log('[CreditPackageModal] Modal opened');
4260
+ console.log(`[${this.constructor.name}] Modal opened`);
4326
4261
  }
4327
4262
  /**
4328
- * 等待 SDK 初始化完成(支持超时和重试)
4329
- * @param timeout 超时时间(毫秒),默认 30 秒
4330
- * @param maxRetries 最大重试次数,默认 1 次
4331
- * @returns 是否初始化成功
4263
+ * Close the modal
4332
4264
  */
4333
- async waitForSDKInitialization(timeout = 30000, maxRetries = 1) {
4334
- const startTime = Date.now();
4335
- // 如果已经初始化完成,直接返回
4336
- if (this.sdkInitialized) {
4337
- console.log('[CreditPackageModal] SDK already initialized');
4338
- return true;
4339
- }
4340
- // 如果还没开始初始化且有配置,主动触发
4341
- if (!this.isInitializingSDK && this.options.sdkConfig) {
4342
- console.log('[CreditPackageModal] Starting SDK initialization...');
4343
- await this.initializeSDK();
4344
- if (this.sdkInitialized) {
4345
- return true;
4346
- }
4347
- }
4348
- // 等待初始化完成
4349
- console.log('[CreditPackageModal] Waiting for SDK initialization...');
4350
- while (Date.now() - startTime < timeout) {
4351
- if (this.sdkInitialized) {
4352
- console.log('[CreditPackageModal] SDK initialization completed');
4353
- return true;
4354
- }
4355
- if (!this.isInitializingSDK) {
4356
- // 初始化已结束但未成功,尝试重试
4357
- if (maxRetries > 0) {
4358
- console.log(`[CreditPackageModal] SDK initialization failed, retrying... (${maxRetries} retries left)`);
4359
- await this.initializeSDK();
4360
- return this.waitForSDKInitialization(timeout - (Date.now() - startTime), maxRetries - 1);
4361
- }
4362
- else {
4363
- console.error('[CreditPackageModal] SDK initialization failed after all retries');
4364
- return false;
4365
- }
4366
- }
4367
- // 等待 100ms 后重试
4368
- await new Promise(resolve => setTimeout(resolve, 100));
4369
- }
4370
- // 超时
4371
- console.error('[CreditPackageModal] SDK initialization timed out');
4372
- return false;
4265
+ close() {
4266
+ console.log(`[${this.constructor.name}] Closing modal...`);
4267
+ this.modal.close();
4373
4268
  }
4374
4269
  /**
4375
- * 初始化支付SDK(后台静默执行)
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)
4376
4286
  */
4377
4287
  async initializeSDK() {
4378
4288
  if (this.isInitializingSDK || this.sdkInitialized) {
4379
4289
  return;
4380
4290
  }
4381
4291
  if (!this.options.sdkConfig) {
4382
- console.log('[CreditPackageModal] No SDK configuration provided, skipping initialization');
4292
+ console.log(`[${this.constructor.name}] No SDK configuration provided, skipping initialization`);
4383
4293
  return;
4384
4294
  }
4385
4295
  this.isInitializingSDK = true;
4386
- console.log('[CreditPackageModal] Initializing payment SDK...');
4387
- // 显示加载指示器
4296
+ console.log(`[${this.constructor.name}] Initializing payment SDK...`);
4297
+ // Show loading indicator
4388
4298
  const loader = showLoadingIndicator('Initializing payment system...');
4389
4299
  try {
4390
4300
  const config = this.options.sdkConfig;
4391
- // 1. 从环境配置中获取基础配置
4301
+ // 1. Get base configuration from environment
4392
4302
  const envConfig = ENVIRONMENT_CONFIGS[config.environment];
4393
- // 2. 合并配置(自定义配置优先级高于环境配置)
4303
+ // 2. Merge configuration (custom config has higher priority)
4394
4304
  const finalConfig = {
4395
4305
  scriptUrl: config.scriptUrl || envConfig.scriptUrl,
4396
4306
  clientId: config.clientId || envConfig.clientId,
4397
4307
  orderApiUrl: config.orderApiUrl || envConfig.orderApiUrl,
4398
4308
  cssUrl: config.cssUrl || envConfig.cssUrl,
4399
4309
  };
4400
- console.log('[CreditPackageModal] Using environment:', config.environment);
4401
- // 3. 初始化 SeaartPaymentSDK
4310
+ console.log(`[${this.constructor.name}] Using environment:`, config.environment);
4311
+ // 3. Initialize SeaartPaymentSDK
4402
4312
  await SeaartPaymentSDK.getInstance().init({
4403
4313
  scriptUrl: finalConfig.scriptUrl,
4404
4314
  clientId: finalConfig.clientId,
@@ -4406,84 +4316,388 @@ class CreditPackageModal {
4406
4316
  scriptTimeout: config.scriptTimeout,
4407
4317
  cssUrl: finalConfig.cssUrl,
4408
4318
  });
4409
- // 4. 获取支付方式列表
4319
+ // 4. Get payment methods list
4410
4320
  const paymentMethods = await SeaartPaymentSDK.getInstance().getPaymentMethods({
4411
4321
  country_code: config.countryCode,
4412
- business_type: config.businessType ?? 1, // 默认为 1(一次性购买)
4322
+ business_type: config.businessType ?? 1, // Default to 1 (one-time purchase)
4413
4323
  });
4414
- // 5. 查找匹配的支付方式
4324
+ // 5. Find matching payment method
4415
4325
  const paymentMethod = config.paymentMethodType
4416
4326
  ? paymentMethods.find((m) => m.payment_method_type === config.paymentMethodType ||
4417
4327
  m.payment_method_name.toLowerCase().includes(config.paymentMethodType.toLowerCase()))
4418
- : paymentMethods.find((m) => m.payment_type === 2); // 默认使用 dropin (payment_type === 2)
4328
+ : paymentMethods.find((m) => m.payment_type === 2); // Default to dropin (payment_type === 2)
4419
4329
  if (!paymentMethod) {
4420
4330
  throw new Error(`Payment method "${config.paymentMethodType || 'dropin'}" not found`);
4421
4331
  }
4422
- // 6. 存储到类成员变量(包括 finalConfig 以供后续使用)
4423
- this.options.paymentMethod = paymentMethod;
4424
- this.options.accountToken = config.accountToken;
4332
+ // 6. Store to class members (including finalConfig for later use)
4333
+ this.paymentMethod = paymentMethod;
4334
+ this.accountToken = config.accountToken;
4425
4335
  this.sdkInitialized = true;
4426
- // 存储最终配置到 config 对象(用于 handlePaymentFlow
4336
+ // Store final config to config object (for handlePaymentFlow)
4427
4337
  this.options.sdkConfig._resolvedOrderApiUrl = finalConfig.orderApiUrl;
4428
- console.log('[CreditPackageModal] SDK initialized with environment config:', {
4338
+ console.log(`[${this.constructor.name}] SDK initialized with environment config:`, {
4429
4339
  environment: config.environment,
4430
4340
  paymentMethod: paymentMethod.payment_method_name,
4431
4341
  accountToken: config.accountToken ? 'provided' : 'not provided',
4432
4342
  });
4433
4343
  }
4434
4344
  catch (error) {
4435
- console.error('[CreditPackageModal] Failed to initialize payment SDK:', error);
4436
- // SDK 初始化失败不影响浏览积分包,只是无法进行支付
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
4437
4347
  }
4438
4348
  finally {
4439
4349
  this.isInitializingSDK = false;
4440
- // 隐藏加载指示器
4350
+ // Hide loading indicator
4441
4351
  hideLoadingIndicator(loader);
4442
4352
  }
4443
4353
  }
4444
4354
  /**
4445
- * 关闭弹框
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
4446
4359
  */
4447
- close() {
4448
- console.log('[CreditPackageModal] Closing modal...');
4449
- this.modal.close();
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;
4450
4400
  }
4451
4401
  /**
4452
- * 获取响应式样式配置
4402
+ * Handle payment flow (order creation + payment modal)
4453
4403
  */
4454
- getResponsiveStyles() {
4455
- const isMobile = window.matchMedia('(max-width: 768px)').matches;
4456
- const isTablet = window.matchMedia('(max-width: 1200px)').matches;
4457
- const isLaptop = window.matchMedia('(max-width: 1400px)').matches;
4458
- let computeColumns = 5;
4459
- let packColumns = 4;
4460
- let padding = '0 60px 60px';
4461
- if (isMobile) {
4462
- computeColumns = 1;
4463
- packColumns = 1;
4464
- padding = '0 20px 20px';
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
4509
+ */
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
+ });
4546
+ }
4547
+ });
4548
+ }
4549
+ /**
4550
+ * Cleanup resources
4551
+ */
4552
+ cleanup() {
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;
4558
+ }
4559
+ }
4560
+ // === Utility Methods ===
4561
+ /**
4562
+ * Format number with commas
4563
+ */
4564
+ formatNumber(num) {
4565
+ return parseInt(num).toLocaleString();
4566
+ }
4567
+ /**
4568
+ * Get content container from modal
4569
+ */
4570
+ getContentContainer() {
4571
+ return this.modal.getContentContainer();
4572
+ }
4573
+ }
4574
+
4575
+ /**
4576
+ * CreditPackageModal - 积分套餐选择弹框
4577
+ * 展示不同的积分套餐供用户选择
4578
+ */
4579
+ class CreditPackageModal extends BasePackageModal {
4580
+ constructor() {
4581
+ super(...arguments);
4582
+ // Design system constants
4583
+ this.SPACING = {
4584
+ xs: '8px',
4585
+ sm: '16px',
4586
+ md: '24px',
4587
+ lg: '32px',
4588
+ xl: '48px',
4589
+ };
4590
+ this.COLORS = {
4591
+ text: {
4592
+ primary: 'rgba(255, 255, 255, 0.95)',
4593
+ secondary: 'rgba(255, 255, 255, 0.65)',
4594
+ tertiary: 'rgba(255, 255, 255, 0.5)',
4595
+ },
4596
+ green: {
4597
+ primary: '#00ff88',
4598
+ secondary: '#00f2fe',
4599
+ accent: '#22ce9c',
4600
+ },
4601
+ };
4602
+ }
4603
+ // === Abstract Method Implementations ===
4604
+ /**
4605
+ * Create and configure the PaymentModal instance
4606
+ */
4607
+ createModal() {
4608
+ return new PaymentModal({
4609
+ title: this.language === 'zh-CN'
4610
+ ? (this.options.title_cn || '选择您的创作力量')
4611
+ : (this.options.title || 'Choose Your Creative Power'),
4612
+ showCloseButton: true,
4613
+ closeOnOverlayClick: false, // Disable click overlay to close
4614
+ closeOnEsc: false, // Disable ESC key to close
4615
+ maxWidth: '1200px',
4616
+ onClose: () => {
4617
+ this.cleanup();
4618
+ this.options.onClose?.();
4619
+ },
4620
+ });
4621
+ }
4622
+ /**
4623
+ * Get packages to display
4624
+ */
4625
+ getPackages() {
4626
+ return CREDIT_PACKAGES;
4627
+ }
4628
+ /**
4629
+ * Get package display name for payment modal title
4630
+ */
4631
+ getPackageDisplayName(pkg) {
4632
+ const isZh = this.language === 'zh-CN';
4633
+ return isZh ? `${pkg.credits} 积分套餐` : `${pkg.credits} Credits Package`;
4634
+ }
4635
+ /**
4636
+ * Get loading button HTML with spinner
4637
+ */
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
+ `;
4652
+ }
4653
+ /**
4654
+ * Apply modal styling (hook method override)
4655
+ */
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)';
4465
4662
  }
4466
- else if (isTablet) {
4467
- computeColumns = 2;
4468
- packColumns = 2;
4469
- 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';
4470
4672
  }
4471
- else if (isLaptop) {
4472
- computeColumns = 3;
4473
- packColumns = 2;
4474
- 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());
4475
4694
  }
4476
- return {
4477
- containerPadding: padding,
4478
- computeGridColumns: `repeat(${computeColumns}, 1fr)`,
4479
- packGridColumns: `repeat(${packColumns}, 1fr)`,
4480
- };
4481
4695
  }
4482
4696
  /**
4483
- * 渲染弹框内容
4697
+ * Render modal content
4484
4698
  */
4485
4699
  renderContent() {
4486
- const container = this.modal.getContentContainer();
4700
+ const container = this.getContentContainer();
4487
4701
  if (!container) {
4488
4702
  throw new Error('Modal content container not found');
4489
4703
  }
@@ -4496,7 +4710,7 @@ class CreditPackageModal {
4496
4710
  color: white;
4497
4711
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
4498
4712
  ">
4499
- <!-- 副标题 -->
4713
+ <!-- Subtitle -->
4500
4714
  <p style="
4501
4715
  text-align: center;
4502
4716
  font-size: 16px;
@@ -4506,11 +4720,11 @@ class CreditPackageModal {
4506
4720
  font-weight: 400;
4507
4721
  ">
4508
4722
  ${isZh
4509
- ? (this.options.subtitle_cn || '免费开始,随创作扩展。所有套餐都包含用于电影、游戏、音乐和世界的算力积分。')
4723
+ ? (this.options.subtitle_cn || '免费开始,随创作扩展。所有套餐都包含用于电影、游戏、音乐和世界的算力积分。')
4510
4724
  : (this.options.subtitle || 'Start free, scale with creation. All packages include credits for movies, games, music, and worlds.')}
4511
4725
  </p>
4512
4726
 
4513
- <!-- 算力积分说明区域 - 完全复刻 next-meta pricing -->
4727
+ <!-- Compute credits section - fully replicate next-meta pricing -->
4514
4728
  <div style="margin-bottom: ${this.SPACING.xl};">
4515
4729
  <!-- Section Header -->
4516
4730
  <div style="margin-bottom: ${this.SPACING.xl}; text-align: center;">
@@ -4534,221 +4748,18 @@ class CreditPackageModal {
4534
4748
  </p>
4535
4749
  </div>
4536
4750
 
4537
- <!-- Credits Grid - 完全复刻 next-meta 5 个卡片 -->
4751
+ <!-- Credits Grid - fully replicate next-meta 5 cards -->
4538
4752
  <div style="
4539
4753
  display: grid;
4540
4754
  grid-template-columns: ${styles.computeGridColumns};
4541
4755
  gap: 12px;
4542
4756
  margin-bottom: 20px;
4543
4757
  ">
4544
- <!-- Film Clips -->
4545
- <article style="
4546
- position: relative;
4547
- overflow: hidden;
4548
- border-radius: 16px;
4549
- border: 1px solid rgba(255, 255, 255, 0.06);
4550
- background: rgba(255, 255, 255, 0.05);
4551
- padding: 20px;
4552
- text-align: center;
4553
- backdrop-filter: blur(40px);
4554
- opacity: 0.7;
4555
- transform: scale(0.98);
4556
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
4557
- " 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)';"
4558
- 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';">
4559
- <div style="
4560
- margin: 0 auto 16px;
4561
- display: flex;
4562
- align-items: center;
4563
- justify-content: center;
4564
- height: 48px;
4565
- width: 48px;
4566
- border-radius: 12px;
4567
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
4568
- box-shadow: 0 8px 20px rgba(0,0,0,0.2);
4569
- ">
4570
- <svg style="height: 24px; width: 24px; color: white;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4571
- <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"/>
4572
- </svg>
4573
- </div>
4574
- <h3 style="margin-bottom: 8px; font-size: 16px; font-weight: 600; color: white;">
4575
- ${isZh ? '电影片段' : 'Film Clips'}
4576
- </h3>
4577
- <p style="margin-bottom: 8px; font-family: 'Monaco', 'Menlo', monospace; font-size: 20px; font-weight: 800; color: #00ff88;">
4578
- 100-300
4579
- </p>
4580
- <p style="font-size: 12px; line-height: 1.5; color: rgba(255, 255, 255, 0.5);">
4581
- ${isZh ? '每次生成' : 'per generation'}
4582
- </p>
4583
- </article>
4584
-
4585
- <!-- Game Scenes -->
4586
- <article style="
4587
- position: relative;
4588
- overflow: hidden;
4589
- border-radius: 16px;
4590
- border: 1px solid rgba(255, 255, 255, 0.06);
4591
- background: rgba(255, 255, 255, 0.05);
4592
- padding: 20px;
4593
- text-align: center;
4594
- backdrop-filter: blur(40px);
4595
- opacity: 0.7;
4596
- transform: scale(0.98);
4597
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
4598
- " 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)';"
4599
- 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';">
4600
- <div style="
4601
- margin: 0 auto 16px;
4602
- display: flex;
4603
- align-items: center;
4604
- justify-content: center;
4605
- height: 48px;
4606
- width: 48px;
4607
- border-radius: 12px;
4608
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
4609
- box-shadow: 0 8px 20px rgba(0,0,0,0.2);
4610
- ">
4611
- <svg style="height: 24px; width: 24px; color: white;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4612
- <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"/>
4613
- </svg>
4614
- </div>
4615
- <h3 style="margin-bottom: 8px; font-size: 16px; font-weight: 600; color: white;">
4616
- ${isZh ? '游戏场景' : 'Game Scenes'}
4617
- </h3>
4618
- <p style="margin-bottom: 8px; font-family: 'Monaco', 'Menlo', monospace; font-size: 20px; font-weight: 800; color: #00ff88;">
4619
- 50-200
4620
- </p>
4621
- <p style="font-size: 12px; line-height: 1.5; color: rgba(255, 255, 255, 0.5);">
4622
- ${isZh ? '每个场景' : 'per scene'}
4623
- </p>
4624
- </article>
4625
-
4626
- <!-- Music -->
4627
- <article style="
4628
- position: relative;
4629
- overflow: hidden;
4630
- border-radius: 16px;
4631
- border: 1px solid rgba(255, 255, 255, 0.06);
4632
- background: rgba(255, 255, 255, 0.05);
4633
- padding: 20px;
4634
- text-align: center;
4635
- backdrop-filter: blur(40px);
4636
- opacity: 0.7;
4637
- transform: scale(0.98);
4638
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
4639
- " 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)';"
4640
- 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';">
4641
- <div style="
4642
- margin: 0 auto 16px;
4643
- display: flex;
4644
- align-items: center;
4645
- justify-content: center;
4646
- height: 48px;
4647
- width: 48px;
4648
- border-radius: 12px;
4649
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
4650
- box-shadow: 0 8px 20px rgba(0,0,0,0.2);
4651
- ">
4652
- <svg style="height: 24px; width: 24px; color: white;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4653
- <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"/>
4654
- </svg>
4655
- </div>
4656
- <h3 style="margin-bottom: 8px; font-size: 16px; font-weight: 600; color: white;">
4657
- ${isZh ? '音乐' : 'Music'}
4658
- </h3>
4659
- <p style="margin-bottom: 8px; font-family: 'Monaco', 'Menlo', monospace; font-size: 20px; font-weight: 800; color: #00ff88;">
4660
- 30-100
4661
- </p>
4662
- <p style="font-size: 12px; line-height: 1.5; color: rgba(255, 255, 255, 0.5);">
4663
- ${isZh ? '每首曲目' : 'per track'}
4664
- </p>
4665
- </article>
4666
-
4667
- <!-- 3D Worlds -->
4668
- <article style="
4669
- position: relative;
4670
- overflow: hidden;
4671
- border-radius: 16px;
4672
- border: 1px solid rgba(255, 255, 255, 0.06);
4673
- background: rgba(255, 255, 255, 0.05);
4674
- padding: 20px;
4675
- text-align: center;
4676
- backdrop-filter: blur(40px);
4677
- opacity: 0.7;
4678
- transform: scale(0.98);
4679
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
4680
- " 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)';"
4681
- 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';">
4682
- <div style="
4683
- margin: 0 auto 16px;
4684
- display: flex;
4685
- align-items: center;
4686
- justify-content: center;
4687
- height: 48px;
4688
- width: 48px;
4689
- border-radius: 12px;
4690
- background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
4691
- box-shadow: 0 8px 20px rgba(0,0,0,0.2);
4692
- ">
4693
- <svg style="height: 24px; width: 24px; color: white;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4694
- <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"/>
4695
- </svg>
4696
- </div>
4697
- <h3 style="margin-bottom: 8px; font-size: 16px; font-weight: 600; color: white;">
4698
- ${isZh ? '3D 世界' : '3D Worlds'}
4699
- </h3>
4700
- <p style="margin-bottom: 8px; font-family: 'Monaco', 'Menlo', monospace; font-size: 20px; font-weight: 800; color: #00ff88;">
4701
- 150-500
4702
- </p>
4703
- <p style="font-size: 12px; line-height: 1.5; color: rgba(255, 255, 255, 0.5);">
4704
- ${isZh ? '每个世界' : 'per world'}
4705
- </p>
4706
- </article>
4707
-
4708
- <!-- Agent Sessions -->
4709
- <article style="
4710
- position: relative;
4711
- overflow: hidden;
4712
- border-radius: 16px;
4713
- border: 1px solid rgba(255, 255, 255, 0.06);
4714
- background: rgba(255, 255, 255, 0.05);
4715
- padding: 20px;
4716
- text-align: center;
4717
- backdrop-filter: blur(40px);
4718
- opacity: 0.7;
4719
- transform: scale(0.98);
4720
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
4721
- " 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)';"
4722
- 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';">
4723
- <div style="
4724
- margin: 0 auto 16px;
4725
- display: flex;
4726
- align-items: center;
4727
- justify-content: center;
4728
- height: 48px;
4729
- width: 48px;
4730
- border-radius: 12px;
4731
- background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
4732
- box-shadow: 0 8px 20px rgba(0,0,0,0.2);
4733
- ">
4734
- <svg style="height: 24px; width: 24px; color: white;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4735
- <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"/>
4736
- </svg>
4737
- </div>
4738
- <h3 style="margin-bottom: 8px; font-size: 16px; font-weight: 600; color: white;">
4739
- ${isZh ? 'AI 会话' : 'Agent Sessions'}
4740
- </h3>
4741
- <p style="margin-bottom: 8px; font-family: 'Monaco', 'Menlo', monospace; font-size: 20px; font-weight: 800; color: #00ff88;">
4742
- 10
4743
- </p>
4744
- <p style="font-size: 12px; line-height: 1.5; color: rgba(255, 255, 255, 0.5);">
4745
- ${isZh ? '每次会话' : 'per session'}
4746
- </p>
4747
- </article>
4758
+ ${this.renderComputeCreditsCards()}
4748
4759
  </div>
4749
4760
  </div>
4750
4761
 
4751
- <!-- Credit Packs - 积分包购买标题 -->
4762
+ <!-- Credit Packs - credit package purchase title -->
4752
4763
  <div style="margin: 0 auto ${this.SPACING.lg}; max-width: 80rem; text-align: center;">
4753
4764
  <h3 style="
4754
4765
  margin-bottom: ${this.SPACING.xs};
@@ -4771,7 +4782,7 @@ class CreditPackageModal {
4771
4782
  </p>
4772
4783
  </div>
4773
4784
 
4774
- <!-- 套餐卡片 -->
4785
+ <!-- Package cards -->
4775
4786
  <div style="
4776
4787
  display: grid;
4777
4788
  grid-template-columns: ${styles.packGridColumns};
@@ -4781,17 +4792,133 @@ class CreditPackageModal {
4781
4792
  </div>
4782
4793
  </div>
4783
4794
  `;
4784
- // 添加点击事件监听
4795
+ // Attach event listeners
4785
4796
  this.attachEventListeners(container);
4786
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
+ }
4787
4914
  /**
4788
- * 渲染套餐卡片
4915
+ * Render package card
4789
4916
  */
4790
4917
  renderPackageCard(pkg, index) {
4791
4918
  const isZh = this.language === 'zh-CN';
4792
4919
  const isPopular = pkg.is_popular;
4793
- const hasBonus = parseInt(pkg.bonus_credits) > 0;
4794
- // Popular包的呼吸动画
4920
+ const hasBonus = pkg.bonus_credits && parseInt(pkg.bonus_credits) > 0;
4921
+ // Popular package breathing animation
4795
4922
  const pulseAnimation = isPopular ? `
4796
4923
  <style>
4797
4924
  @keyframes pulse-${pkg.id} {
@@ -4851,7 +4978,7 @@ class CreditPackageModal {
4851
4978
  </div>
4852
4979
  ` : ''}
4853
4980
 
4854
- <!-- 积分总数 -->
4981
+ <!-- Total credits -->
4855
4982
  <div style="
4856
4983
  margin-bottom: 8px;
4857
4984
  font-size: 36px;
@@ -4863,7 +4990,7 @@ class CreditPackageModal {
4863
4990
  ${this.formatNumber(pkg.credits)}
4864
4991
  </div>
4865
4992
 
4866
- <!-- 积分明细 -->
4993
+ <!-- Credits breakdown -->
4867
4994
  <div style="
4868
4995
  margin-bottom: 12px;
4869
4996
  display: flex;
@@ -4877,7 +5004,7 @@ class CreditPackageModal {
4877
5004
  font-weight: 500;
4878
5005
  color: hsl(240, 5%, 65%);
4879
5006
  ">
4880
- ${this.formatNumber(pkg.base_credits)} ${isZh ? '积分' : 'Credits'}
5007
+ ${this.formatNumber(pkg.base_credits || pkg.credits)} ${isZh ? '积分' : 'Credits'}
4881
5008
  </span>
4882
5009
  ${hasBonus ? `
4883
5010
  <span style="
@@ -4893,7 +5020,7 @@ class CreditPackageModal {
4893
5020
  ` : ''}
4894
5021
  </div>
4895
5022
 
4896
- <!-- 购买按钮 -->
5023
+ <!-- Buy button -->
4897
5024
  <button
4898
5025
  data-package-button="${pkg.id}"
4899
5026
  style="
@@ -4918,202 +5045,6 @@ class CreditPackageModal {
4918
5045
  </div>
4919
5046
  `;
4920
5047
  }
4921
- /**
4922
- * 格式化数字(添加逗号)
4923
- */
4924
- formatNumber(num) {
4925
- return parseInt(num).toLocaleString();
4926
- }
4927
- /**
4928
- * 获取加载按钮的 HTML(带旋转动画)
4929
- * @param text 加载文本
4930
- * @param isPopular 是否为 Popular 套餐(用于调整颜色)
4931
- */
4932
- getLoadingButtonHTML(text, isPopular = false) {
4933
- return `
4934
- <svg style="
4935
- display: inline-block;
4936
- width: 16px;
4937
- height: 16px;
4938
- border: 2px solid ${isPopular ? 'rgba(10, 10, 15, 0.3)' : 'rgba(255, 255, 255, 0.3)'};
4939
- border-top-color: ${isPopular ? '#0a0a0f' : 'white'};
4940
- border-radius: 50%;
4941
- animation: spin 0.6s linear infinite;
4942
- " viewBox="0 0 24 24"></svg>
4943
- <style>@keyframes spin { to { transform: rotate(360deg); } }</style>
4944
- ${text ? `<span style="margin-left: 8px;">${text}</span>` : ''}
4945
- `;
4946
- }
4947
- /**
4948
- * 添加事件监听
4949
- */
4950
- attachEventListeners(container) {
4951
- // 为每个套餐按钮添加点击事件
4952
- CREDIT_PACKAGES.forEach(pkg => {
4953
- const button = container.querySelector(`[data-package-button="${pkg.id}"]`);
4954
- if (button) {
4955
- button.addEventListener('click', async (e) => {
4956
- e.preventDefault();
4957
- e.stopPropagation();
4958
- console.log('[CreditPackageModal] Package selected:', pkg.id);
4959
- // 保存原始按钮文本
4960
- const originalText = button.innerHTML;
4961
- const originalDisabled = button.disabled;
4962
- try {
4963
- // 禁用按钮,显示初始化状态
4964
- button.disabled = true;
4965
- const isZh = this.language === 'zh-CN';
4966
- button.innerHTML = this.getLoadingButtonHTML(isZh ? '初始化中...' : 'Initializing...', pkg.is_popular);
4967
- // 等待 SDK 初始化(支持重试)
4968
- const initialized = await this.waitForSDKInitialization(30000, 1);
4969
- if (!initialized) {
4970
- throw new Error('SDK initialization failed or timed out. Please try again.');
4971
- }
4972
- // SDK 初始化成功,执行支付流程
4973
- await this.handlePaymentFlow(pkg, button, originalText);
4974
- }
4975
- catch (error) {
4976
- console.error('[CreditPackageModal] Failed to process payment:', error);
4977
- // 恢复按钮状态
4978
- button.disabled = originalDisabled;
4979
- button.innerHTML = originalText;
4980
- // 显示错误提示(使用自定义 UI 替代 alert)
4981
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
4982
- showErrorMessage(`Payment failed: ${errorMessage}`);
4983
- // 触发失败回调
4984
- this.options.onPaymentFailed?.(error instanceof Error ? error : new Error(String(error)));
4985
- }
4986
- });
4987
- }
4988
- });
4989
- }
4990
- /**
4991
- * 处理支付流程
4992
- */
4993
- async handlePaymentFlow(pkg, button, originalHTML) {
4994
- try {
4995
- // 更新按钮状态为"创建订单中"
4996
- button.innerHTML = this.getLoadingButtonHTML('Creating order...', pkg.is_popular);
4997
- console.log('[CreditPackageModal] Creating order for package:', pkg.id);
4998
- // 使用默认实现:调用解析后的 orderApiUrl
4999
- const resolvedOrderApiUrl = this.options.sdkConfig._resolvedOrderApiUrl || ENVIRONMENT_CONFIGS[this.options.sdkConfig.environment].orderApiUrl;
5000
- const response = await createOrder(resolvedOrderApiUrl, this.options.sdkConfig.accountToken || '', {
5001
- product_id: pkg.id,
5002
- purchase_type: this.options.sdkConfig.businessType ?? 1, // 默认为 1
5003
- });
5004
- console.log('[CreditPackageModal] Create order response:', response);
5005
- if (!response || !response.transaction_id) {
5006
- throw new Error('Failed to create order: Invalid response from API');
5007
- }
5008
- const orderId = response.transaction_id;
5009
- console.log('[CreditPackageModal] Order created:', orderId);
5010
- // 恢复按钮状态
5011
- button.disabled = false;
5012
- button.innerHTML = originalHTML;
5013
- // 创建并打开支付弹框
5014
- await this.openPaymentModal(orderId, pkg);
5015
- }
5016
- catch (error) {
5017
- console.error('[CreditPackageModal] Payment flow failed:', error);
5018
- // 恢复按钮状态
5019
- button.disabled = false;
5020
- button.innerHTML = originalHTML;
5021
- // 显示错误提示(使用自定义 UI 替代 alert)
5022
- showErrorMessage(`Payment failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
5023
- // 触发失败回调
5024
- this.options.onPaymentFailed?.(error instanceof Error ? error : new Error(String(error)));
5025
- }
5026
- }
5027
- /**
5028
- * 打开支付弹框
5029
- */
5030
- async openPaymentModal(orderId, pkg) {
5031
- if (!this.options.paymentMethod) {
5032
- throw new Error('Payment method not configured');
5033
- }
5034
- const isZh = this.language === 'zh-CN';
5035
- // 创建支付实例(使用 orderId)
5036
- let paymentInstance;
5037
- if (this.options.sdkConfig || this.sdkInitialized) {
5038
- // 方案A:使用 sdkConfig 初始化后,创建支付实例
5039
- paymentInstance = window.SeaartPaymentComponent.createPayment({
5040
- sys_order_id: orderId,
5041
- account_token: this.options.accountToken,
5042
- });
5043
- }
5044
- else {
5045
- throw new Error('Payment SDK not initialized. Please provide sdkConfig or paymentSDK.');
5046
- }
5047
- const dropinModal = new DropinPaymentModal(paymentInstance, orderId, this.options.accountToken, this.options.paymentMethod, {
5048
- modalTitle: isZh ? `购买 ${pkg.credits} 积分` : `Purchase ${pkg.credits} Credits`,
5049
- onCompleted: (payload) => {
5050
- console.log('[CreditPackageModal] Payment completed:', payload);
5051
- this.close();
5052
- // 显示购买成功弹窗
5053
- const successModal = new PurchaseSuccessModal({
5054
- data: {
5055
- packName: isZh ? `${pkg.credits} 积分套餐` : `${pkg.credits} Credits Package`,
5056
- credits: parseInt(pkg.credits),
5057
- amount: pkg.price,
5058
- currency: '$',
5059
- orderId: orderId,
5060
- transactionId: payload.transaction_id,
5061
- },
5062
- language: this.options.language,
5063
- onClose: () => {
5064
- // 弹窗关闭后刷新积分
5065
- (async () => {
5066
- try {
5067
- const envConfig = ENVIRONMENT_CONFIGS[this.options.sdkConfig.environment];
5068
- const walletApiUrl = envConfig.walletApiUrl;
5069
- console.log('[CreditPackageModal] Refreshing credits from:', walletApiUrl);
5070
- const creditDetail = await getCreditDetail(walletApiUrl, this.options.sdkConfig.accountToken);
5071
- if (creditDetail) {
5072
- console.log('[CreditPackageModal] Credits refreshed, total balance:', creditDetail.total_balance);
5073
- }
5074
- else {
5075
- console.warn('[CreditPackageModal] Failed to refresh credits');
5076
- }
5077
- }
5078
- catch (error) {
5079
- console.error('[CreditPackageModal] Failed to refresh credits:', error);
5080
- }
5081
- })();
5082
- // 触发用户回调
5083
- this.options.onPaymentSuccess?.(orderId, payload.transaction_id);
5084
- },
5085
- });
5086
- successModal.open();
5087
- },
5088
- onFailed: (payload) => {
5089
- console.error('[CreditPackageModal] Payment failed:', payload);
5090
- const error = new Error(payload.message || 'Payment failed');
5091
- this.options.onPaymentFailed?.(error);
5092
- },
5093
- onError: (payload, error) => {
5094
- console.error('[CreditPackageModal] Payment error:', error);
5095
- this.options.onPaymentFailed?.(error);
5096
- },
5097
- });
5098
- await dropinModal.open();
5099
- }
5100
- /**
5101
- * 清理资源
5102
- */
5103
- cleanup() {
5104
- console.log('[CreditPackageModal] Cleaning up...');
5105
- // 移除resize监听器
5106
- if (this.resizeHandler) {
5107
- window.removeEventListener('resize', this.resizeHandler);
5108
- this.resizeHandler = null;
5109
- }
5110
- }
5111
- /**
5112
- * 检查弹框是否打开
5113
- */
5114
- isOpen() {
5115
- return this.modal.isModalOpen();
5116
- }
5117
5048
  }
5118
5049
 
5119
5050
  /**
@@ -5121,46 +5052,75 @@ class CreditPackageModal {
5121
5052
  * 支持多种套餐类型(破冰包、告急包、首充包等)
5122
5053
  * 套餐数据从外部配置传入,无硬编码
5123
5054
  */
5124
- class GenericPackageModal {
5055
+ class GenericPackageModal extends BasePackageModal {
5125
5056
  constructor(options) {
5126
- this.resizeHandler = null;
5127
- this.isInitializingSDK = false;
5128
- this.sdkInitialized = false;
5129
- // 验证必填字段
5057
+ // Validate required fields
5130
5058
  if (!options.packages || options.packages.length === 0) {
5131
5059
  throw new Error('GenericPackageModal: packages array is required and cannot be empty');
5132
5060
  }
5133
- this.options = options;
5134
- this.language = options.language || 'en';
5135
- // 创建弹框 - 根据套餐数量动态调整宽度
5136
- const packageCount = options.packages.length;
5137
- 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)
5138
5071
  if (packageCount === 2) {
5139
5072
  maxWidth = '1100px';
5140
5073
  }
5141
5074
  else if (packageCount >= 3) {
5142
5075
  maxWidth = '1200px';
5143
5076
  }
5144
- this.modal = new PaymentModal({
5145
- title: '', // 不显示标题
5146
- showCloseButton: false, // 不显示弹框关闭按钮,使用卡片内的关闭按钮
5147
- closeOnOverlayClick: false, // 禁用点击空白处关闭
5148
- 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
5149
5082
  maxWidth: maxWidth,
5150
5083
  onClose: () => {
5151
5084
  this.cleanup();
5152
- options.onClose?.();
5085
+ this.options.onClose?.();
5153
5086
  },
5154
5087
  });
5155
- console.log('[GenericPackageModal] Created with', options.packages.length, 'packages');
5156
5088
  }
5157
5089
  /**
5158
- * 打开弹框
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)
5159
5121
  */
5160
- async open() {
5161
- console.log('[GenericPackageModal] Opening modal...');
5162
- this.modal.open();
5163
- // 修改弹框样式 - 让卡片完全填充
5122
+ applyModalStyling() {
5123
+ // Modify modal style - make card fully fill
5164
5124
  const modalElement = document.querySelector('.payment-modal');
5165
5125
  if (modalElement) {
5166
5126
  modalElement.style.background = 'transparent';
@@ -5169,176 +5129,41 @@ class GenericPackageModal {
5169
5129
  modalElement.style.borderRadius = '16px';
5170
5130
  modalElement.style.overflow = 'hidden';
5171
5131
  }
5172
- // 修改内容容器样式 - 移除padding
5132
+ // Modify content container style - remove padding
5173
5133
  const contentElement = document.querySelector('.payment-modal-content');
5174
5134
  if (contentElement) {
5175
5135
  contentElement.style.padding = '0';
5176
5136
  contentElement.style.margin = '0';
5177
5137
  }
5178
- // 监听卡片内的关闭按钮事件
5179
- const container = this.modal.getContentContainer();
5138
+ // Listen for close button event from card
5139
+ const container = this.getContentContainer();
5180
5140
  if (container) {
5181
5141
  container.addEventListener('close-modal', () => {
5182
5142
  this.close();
5183
5143
  });
5184
5144
  }
5185
- // 渲染内容
5186
- this.renderContent();
5187
- // 添加resize监听器,窗口大小改变时重新渲染以应用响应式样式
5188
- this.resizeHandler = () => {
5189
- this.renderContent();
5190
- };
5191
- window.addEventListener('resize', this.resizeHandler);
5192
- // 如果配置了 sdkConfig,在后台自动初始化 SDK
5193
- if (!this.sdkInitialized && !this.isInitializingSDK) {
5194
- this.initializeSDK();
5195
- }
5196
- console.log('[GenericPackageModal] Modal opened');
5197
- }
5198
- /**
5199
- * 等待 SDK 初始化完成(支持超时和重试)
5200
- * @param timeout 超时时间(毫秒),默认 30 秒
5201
- * @param maxRetries 最大重试次数,默认 1 次
5202
- * @returns 是否初始化成功
5203
- */
5204
- async waitForSDKInitialization(timeout = 30000, maxRetries = 1) {
5205
- const startTime = Date.now();
5206
- // 如果已经初始化完成,直接返回
5207
- if (this.sdkInitialized) {
5208
- console.log('[GenericPackageModal] SDK already initialized');
5209
- return true;
5210
- }
5211
- // 如果还没开始初始化且有配置,主动触发
5212
- if (!this.isInitializingSDK && this.options.sdkConfig) {
5213
- console.log('[GenericPackageModal] Starting SDK initialization...');
5214
- await this.initializeSDK();
5215
- if (this.sdkInitialized) {
5216
- return true;
5217
- }
5218
- }
5219
- // 等待初始化完成
5220
- console.log('[GenericPackageModal] Waiting for SDK initialization...');
5221
- while (Date.now() - startTime < timeout) {
5222
- if (this.sdkInitialized) {
5223
- console.log('[GenericPackageModal] SDK initialization completed');
5224
- return true;
5225
- }
5226
- if (!this.isInitializingSDK) {
5227
- // 初始化已结束但未成功,尝试重试
5228
- if (maxRetries > 0) {
5229
- console.log(`[GenericPackageModal] SDK initialization failed, retrying... (${maxRetries} retries left)`);
5230
- await this.initializeSDK();
5231
- return this.waitForSDKInitialization(timeout - (Date.now() - startTime), maxRetries - 1);
5232
- }
5233
- else {
5234
- console.error('[GenericPackageModal] SDK initialization failed after all retries');
5235
- return false;
5236
- }
5237
- }
5238
- // 等待 100ms 后重试
5239
- await new Promise(resolve => setTimeout(resolve, 100));
5240
- }
5241
- // 超时
5242
- console.error('[GenericPackageModal] SDK initialization timed out');
5243
- return false;
5244
- }
5245
- /**
5246
- * 初始化支付SDK(后台静默执行)
5247
- */
5248
- async initializeSDK() {
5249
- if (this.isInitializingSDK || this.sdkInitialized) {
5250
- return;
5251
- }
5252
- if (!this.options.sdkConfig) {
5253
- console.log('[GenericPackageModal] No SDK configuration provided, skipping initialization');
5254
- return;
5255
- }
5256
- this.isInitializingSDK = true;
5257
- console.log('[GenericPackageModal] Initializing payment SDK...');
5258
- // 显示加载指示器
5259
- const loader = showLoadingIndicator('Initializing payment system...');
5260
- try {
5261
- const config = this.options.sdkConfig;
5262
- // 1. 从环境配置中获取基础配置
5263
- const envConfig = ENVIRONMENT_CONFIGS[config.environment];
5264
- // 2. 合并配置(自定义配置优先级高于环境配置)
5265
- const finalConfig = {
5266
- scriptUrl: config.scriptUrl || envConfig.scriptUrl,
5267
- clientId: config.clientId || envConfig.clientId,
5268
- orderApiUrl: config.orderApiUrl || envConfig.orderApiUrl,
5269
- cssUrl: config.cssUrl || envConfig.cssUrl,
5270
- };
5271
- console.log('[GenericPackageModal] Using environment:', config.environment);
5272
- // 3. 初始化 SeaartPaymentSDK
5273
- await SeaartPaymentSDK.getInstance().init({
5274
- scriptUrl: finalConfig.scriptUrl,
5275
- clientId: finalConfig.clientId,
5276
- language: 'en',
5277
- scriptTimeout: config.scriptTimeout,
5278
- cssUrl: finalConfig.cssUrl,
5279
- });
5280
- // 4. 获取支付方式列表
5281
- const paymentMethods = await SeaartPaymentSDK.getInstance().getPaymentMethods({
5282
- country_code: config.countryCode,
5283
- business_type: config.businessType ?? 1, // 默认为 1(一次性购买)
5284
- });
5285
- // 5. 查找匹配的支付方式
5286
- const paymentMethod = config.paymentMethodType
5287
- ? paymentMethods.find((m) => m.payment_method_type === config.paymentMethodType ||
5288
- m.payment_method_name.toLowerCase().includes(config.paymentMethodType.toLowerCase()))
5289
- : paymentMethods.find((m) => m.payment_type === 2); // 默认使用 dropin (payment_type === 2)
5290
- if (!paymentMethod) {
5291
- throw new Error(`Payment method "${config.paymentMethodType || 'dropin'}" not found`);
5292
- }
5293
- // 6. 存储到类成员变量(包括 finalConfig 以供后续使用)
5294
- this.paymentMethod = paymentMethod;
5295
- this.accountToken = config.accountToken;
5296
- this.sdkInitialized = true;
5297
- // 存储最终配置到 config 对象(用于 handlePaymentFlow)
5298
- this.options.sdkConfig._resolvedOrderApiUrl = finalConfig.orderApiUrl;
5299
- console.log('[GenericPackageModal] SDK initialized with environment config:', {
5300
- environment: config.environment,
5301
- paymentMethod: paymentMethod.payment_method_name,
5302
- accountToken: config.accountToken ? 'provided' : 'not provided',
5303
- });
5304
- }
5305
- catch (error) {
5306
- console.error('[GenericPackageModal] Failed to initialize payment SDK:', error);
5307
- // SDK 初始化失败不影响浏览套餐,只是无法进行支付
5308
- }
5309
- finally {
5310
- this.isInitializingSDK = false;
5311
- // 隐藏加载指示器
5312
- hideLoadingIndicator(loader);
5313
- }
5314
- }
5315
- /**
5316
- * 关闭弹框
5317
- */
5318
- close() {
5319
- console.log('[GenericPackageModal] Closing modal...');
5320
- this.modal.close();
5321
5145
  }
5322
5146
  /**
5323
- * 渲染弹框内容
5147
+ * Render modal content
5324
5148
  */
5325
5149
  renderContent() {
5326
- const container = this.modal.getContentContainer();
5150
+ const container = this.getContentContainer();
5327
5151
  if (!container) {
5328
5152
  throw new Error('Modal content container not found');
5329
5153
  }
5330
- // 直接渲染卡片内容,不添加任何外层包装
5154
+ // Directly render card content without any outer wrapper
5331
5155
  container.innerHTML = this.options.packages.map((pkg, index) => this.renderPackageCard(pkg, index)).join('');
5332
- // 添加点击事件监听
5156
+ // Attach event listeners
5333
5157
  this.attachEventListeners(container);
5334
5158
  }
5159
+ // === Private Helper Methods ===
5335
5160
  /**
5336
- * 渲染套餐卡片
5161
+ * Render package card
5337
5162
  */
5338
5163
  renderPackageCard(pkg, index) {
5339
5164
  const hasBonus = pkg.bonus_credits && parseInt(pkg.bonus_credits) > 0;
5340
5165
  const hasBonusPercentage = pkg.bonus_percentage && pkg.bonus_percentage > 0;
5341
- // 根据套餐类型决定显示的标题
5166
+ // Determine title based on package type
5342
5167
  let packageTitle = '';
5343
5168
  if (pkg.package_type === 'iceBreaker' || pkg.package_type === 'firstCharge') {
5344
5169
  packageTitle = 'One-time Only';
@@ -5367,7 +5192,7 @@ class GenericPackageModal {
5367
5192
  onmouseover="this.style.borderColor='rgba(255, 255, 255, 0.2)';"
5368
5193
  onmouseout="this.style.borderColor='rgba(255, 255, 255, 0.1)';"
5369
5194
  >
5370
- <!-- 套餐标题 - 卡片内顶部居中 -->
5195
+ <!-- Package title - top center of card -->
5371
5196
  ${packageTitle ? `
5372
5197
  <div style="
5373
5198
  position: absolute;
@@ -5385,7 +5210,7 @@ class GenericPackageModal {
5385
5210
  </div>
5386
5211
  ` : ''}
5387
5212
 
5388
- <!-- 关闭按钮 - 右上角 -->
5213
+ <!-- Close button - top right -->
5389
5214
  <button
5390
5215
  onclick="this.closest('[data-package-id]').dispatchEvent(new CustomEvent('close-modal', { bubbles: true }));"
5391
5216
  style="
@@ -5413,7 +5238,7 @@ class GenericPackageModal {
5413
5238
  ×
5414
5239
  </button>
5415
5240
 
5416
- <!-- 折扣标签 - 关闭按钮下方 -->
5241
+ <!-- Discount tag - below close button -->
5417
5242
  ${hasBonusPercentage ? `
5418
5243
  <div style="
5419
5244
  position: absolute;
@@ -5433,7 +5258,7 @@ class GenericPackageModal {
5433
5258
  </div>
5434
5259
  ` : ''}
5435
5260
 
5436
- <!-- 积分显示区域 - 确保不换行 -->
5261
+ <!-- Credits display area - ensure no line break -->
5437
5262
  <div style="
5438
5263
  display: flex;
5439
5264
  flex-direction: column;
@@ -5450,7 +5275,7 @@ class GenericPackageModal {
5450
5275
  gap: 12px;
5451
5276
  flex-wrap: nowrap;
5452
5277
  ">
5453
- <!-- 基础积分 -->
5278
+ <!-- Base credits -->
5454
5279
  <span style="
5455
5280
  font-size: 80px;
5456
5281
  line-height: 1;
@@ -5462,7 +5287,7 @@ class GenericPackageModal {
5462
5287
  ${this.formatNumber(pkg.base_credits || pkg.credits)}
5463
5288
  </span>
5464
5289
 
5465
- <!-- credits 文本 -->
5290
+ <!-- credits text -->
5466
5291
  <span style="
5467
5292
  font-size: 52px;
5468
5293
  line-height: 1;
@@ -5474,7 +5299,7 @@ class GenericPackageModal {
5474
5299
  credits
5475
5300
  </span>
5476
5301
 
5477
- <!-- 奖励积分 -->
5302
+ <!-- Bonus credits -->
5478
5303
  ${hasBonus ? `
5479
5304
  <span style="
5480
5305
  font-size: 52px;
@@ -5490,7 +5315,7 @@ class GenericPackageModal {
5490
5315
  </div>
5491
5316
  </div>
5492
5317
 
5493
- <!-- 副标题 - Valid for all platform products -->
5318
+ <!-- Subtitle - Valid for all platform products -->
5494
5319
  <div style="
5495
5320
  font-size: 22px;
5496
5321
  line-height: 1.4;
@@ -5501,7 +5326,7 @@ class GenericPackageModal {
5501
5326
  Valid for all platform products
5502
5327
  </div>
5503
5328
 
5504
- <!-- 购买按钮 -->
5329
+ <!-- Buy button -->
5505
5330
  <button
5506
5331
  data-package-button="${pkg.id}"
5507
5332
  style="
@@ -5529,201 +5354,6 @@ class GenericPackageModal {
5529
5354
  </div>
5530
5355
  `;
5531
5356
  }
5532
- /**
5533
- * 格式化数字(添加逗号)
5534
- */
5535
- formatNumber(num) {
5536
- return parseInt(num).toLocaleString();
5537
- }
5538
- /**
5539
- * 获取加载按钮的 HTML(带旋转动画)
5540
- */
5541
- getLoadingButtonHTML(text) {
5542
- return `
5543
- <svg style="
5544
- display: inline-block;
5545
- width: 16px;
5546
- height: 16px;
5547
- border: 2px solid rgba(255, 255, 255, 0.3);
5548
- border-top-color: white;
5549
- border-radius: 50%;
5550
- animation: spin 0.6s linear infinite;
5551
- " viewBox="0 0 24 24"></svg>
5552
- <style>@keyframes spin { to { transform: rotate(360deg); } }</style>
5553
- <span style="margin-left: 8px;">${text}</span>
5554
- `;
5555
- }
5556
- /**
5557
- * 添加事件监听
5558
- */
5559
- attachEventListeners(container) {
5560
- // 为每个套餐按钮添加点击事件
5561
- this.options.packages.forEach(pkg => {
5562
- const button = container.querySelector(`[data-package-button="${pkg.id}"]`);
5563
- if (button) {
5564
- button.addEventListener('click', async (e) => {
5565
- e.preventDefault();
5566
- e.stopPropagation();
5567
- console.log('[GenericPackageModal] Package selected:', pkg.id);
5568
- // 保存原始按钮文本
5569
- const originalText = button.innerHTML;
5570
- const originalDisabled = button.disabled;
5571
- try {
5572
- // 禁用按钮,显示初始化状态
5573
- button.disabled = true;
5574
- const isZh = this.language === 'zh-CN';
5575
- button.innerHTML = this.getLoadingButtonHTML(isZh ? '初始化中...' : 'Initializing...');
5576
- // 等待 SDK 初始化(支持重试)
5577
- const initialized = await this.waitForSDKInitialization(30000, 1);
5578
- if (!initialized) {
5579
- throw new Error('SDK initialization failed or timed out. Please try again.');
5580
- }
5581
- // SDK 初始化成功,执行支付流程
5582
- await this.handlePaymentFlow(pkg, button, originalText);
5583
- }
5584
- catch (error) {
5585
- console.error('[GenericPackageModal] Failed to process payment:', error);
5586
- // 恢复按钮状态
5587
- button.disabled = originalDisabled;
5588
- button.innerHTML = originalText;
5589
- // 显示错误提示(使用自定义 UI 替代 alert)
5590
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
5591
- showErrorMessage(`Payment failed: ${errorMessage}`);
5592
- // 触发失败回调
5593
- this.options.onPaymentFailed?.(error instanceof Error ? error : new Error(String(error)), pkg);
5594
- }
5595
- });
5596
- }
5597
- });
5598
- }
5599
- /**
5600
- * 处理支付流程
5601
- */
5602
- async handlePaymentFlow(pkg, button, originalHTML) {
5603
- try {
5604
- // 更新按钮状态为"创建订单中"
5605
- button.innerHTML = this.getLoadingButtonHTML('Creating order...');
5606
- console.log('[GenericPackageModal] Creating order for package:', pkg.id);
5607
- // 调用回调创建订单,或使用默认实现
5608
- let orderId;
5609
- // 使用默认实现:调用解析后的 orderApiUrl
5610
- const resolvedOrderApiUrl = this.options.sdkConfig._resolvedOrderApiUrl || ENVIRONMENT_CONFIGS[this.options.sdkConfig.environment].orderApiUrl;
5611
- const response = await createOrder(resolvedOrderApiUrl, this.options.sdkConfig.accountToken || '', {
5612
- product_id: pkg.id,
5613
- purchase_type: this.options.sdkConfig.businessType ?? 1, // 默认为 1
5614
- });
5615
- console.log('[GenericPackageModal] Create order response:', response);
5616
- if (!response || !response.transaction_id) {
5617
- throw new Error('Failed to create order: Invalid response from API');
5618
- }
5619
- orderId = response.transaction_id;
5620
- console.log('[GenericPackageModal] Order created:', orderId);
5621
- if (!orderId) {
5622
- throw new Error('Order ID not returned');
5623
- }
5624
- // 创建并打开支付弹框(按钮状态将在支付完成后处理)
5625
- await this.openPaymentModal(orderId, pkg);
5626
- // 支付弹框打开后,恢复按钮状态
5627
- button.disabled = false;
5628
- button.innerHTML = originalHTML;
5629
- }
5630
- catch (error) {
5631
- console.error('[GenericPackageModal] Payment flow failed:', error);
5632
- // 恢复按钮状态
5633
- button.disabled = false;
5634
- button.innerHTML = originalHTML;
5635
- // 显示错误提示(使用自定义 UI 替代 alert)
5636
- showErrorMessage(`Payment failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
5637
- // 触发失败回调
5638
- this.options.onPaymentFailed?.(error instanceof Error ? error : new Error(String(error)), pkg);
5639
- }
5640
- }
5641
- /**
5642
- * 打开支付弹框
5643
- */
5644
- async openPaymentModal(orderId, pkg) {
5645
- if (!this.paymentMethod) {
5646
- throw new Error('Payment method not configured');
5647
- }
5648
- const pkgName = pkg.name;
5649
- // 创建支付实例(使用 orderId)
5650
- if (!this.sdkInitialized) {
5651
- throw new Error('Payment SDK not initialized. Please provide sdkConfig.');
5652
- }
5653
- const paymentInstance = window.SeaartPaymentComponent.createPayment({
5654
- sys_order_id: orderId,
5655
- account_token: this.accountToken,
5656
- });
5657
- const dropinModal = new DropinPaymentModal(paymentInstance, orderId, this.accountToken, this.paymentMethod, {
5658
- modalTitle: `Purchase ${pkgName}`,
5659
- onCompleted: (payload) => {
5660
- console.log('[GenericPackageModal] Payment completed:', payload);
5661
- this.close();
5662
- // 显示购买成功弹窗
5663
- const successModal = new PurchaseSuccessModal({
5664
- data: {
5665
- packName: pkg.name,
5666
- credits: parseInt(pkg.credits),
5667
- amount: pkg.price,
5668
- currency: pkg.currency === 'USD' ? '$' : pkg.currency,
5669
- orderId: orderId,
5670
- transactionId: payload.transaction_id,
5671
- },
5672
- language: this.language,
5673
- onClose: () => {
5674
- // 弹窗关闭后刷新积分
5675
- (async () => {
5676
- try {
5677
- const envConfig = ENVIRONMENT_CONFIGS[this.options.sdkConfig.environment];
5678
- const walletApiUrl = envConfig.walletApiUrl;
5679
- console.log('[GenericPackageModal] Refreshing credits from:', walletApiUrl);
5680
- const creditDetail = await getCreditDetail(walletApiUrl, this.options.sdkConfig.accountToken);
5681
- if (creditDetail) {
5682
- console.log('[GenericPackageModal] Credits refreshed, total balance:', creditDetail.total_balance);
5683
- }
5684
- else {
5685
- console.warn('[GenericPackageModal] Failed to refresh credits');
5686
- }
5687
- }
5688
- catch (error) {
5689
- console.error('[GenericPackageModal] Failed to refresh credits:', error);
5690
- }
5691
- })();
5692
- // 触发用户回调
5693
- this.options.onPaymentSuccess?.(orderId, payload.transaction_id, pkg);
5694
- },
5695
- });
5696
- successModal.open();
5697
- },
5698
- onFailed: (payload) => {
5699
- console.error('[GenericPackageModal] Payment failed:', payload);
5700
- const error = new Error(payload.message || 'Payment failed');
5701
- this.options.onPaymentFailed?.(error, pkg);
5702
- },
5703
- onError: (payload, error) => {
5704
- console.error('[GenericPackageModal] Payment error:', error);
5705
- this.options.onPaymentFailed?.(error, pkg);
5706
- },
5707
- });
5708
- await dropinModal.open();
5709
- }
5710
- /**
5711
- * 清理资源
5712
- */
5713
- cleanup() {
5714
- console.log('[GenericPackageModal] Cleaning up...');
5715
- // 移除resize监听器
5716
- if (this.resizeHandler) {
5717
- window.removeEventListener('resize', this.resizeHandler);
5718
- this.resizeHandler = null;
5719
- }
5720
- }
5721
- /**
5722
- * 检查弹框是否打开
5723
- */
5724
- isOpen() {
5725
- return this.modal.isModalOpen();
5726
- }
5727
5357
  }
5728
5358
 
5729
5359
  /**