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