import { IPaymentChoice } from '@/common/rewards/reward-emitter'; import { paymentHelper } from './index'; import { isUndefinedOrNull } from '@jolibox/common'; import { StandardResponse } from '@jolibox/types'; import { applyNative } from '@jolibox/native-bridge'; import { innerFetch as fetch } from '@/native/network'; import type { PaymentResult } from './payment-helper'; import { RequestCacheService, RequestAdapter } from '@jolibox/common'; import { executeWithPersistentCache } from './utils/cache-with-storage'; type PaymentPurchaseType = 'JOLI_COIN' | 'JOLI_GEM'; // Request/Response interfaces for RequestCacheService type PaymentRequest = Record; interface PaymentResponse { balance: number; enableAutoDeduct: boolean; paymentChoices: IPaymentChoice[]; } interface PaymentCacheData { paymentChoices: IPaymentChoice[]; } interface PaymentRealTimeData { balance: number; enableAutoDeduct: boolean; } // Utility function for merging response data function mergeResponseData( responseData: { paymentChoices: IPaymentChoice[] }, data: { [appStoreProductId: string]: { price: string } } ) { Object.keys(data).forEach((key) => { const choice = responseData.paymentChoices.find((choice) => choice.appStoreProductId === key); if (choice) { choice.totalAmountStr = data[key].price; } }); responseData.paymentChoices = responseData.paymentChoices.filter( (choice) => !isUndefinedOrNull(choice.totalAmountStr) ) as IPaymentChoice[]; } // Payment Request Adapter for RequestCacheService class PaymentRequestAdapter implements RequestAdapter { // applyNative 的缓存 private static nativePriceCache = new Map(); private static readonly NATIVE_PRICE_CACHE_DURATION = 10 * 60 * 1000; // 10 minutes private static nativePriceCacheTimestamp = new Map(); async execute(endpoint: string, _options?: PaymentRequest): Promise { // 获取服务端数据(余额等实时数据) const { response } = await fetch>(endpoint, { method: 'GET', appendHostCookie: true, responseType: 'json' }); console.log('[PaymentService] get products info from server', endpoint, response.data); if (!response.data?.data) { throw new Error('get products info failed'); } const serverData = response.data.data; const appStoreProductIds = serverData.paymentChoices ?.filter((choice) => typeof choice.appStoreProductId === 'string') .map((choice) => choice.appStoreProductId as string) ?? []; if (appStoreProductIds.length > 0) { const nativeData = await this.getNativePriceData(appStoreProductIds); if (nativeData) { mergeResponseData(serverData, nativeData); } } if (serverData.paymentChoices.length === 0) { throw new Error('paymentChoices is empty'); } return serverData; } private async getNativePriceData( appStoreProductIds: string[] ): Promise<{ [appStoreProductId: string]: { price: string } } | null> { const cacheKey = appStoreProductIds.sort().join(','); return executeWithPersistentCache( cacheKey, async () => { try { const { data } = await applyNative('requestProductDetailsAsync', { appStoreProductIds }); if (data) { // cache PaymentRequestAdapter.nativePriceCache.set(cacheKey, data); PaymentRequestAdapter.nativePriceCacheTimestamp.set(cacheKey, Date.now()); } return data; } catch (error) { console.warn('[PaymentService] Failed to fetch native product details:', error); throw error; // rethrow error, let upper layer handle it } }, { cacheDuration: PaymentRequestAdapter.NATIVE_PRICE_CACHE_DURATION, storageKeyPrefix: 'joli_native_price_' } ); } extractCacheableData(response: PaymentResponse): PaymentCacheData { return { paymentChoices: response.paymentChoices }; } extractRealTimeData?: undefined; mergeData(cached: PaymentCacheData, realTime: PaymentRealTimeData): PaymentResponse { return { balance: realTime.balance, enableAutoDeduct: realTime.enableAutoDeduct, paymentChoices: cached.paymentChoices }; } processCachedData(cached: PaymentCacheData): PaymentResponse { // when only cached data is available, use default real-time data values return { balance: 0, // default balance enableAutoDeduct: false, // default not enable auto deduct paymentChoices: cached.paymentChoices }; } // clear native price cache static method static clearNativePriceCache(): void { this.nativePriceCache.clear(); this.nativePriceCacheTimestamp.clear(); console.log('[PaymentService] Cleared native price cache'); } } // Base Payment Service class class BasePaymentService extends RequestCacheService< PaymentRequest, PaymentResponse, PaymentCacheData, PaymentRealTimeData > { // 失败计数器,按 endpoint 统计 private static failureCounters = new Map(); private static readonly MAX_FAILURE_COUNT = 2; constructor(private apiEndpoint: string, private paymentType: PaymentPurchaseType) { super(new PaymentRequestAdapter(), { duration: 10 * 60 * 1000, // 10分钟缓存 paymentChoices timeout: 3000 // 3秒超时 }); } async getProductsInfo(): Promise { // 检查失败计数,如果达到最大失败次数则直接抛出错误 const currentFailureCount = BasePaymentService.failureCounters.get(this.apiEndpoint) || 0; if (currentFailureCount >= BasePaymentService.MAX_FAILURE_COUNT) { throw new Error( `getProductsInfo has failed more than ${BasePaymentService.MAX_FAILURE_COUNT} times for ${this.apiEndpoint}` ); } try { const result = await this.request(this.apiEndpoint); // 成功时重置失败计数器 BasePaymentService.failureCounters.delete(this.apiEndpoint); return result; } catch (error) { // 失败时增加计数器 const newCount = currentFailureCount + 1; BasePaymentService.failureCounters.set(this.apiEndpoint, newCount); console.warn( `[PaymentService] getProductsInfo failed (${newCount}/${ BasePaymentService.MAX_FAILURE_COUNT + 1 }) for ${this.apiEndpoint}:`, error ); throw error; } } async purchase(productId: string): Promise> { const productsInfo = await this.getProductsInfo(); const appStoreProductId = productsInfo?.paymentChoices.find( (choice) => choice.productId === productId )?.appStoreProductId; if (!appStoreProductId) { throw new Error('appStoreProductId not found'); } const result = (await paymentHelper.invokePayment( this.paymentType === 'JOLI_COIN' ? 'JOLI_COIN_IAP' : 'JOLI_GEM_IAP', { productId, appStoreProductId } )) as PaymentResult<{ totalAmount: string }>; return result; } // 获取产品信息(使用 RequestCacheService) async getProductsInfoWithBalance(): Promise { return this.request(this.apiEndpoint); } // 强制刷新产品信息 async refreshProductsInfo(): Promise { return this.forceRequest(this.apiEndpoint); } // 重置失败计数器(供测试使用) static resetFailureCounters(): void { this.failureCounters.clear(); } // 获取失败计数器状态(供测试使用) static getFailureCount(endpoint: string): number { return this.failureCounters.get(endpoint) || 0; } } // JOLI_COIN payment service export class JoliCoinPaymentService extends BasePaymentService { constructor() { super('/api/joli-coin/balance-detail', 'JOLI_COIN'); } } // JOLI_GEM payment service export class JoliGemPaymentService extends BasePaymentService { constructor() { super('/api/joli-gem/balance-detail', 'JOLI_GEM'); } } // service instance management let joliCoinServiceInstance: JoliCoinPaymentService | null = null; let joliGemServiceInstance: JoliGemPaymentService | null = null; // create JOLI_COIN payment service instance export const createJoliCoinPaymentService = (): JoliCoinPaymentService => { if (joliCoinServiceInstance) { return joliCoinServiceInstance; } joliCoinServiceInstance = new JoliCoinPaymentService(); return joliCoinServiceInstance; }; // create JOLI_GEM payment service instance export const createJoliGemPaymentService = (): JoliGemPaymentService => { if (joliGemServiceInstance) { return joliGemServiceInstance; } joliGemServiceInstance = new JoliGemPaymentService(); return joliGemServiceInstance; }; // 导出原生价格缓存清理方法供测试使用 export const clearNativePriceCache = (): void => { PaymentRequestAdapter.clearNativePriceCache(); }; // 导出失败计数器重置方法供测试使用 export const resetFailureCounters = (): void => { BasePaymentService.resetFailureCounters(); }; // 导出获取失败计数方法供测试使用 export const getFailureCount = (endpoint: string): number => { return BasePaymentService.getFailureCount(endpoint); };