/** * PaymentManager.js * Simplified payment manager for monetization-lite. * Only 3 products: Ad-Free (¥18), Gold Pack (¥6=1000g), Newcomer Pack (¥1=500g). * Handles WeChat payment, order recovery, and newcomer pack 24h timer. */ /** Product definitions. */ const PRODUCTS = { AD_FREE: { id: 'ad_free', price: 18, // ¥18 name: 'Remove Ads (Permanent)', type: 'permanent', }, GOLD_PACK: { id: 'gold_pack', price: 6, // ¥6 goldAmount: 1000, name: 'Gold Pack', type: 'consumable', }, NEWCOMER_PACK: { id: 'newcomer_pack', price: 1, // ¥1 goldAmount: 500, name: 'Newcomer Pack', type: 'one_time', }, }; /** Newcomer pack availability window in milliseconds (24 hours). */ const NEWCOMER_WINDOW_MS = 24 * 60 * 60 * 1000; class PaymentManager { constructor() { this._adFreePurchased = false; this._newcomerPackPurchased = false; this._newcomerPackStartTime = 0; // timestamp when user first entered game this._pendingOrders = []; this._load(); } // ============================================================ // Persistence // ============================================================ /** * Load payment state from StorageManager. * @private */ _load() { try { if (GameGlobal && GameGlobal.storageManager) { const data = GameGlobal.storageManager.get('payment', null); if (data) { this._adFreePurchased = data.adFreePurchased || false; this._newcomerPackPurchased = data.newcomerPackPurchased || false; this._newcomerPackStartTime = data.newcomerPackStartTime || 0; this._pendingOrders = data.pendingOrders || []; } // Initialize newcomer pack timer on first load if (this._newcomerPackStartTime === 0) { this._newcomerPackStartTime = Date.now(); this._save(); } } } catch (e) { console.warn('[PaymentManager] Failed to load payment data:', e); } // Try to recover any pending orders this._recoverPendingOrders(); } /** * Save payment state to StorageManager. * @private */ _save() { try { if (GameGlobal && GameGlobal.storageManager) { GameGlobal.storageManager.set('payment', { adFreePurchased: this._adFreePurchased, newcomerPackPurchased: this._newcomerPackPurchased, newcomerPackStartTime: this._newcomerPackStartTime, pendingOrders: this._pendingOrders, }); } } catch (e) { console.warn('[PaymentManager] Failed to save payment data:', e); } } // ============================================================ // Purchase API // ============================================================ /** * Purchase the ad-free privilege (¥18, permanent). * @param {Function} callback - Called with { success: boolean, error?: string }. */ purchaseAdFree(callback) { if (this._adFreePurchased) { if (callback) callback({ success: false, error: 'Already purchased' }); return; } this._requestPayment(PRODUCTS.AD_FREE, (result) => { if (result.success) { this._adFreePurchased = true; this._save(); // Enable ad-free in AdManager if (GameGlobal.adManager) { GameGlobal.adManager.enableAdFree(); } // Emit event this._emitPurchaseEvent('ad_free'); } if (callback) callback(result); }); } /** * Purchase a gold pack (¥6 = 1000 gold). * @param {Function} callback - Called with { success: boolean, error?: string }. */ purchaseGoldPack(callback) { this._requestPayment(PRODUCTS.GOLD_PACK, (result) => { if (result.success) { // Award gold if (GameGlobal.currencyManager) { GameGlobal.currencyManager.addGold(PRODUCTS.GOLD_PACK.goldAmount); } this._emitPurchaseEvent('gold_pack'); } if (callback) callback(result); }); } /** * Purchase the newcomer pack (¥1 = 500 gold, one-time only). * @param {Function} callback - Called with { success: boolean, error?: string }. */ purchaseNewcomerPack(callback) { if (this._newcomerPackPurchased) { if (callback) callback({ success: false, error: 'Already purchased' }); return; } if (!this.isNewcomerPackAvailable()) { if (callback) callback({ success: false, error: 'Newcomer pack expired' }); return; } this._requestPayment(PRODUCTS.NEWCOMER_PACK, (result) => { if (result.success) { this._newcomerPackPurchased = true; this._save(); // Award gold if (GameGlobal.currencyManager) { GameGlobal.currencyManager.addGold(PRODUCTS.NEWCOMER_PACK.goldAmount); } this._emitPurchaseEvent('newcomer_pack'); } if (callback) callback(result); }); } // ============================================================ // Status Queries // ============================================================ /** * Whether ad-free has been purchased. * @returns {boolean} */ isAdFreePurchased() { return this._adFreePurchased; } /** * Whether the newcomer pack is still available (within 24h and not purchased). * @returns {boolean} */ isNewcomerPackAvailable() { if (this._newcomerPackPurchased) return false; const elapsed = Date.now() - this._newcomerPackStartTime; return elapsed < NEWCOMER_WINDOW_MS; } /** * Get remaining time for newcomer pack in milliseconds. * @returns {number} Remaining ms, or 0 if expired. */ getNewcomerPackRemainingMs() { if (this._newcomerPackPurchased) return 0; const remaining = NEWCOMER_WINDOW_MS - (Date.now() - this._newcomerPackStartTime); return Math.max(0, remaining); } /** * Get product definitions. * @returns {object} */ getProducts() { return PRODUCTS; } // ============================================================ // WeChat Payment // ============================================================ /** * Request payment via WeChat Midas payment. * @param {object} product - Product definition. * @param {Function} callback - Called with { success: boolean, error?: string }. * @private */ _requestPayment(product, callback) { // Add to pending orders for recovery const orderId = `${product.id}_${Date.now()}`; this._pendingOrders.push({ orderId, productId: product.id, timestamp: Date.now() }); this._save(); try { if (typeof wx === 'undefined' || typeof wx.requestMidasPayment !== 'function') { // Dev environment: simulate success console.log(`[PaymentManager] Dev mode: simulating purchase of ${product.id}`); this._removePendingOrder(orderId); if (callback) callback({ success: true }); return; } wx.requestMidasPayment({ mode: 'game', env: 0, // 0 = production, 1 = sandbox offerId: '', // Replace with actual offer ID currencyType: 'CNY', buyQuantity: product.price * 10, // Midas uses 1/10 yuan units success: () => { console.log(`[PaymentManager] Purchase successful: ${product.id}`); this._removePendingOrder(orderId); if (callback) callback({ success: true }); }, fail: (err) => { console.warn(`[PaymentManager] Purchase failed: ${product.id}`, err); this._removePendingOrder(orderId); if (callback) callback({ success: false, error: err.errMsg || 'Payment failed' }); }, }); } catch (e) { console.warn('[PaymentManager] Payment request error:', e); this._removePendingOrder(orderId); if (callback) callback({ success: false, error: e.message }); } } /** * Remove a pending order after completion. * @param {string} orderId * @private */ _removePendingOrder(orderId) { this._pendingOrders = this._pendingOrders.filter(o => o.orderId !== orderId); this._save(); } /** * Attempt to recover pending orders (e.g., after network interruption). * @private */ _recoverPendingOrders() { if (this._pendingOrders.length === 0) return; // Remove orders older than 1 hour (stale) const oneHourAgo = Date.now() - 60 * 60 * 1000; this._pendingOrders = this._pendingOrders.filter(o => o.timestamp > oneHourAgo); this._save(); // In production, query server for order status and deliver items // For now, just log if (this._pendingOrders.length > 0) { console.log(`[PaymentManager] ${this._pendingOrders.length} pending orders to recover`); } } /** * Emit a purchase completed event. * @param {string} productId * @private */ _emitPurchaseEvent(productId) { try { if (GameGlobal && GameGlobal.eventBus) { GameGlobal.eventBus.emit('purchase:completed', { productId }); } } catch (e) {} } // ============================================================ // Cloud Sync // ============================================================ /** * Get payment data for cloud sync. * @returns {object} */ getCloudSyncData() { return { adFreePurchased: this._adFreePurchased, newcomerPackPurchased: this._newcomerPackPurchased, newcomerPackStartTime: this._newcomerPackStartTime, }; } /** * Restore payment data from cloud. * @param {object} cloudData */ restoreFromCloud(cloudData) { if (!cloudData) return; let changed = false; // Ad-free is permanent — if cloud says purchased, trust it if (cloudData.adFreePurchased && !this._adFreePurchased) { this._adFreePurchased = true; if (GameGlobal.adManager) { GameGlobal.adManager.enableAdFree(); } changed = true; } if (cloudData.newcomerPackPurchased && !this._newcomerPackPurchased) { this._newcomerPackPurchased = true; changed = true; } if (changed) { this._save(); } } } PaymentManager.PRODUCTS = PRODUCTS; module.exports = PaymentManager;