first commit

This commit is contained in:
jakciehan
2026-04-10 22:59:39 +08:00
commit cc2e7b9bb0
89 changed files with 23631 additions and 0 deletions
+352
View File
@@ -0,0 +1,352 @@
/**
* 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;