@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/README.md +65 -3
- package/dist/index.browser.js +595 -965
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +595 -965
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +175 -155
- package/dist/index.js +595 -965
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -4221,184 +4221,94 @@ class PurchaseSuccessModal {
|
|
|
4221
4221
|
}
|
|
4222
4222
|
|
|
4223
4223
|
/**
|
|
4224
|
-
*
|
|
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
|
|
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.
|
|
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(
|
|
4247
|
+
console.log(`[${this.constructor.name}] Opening modal...`);
|
|
4275
4248
|
this.modal.open();
|
|
4276
|
-
//
|
|
4277
|
-
|
|
4278
|
-
|
|
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
|
-
//
|
|
4317
|
-
this.resizeHandler = () =>
|
|
4318
|
-
this.renderContent();
|
|
4319
|
-
};
|
|
4253
|
+
// Add resize listener
|
|
4254
|
+
this.resizeHandler = () => this.renderContent();
|
|
4320
4255
|
window.addEventListener('resize', this.resizeHandler);
|
|
4321
|
-
//
|
|
4256
|
+
// Initialize SDK in background
|
|
4322
4257
|
if (!this.sdkInitialized && !this.isInitializingSDK) {
|
|
4323
4258
|
this.initializeSDK();
|
|
4324
4259
|
}
|
|
4325
|
-
console.log(
|
|
4260
|
+
console.log(`[${this.constructor.name}] Modal opened`);
|
|
4326
4261
|
}
|
|
4327
4262
|
/**
|
|
4328
|
-
*
|
|
4329
|
-
* @param timeout 超时时间(毫秒),默认 30 秒
|
|
4330
|
-
* @param maxRetries 最大重试次数,默认 1 次
|
|
4331
|
-
* @returns 是否初始化成功
|
|
4263
|
+
* Close the modal
|
|
4332
4264
|
*/
|
|
4333
|
-
|
|
4334
|
-
|
|
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
|
-
*
|
|
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(
|
|
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(
|
|
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(
|
|
4401
|
-
// 3.
|
|
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, //
|
|
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); //
|
|
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.
|
|
4423
|
-
this.
|
|
4424
|
-
this.
|
|
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
|
-
//
|
|
4336
|
+
// Store final config to config object (for handlePaymentFlow)
|
|
4427
4337
|
this.options.sdkConfig._resolvedOrderApiUrl = finalConfig.orderApiUrl;
|
|
4428
|
-
console.log(
|
|
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(
|
|
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
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
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
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
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
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
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
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
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.
|
|
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
|
-
<!--
|
|
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 -
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
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
|
-
|
|
5145
|
-
title: '', //
|
|
5146
|
-
showCloseButton: false, //
|
|
5147
|
-
closeOnOverlayClick: false, //
|
|
5148
|
-
closeOnEsc: true, //
|
|
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
|
-
|
|
5161
|
-
|
|
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
|
-
//
|
|
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.
|
|
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.
|
|
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
|
-
<!--
|
|
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
|
/**
|