import { StandardResponse } from '@jolibox/types'; import { context } from '@/common/context'; import { innerFetch as fetch } from '@/native/network'; import { BaseSubscriptionRegister } from './base'; import type { IPlaceSubscriptionOrderResponse } from './type'; import { PaymentErrorCodeMap } from '@/native/payment/registers/type'; import { createPaymentInternalError } from '@/native/payment/registers/base'; import { ResponseType } from '@jolibox/types'; import { Deferred } from '@jolibox/common'; import { invokeNative, onNative } from '@jolibox/native-bridge'; import { isNumber } from '@jolibox/common'; import { SubscriptionErrorCodeMap } from './type'; export interface IPlaceOrderSubAppParams { basePlanId: string; appStoreProductId: string; productId: string; } export interface ISubAppProductInfo { subPlanId: string; } export interface ISubAppSubscriptionContext { gameId: string; productId: string; planId: string; } /** global subscription */ onNative('onGlobalSubscriptionStateChange', (data) => { const { status, orderResponse } = data; console.log('[onGlobalSubscriptionStateChange] data', data); if (status === 'SUCCESS' && (orderResponse?.subPlanId || orderResponse?.ticketId)) { // user must be logined context.onEnvConfigChanged({ hostUserInfo: { isLogin: true, isSubUser: true } }); } }); const pendingSubscriptions = new Map>>(); onNative('onSubscritptionStateChange', (data) => { const { subUUID, status, orderResponse } = data; console.log('[onSubscritptionStateChange] data', data); const deferred = pendingSubscriptions.get(subUUID); if (!deferred) { return; } if (status === 'SUCCESS') { console.log( '=======onSubscritptionStateChange', status, orderResponse?.subPlanId ?? orderResponse?.ticketId ); deferred.resolve({ code: 'SUCCESS' as ResponseType, message: 'jolicoin payment success', data: { subPlanId: orderResponse?.subPlanId ?? orderResponse?.ticketId } }); } else { if (status === 'FAILED') { if (isNumber(orderResponse?.errNo) && orderResponse?.errNo !== 0) { throw createPaymentInternalError( `subscription failed: ${orderResponse?.errMsg}`, SubscriptionErrorCodeMap.SubscriptionFailed ); } } deferred.resolve({ code: status as ResponseType, message: orderResponse?.errMsg ?? 'UNKNOWN_ERROR' }); } // Clean up map pendingSubscriptions.delete(subUUID); }); const internalSubscribe = async (params: { appStoreProductId: string; basePlanId: string; productId: string; appAccountToken?: string; }) => { const deferred = new Deferred>(); let targetOrderUUID: string | undefined; try { console.log('[internalSubscribe] requestSubscriptionSync params', params); const response = invokeNative('requestSubscriptionSync', { params: { basePlanId: params.basePlanId, appStoreProductId: params.appStoreProductId, productId: params.productId }, ...(params.appAccountToken ? { paymentBody: { appAccountToken: params.appAccountToken, appStoreProductId: params.appStoreProductId } } : {}) }); targetOrderUUID = response.data?.subUUID; if (!targetOrderUUID) { throw createPaymentInternalError( 'subUUID is null', PaymentErrorCodeMap.JolicoinPlaceOrderRequestFailed ); } pendingSubscriptions.set(targetOrderUUID, deferred); } catch (e) { throw createPaymentInternalError( JSON.stringify(e) ?? 'jolicoin payment failed', PaymentErrorCodeMap.JolicoinPlaceOrderRequestFailed ); } return deferred.promise; }; // Android 实现 - 不需要 placeOrder class SubAppAndroidSubscriptionRegister extends BaseSubscriptionRegister< IPlaceOrderSubAppParams, ISubAppProductInfo, { subPlanId: string } > { constructor(private readonly productId: string, private readonly planId: string) { super(); } async placeOrder( _params: IPlaceOrderSubAppParams ): Promise>> { // Android 不需要实际下单,直接返回成功 return { code: 'SUCCESS', message: 'Android subscription order placed locally' }; } cancelOrder(_orderId: string): Promise> { // Android 本地取消 return Promise.resolve({ code: 'SUCCESS', message: 'Android order cancelled locally', data: undefined }); } generateSubscriptionContext(): ISubAppSubscriptionContext { return { gameId: context.mpId, productId: this.productId, planId: this.planId }; } async subscribe(params: IPlaceOrderSubAppParams): Promise> { const response = await internalSubscribe(params); return response; } } // iOS 实现 - 需要 placeOrder class SubAppIOSSubscriptionRegister extends BaseSubscriptionRegister< IPlaceOrderSubAppParams, ISubAppProductInfo, { subPlanId: string } > { private subscriptionToken: string | undefined; constructor(private readonly productId: string, private readonly planId: string) { super(); } async placeOrder( params: IPlaceOrderSubAppParams ): Promise>> { console.log('----start place order for iOS subscription----'); const env = invokeNative('envSync'); const accountToken = env?.data?.hostUserInfo?.appAccountToken; if (!accountToken) { throw createPaymentInternalError( 'appAccountToken is null', PaymentErrorCodeMap.JolicoinPlaceOrderRequestFailed ); } this.subscriptionToken = accountToken; return { code: 'SUCCESS' as ResponseType, message: 'placeorder sub-app success in native' }; } async cancelOrder(orderId: string): Promise> { // Android 本地取消 return Promise.resolve({ code: 'SUCCESS', message: 'Android order cancelled locally', data: undefined }); } generateSubscriptionContext(): ISubAppSubscriptionContext { return { gameId: context.mpId, productId: this.productId, planId: this.planId }; } async subscribe(params: IPlaceOrderSubAppParams): Promise> { return await internalSubscribe({ ...params, appAccountToken: this.subscriptionToken }); } } const SubAppSubscriptionRegisterMap = { android: SubAppAndroidSubscriptionRegister, ios: SubAppIOSSubscriptionRegister } as const; type SubAppSubscriptionRegister = SubAppAndroidSubscriptionRegister | SubAppIOSSubscriptionRegister; export function createSubAppSubscriptionHandler() { const productSubscriptionMap = new Map(); return async (params: { productId: string; appStoreProductId: string; planId: string }) => { console.log('[SubAppSubscriptionHandler] createSubAppSubscriptionHandler params', params); const { productId, planId } = params; const cacheKey = `${productId}_${planId}`; let instance = productSubscriptionMap.get(cacheKey); if (!instance) { console.log('[SubAppSubscriptionHandler] createSubAppSubscriptionHandler instance not found', cacheKey); const Register = SubAppSubscriptionRegisterMap[context.platform as keyof typeof SubAppSubscriptionRegisterMap]; if (!Register) { throw new Error(`Platform ${context.platform} not supported for sub-app subscription`); } instance = new Register(productId, planId); productSubscriptionMap.set(cacheKey, instance); } return instance.startSubscription({ basePlanId: planId, appStoreProductId: params.appStoreProductId, productId }); }; }