@jolibox/implement 1.2.7 → 1.2.9-beta.9

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.
Files changed (66) hide show
  1. package/.rush/temp/package-deps_build.json +38 -28
  2. package/dist/common/rewards/registers/use-gem-only.d.ts +2 -1
  3. package/dist/common/rewards/registers/use-gem.d.ts +2 -1
  4. package/dist/common/rewards/registers/use-jolicoin-only.d.ts +2 -1
  5. package/dist/common/rewards/registers/use-jolicoin.d.ts +2 -1
  6. package/dist/common/rewards/registers/use-subscription.d.ts +2 -1
  7. package/dist/common/rewards/registers/utils/coins/jolicoin/jolicoin-handler.d.ts +1 -0
  8. package/dist/common/rewards/registers/utils/coins/joligem/gem-handler.d.ts +2 -1
  9. package/dist/common/rewards/registers/utils/common.d.ts +1 -0
  10. package/dist/common/rewards/registers/utils/subscription/sub-handler.d.ts +2 -1
  11. package/dist/common/rewards/reward-emitter.d.ts +8 -0
  12. package/dist/common/rewards/type.d.ts +1 -1
  13. package/dist/common/utils/index.d.ts +1 -1
  14. package/dist/index.js +25 -25
  15. package/dist/index.native.js +54 -54
  16. package/dist/native/payment/utils/__tests__/cache-with-storage.test.d.ts +1 -0
  17. package/dist/native/payment/utils/cache-with-storage.d.ts +7 -0
  18. package/dist/native/rewards/check-frequency.d.ts +8 -0
  19. package/dist/native/rewards/index.d.ts +1 -0
  20. package/dist/native/rewards/ui/subscription-modal.d.ts +1 -0
  21. package/dist/native/subscription/index.d.ts +3 -0
  22. package/dist/native/subscription/registers/base.d.ts +22 -0
  23. package/dist/native/subscription/registers/sub-app.d.ts +21 -0
  24. package/dist/native/subscription/registers/type.d.ts +10 -0
  25. package/dist/native/subscription/subscription-helper.d.ts +22 -0
  26. package/dist/native/subscription/subscription-service.d.ts +43 -0
  27. package/dist/native/subscription/type.d.ts +18 -0
  28. package/implement.build.log +2 -2
  29. package/package.json +5 -5
  30. package/src/common/context/index.ts +4 -1
  31. package/src/common/rewards/cached-fetch-reward.ts +16 -1
  32. package/src/common/rewards/registers/use-gem-only.ts +5 -2
  33. package/src/common/rewards/registers/use-gem.ts +5 -2
  34. package/src/common/rewards/registers/use-jolicoin-only.ts +5 -2
  35. package/src/common/rewards/registers/use-jolicoin.ts +5 -2
  36. package/src/common/rewards/registers/use-subscription.ts +5 -2
  37. package/src/common/rewards/registers/utils/coins/jolicoin/jolicoin-handler.ts +33 -11
  38. package/src/common/rewards/registers/utils/coins/joligem/gem-handler.ts +34 -13
  39. package/src/common/rewards/registers/utils/common.ts +9 -0
  40. package/src/common/rewards/registers/utils/subscription/commands/use-subscription.ts +16 -0
  41. package/src/common/rewards/registers/utils/subscription/sub-handler.ts +23 -7
  42. package/src/common/rewards/reward-emitter.ts +8 -0
  43. package/src/common/rewards/type.ts +1 -1
  44. package/src/common/utils/index.ts +1 -1
  45. package/src/h5/api/ads.ts +6 -3
  46. package/src/h5/bootstrap/auth/__tests__/auth.test.ts +15 -9
  47. package/src/h5/bootstrap/auth/sub.ts +1 -1
  48. package/src/native/api/ads.ts +43 -6
  49. package/src/native/api/call-host-method.ts +5 -61
  50. package/src/native/api/login.ts +22 -7
  51. package/src/native/api/payment.ts +78 -3
  52. package/src/native/payment/__tests__/payment-service-simple.test.ts +14 -1
  53. package/src/native/payment/payment-service.ts +26 -27
  54. package/src/native/payment/utils/__tests__/cache-with-storage.test.ts +414 -0
  55. package/src/native/payment/utils/cache-with-storage.ts +112 -0
  56. package/src/native/rewards/check-frequency.ts +6 -0
  57. package/src/native/rewards/index.ts +1 -0
  58. package/src/native/rewards/ui/payment-modal.ts +2 -2
  59. package/src/native/rewards/ui/subscription-modal.ts +81 -0
  60. package/src/native/subscription/index.ts +12 -0
  61. package/src/native/subscription/registers/base.ts +88 -0
  62. package/src/native/subscription/registers/sub-app.ts +258 -0
  63. package/src/native/subscription/registers/type.ts +13 -0
  64. package/src/native/subscription/subscription-helper.ts +53 -0
  65. package/src/native/subscription/subscription-service.ts +339 -0
  66. package/src/native/subscription/type.ts +18 -0
@@ -0,0 +1,339 @@
1
+ import { ISubscriptionPlanData } from './type';
2
+ import { ISubscriptionTierData } from '@jolibox/types';
3
+ import { Deferred, isUndefinedOrNull } from '@jolibox/common';
4
+ import { StandardResponse } from '@jolibox/types';
5
+ import { applyNative, onNative } from '@jolibox/native-bridge';
6
+ import { innerFetch as fetch } from '@/native/network';
7
+ import { RequestCacheService, RequestAdapter } from '@jolibox/common';
8
+ import { executeAndCache, executeWithPersistentCache } from '../payment/utils/cache-with-storage';
9
+ import { subscriptionHelper } from './subscription-helper';
10
+ import { invokeNative } from '@jolibox/native-bridge';
11
+ import { createSubscriptionInternalError } from './registers/base';
12
+ import { SubscriptionErrorCodeMap } from './registers/type';
13
+ import { ResponseType } from '@jolibox/types';
14
+
15
+ // Request/Response interfaces for RequestCacheService
16
+ type SubscriptionRequest = Record<string, never>;
17
+
18
+ type SubscriptionResponse = ISubscriptionTierData[];
19
+
20
+ interface SubscriptionCacheData {
21
+ subscriptionTiers: ISubscriptionTierData[];
22
+ }
23
+
24
+ // Utility function for merging native pricing data with subscription tiers
25
+ function mergeSubscriptionPriceData(
26
+ responseData: ISubscriptionTierData[],
27
+ nativePriceData: { [appStoreProductId: string]: ISubscriptionPlanData }
28
+ ) {
29
+ Object.keys(nativePriceData).forEach((productId) => {
30
+ const tier = responseData.find((tier) => tier.appStoreProductId === productId);
31
+ if (tier) {
32
+ tier.amountStr = nativePriceData[productId].pricing.formattedPrice;
33
+ tier.originalAmountStr = nativePriceData[productId].pricing.originalPrice;
34
+ tier.discountPercentage = nativePriceData[productId].pricing.discountPercentage?.toString() ?? '';
35
+ tier.trialDay = nativePriceData[productId].trial?.duration ?? 0;
36
+ }
37
+ });
38
+
39
+ // Filter out tiers without pricing information
40
+ responseData = responseData.filter((tier) => !isUndefinedOrNull(tier.amountStr)) as ISubscriptionTierData[];
41
+ }
42
+
43
+ // Subscription Request Adapter for RequestCacheService
44
+ class SubscriptionRequestAdapter
45
+ implements RequestAdapter<SubscriptionRequest, SubscriptionResponse, SubscriptionCacheData, never>
46
+ {
47
+ // Native price cache for subscription tiers
48
+ private static nativePriceCache = new Map<string, { [appStoreProductId: string]: ISubscriptionPlanData }>();
49
+ private static readonly NATIVE_PRICE_CACHE_DURATION = 1 * 24 * 60 * 60 * 1000; // 1 days
50
+ private static nativePriceCacheTimestamp = new Map<string, number>();
51
+
52
+ static getNativePriceCache(appStoreProductIds: string[]): {
53
+ [appStoreProductId: string]: ISubscriptionPlanData;
54
+ } {
55
+ const cacheKey = appStoreProductIds.sort().join(',');
56
+ return this.nativePriceCache.get(cacheKey) ?? {};
57
+ }
58
+
59
+ async execute(endpoint: string, _options?: SubscriptionRequest): Promise<SubscriptionResponse> {
60
+ // 获取服务端订阅档位数据
61
+ const { response } = await fetch<StandardResponse<ISubscriptionTierData[]>>(endpoint, {
62
+ method: 'GET',
63
+ appendHostCookie: true,
64
+ responseType: 'json'
65
+ });
66
+
67
+ if (!response.data?.data) {
68
+ throw new Error('get subscription tiers failed');
69
+ }
70
+
71
+ const serverData = response.data.data;
72
+
73
+ const appStoreProductIds =
74
+ serverData
75
+ ?.filter((tier: ISubscriptionTierData) => typeof tier.appStoreProductId === 'string')
76
+ .map((tier) => tier.appStoreProductId as string) ?? [];
77
+
78
+ // 如果有需要查询原生价格的产品,则获取价格信息
79
+ if (appStoreProductIds.length > 0) {
80
+ const nativePriceData = await this.getNativePriceData(appStoreProductIds);
81
+ if (nativePriceData) {
82
+ mergeSubscriptionPriceData(serverData, nativePriceData);
83
+ }
84
+ }
85
+
86
+ if (serverData.length === 0) {
87
+ throw new Error('subscription tiers is empty');
88
+ }
89
+
90
+ return serverData;
91
+ }
92
+
93
+ private async requestNativeSubscriptionPlans(
94
+ appStoreProductIds: string[],
95
+ cacheKey: string
96
+ ): Promise<{ [appStoreProductId: string]: ISubscriptionPlanData }> {
97
+ try {
98
+ const { data } = await applyNative('requestSubscriptionPlansAsync', {
99
+ appStoreProductIds,
100
+ type: 'game'
101
+ });
102
+
103
+ if (data) {
104
+ SubscriptionRequestAdapter.nativePriceCache.set(cacheKey, data);
105
+ SubscriptionRequestAdapter.nativePriceCacheTimestamp.set(cacheKey, Date.now());
106
+ }
107
+
108
+ return data;
109
+ } catch (error) {
110
+ console.warn('[SubscriptionService] Failed to fetch native subscription pricing:', error);
111
+ throw error;
112
+ }
113
+ }
114
+
115
+ private async getNativePriceData(
116
+ appStoreProductIds: string[],
117
+ forceRefresh = false
118
+ ): Promise<{ [appStoreProductId: string]: ISubscriptionPlanData } | null> {
119
+ const cacheKey = appStoreProductIds.sort().join(',');
120
+
121
+ const executor = forceRefresh ? executeAndCache : executeWithPersistentCache;
122
+ return executor(cacheKey, () => this.requestNativeSubscriptionPlans(appStoreProductIds, cacheKey), {
123
+ storageKeyPrefix: 'joli_subscription_price_'
124
+ });
125
+ }
126
+
127
+ extractCacheableData(response: SubscriptionResponse): SubscriptionCacheData {
128
+ return {
129
+ subscriptionTiers: response
130
+ };
131
+ }
132
+
133
+ extractRealTimeData?: undefined;
134
+
135
+ mergeData(): never {
136
+ throw new Error('SubscriptionService does not support real-time data merging');
137
+ }
138
+
139
+ processCachedData(cached: SubscriptionCacheData): SubscriptionResponse {
140
+ return cached.subscriptionTiers;
141
+ }
142
+
143
+ // Update native price cache method
144
+ async updateNativePriceCache(appStoreProductIds: string[]): Promise<void> {
145
+ await this.getNativePriceData(appStoreProductIds, true);
146
+ console.log('[SubscriptionService] Updated native price cache for products:', appStoreProductIds);
147
+ }
148
+
149
+ // Clear native price cache static method
150
+ static clearNativePriceCache(): void {
151
+ this.nativePriceCache.clear();
152
+ this.nativePriceCacheTimestamp.clear();
153
+ console.log('[SubscriptionService] Cleared native price cache');
154
+ }
155
+ }
156
+
157
+ // Base Subscription Service class
158
+ class BaseSubscriptionService extends RequestCacheService<
159
+ SubscriptionRequest,
160
+ SubscriptionResponse,
161
+ SubscriptionCacheData,
162
+ never
163
+ > {
164
+ private pendingSubscriptions = new Map<
165
+ string,
166
+ Deferred<StandardResponse<{ result: 'SUCCESS' | 'FAILED'; subPlanId?: string }>>
167
+ >();
168
+
169
+ private subInfoCacheKeys: string[] = [];
170
+ constructor(private apiEndpoint: string) {
171
+ super(new SubscriptionRequestAdapter(), {
172
+ duration: 30 * 60 * 1000, // 30分钟缓存订阅档位
173
+ timeout: 3000 // 3秒超时
174
+ });
175
+ this.listenSubscriptionPanelStateChange();
176
+ }
177
+
178
+ async getSubInfo(): Promise<SubscriptionResponse | undefined> {
179
+ try {
180
+ const result = await this.request(this.apiEndpoint);
181
+ this.subInfoCacheKeys = result?.map((tier) => tier.appStoreProductId ?? '') ?? [];
182
+ return result;
183
+ } catch (error) {
184
+ console.warn(`[SubscriptionService] getSubInfo failed for ${this.apiEndpoint}:`, error);
185
+ throw error;
186
+ }
187
+ }
188
+
189
+ // 强制刷新订阅档位信息
190
+ async refreshSubInfo(): Promise<SubscriptionResponse | undefined> {
191
+ return this.forceRequest(this.apiEndpoint);
192
+ }
193
+
194
+ async updateNativePriceCache(appStoreProductIds: string[]): Promise<void> {
195
+ const adapter = this.requestAdapter as SubscriptionRequestAdapter;
196
+ await adapter.updateNativePriceCache(appStoreProductIds);
197
+ }
198
+
199
+ async updateAllNativePriceCache(): Promise<void> {
200
+ try {
201
+ if (this.subInfoCacheKeys.length > 0) {
202
+ await this.updateNativePriceCache(this.subInfoCacheKeys);
203
+ this.forceRequest(this.apiEndpoint);
204
+ }
205
+ } catch (error) {
206
+ console.warn('[SubscriptionService] Failed to update all native price cache:', error);
207
+ throw error;
208
+ }
209
+ }
210
+
211
+ // subscribe
212
+ async subscribe(params: {
213
+ basePlanId: string;
214
+ nativeProductId: string;
215
+ appStoreProductId: string;
216
+ }): Promise<{ subPlanId: string; message: string; result: 'SUCCESS' | 'FAILED' | 'FAILURE_SUBSCRIPTED' }> {
217
+ console.log('[SubscriptionService] subscribe params', subscriptionHelper);
218
+ const result = await subscriptionHelper.invokeSubscription('SUB_APP', {
219
+ productId: params.nativeProductId,
220
+ appStoreProductId: params.appStoreProductId,
221
+ planId: params.basePlanId
222
+ });
223
+
224
+ return {
225
+ result: result.code as 'SUCCESS' | 'FAILED' | 'FAILURE_SUBSCRIPTED',
226
+ subPlanId: result.data?.subPlanId,
227
+ message: result.message
228
+ };
229
+ }
230
+
231
+ async invokeSubscriptionPanel(): Promise<
232
+ StandardResponse<{ result: 'SUCCESS' | 'FAILED'; subPlanId?: string }>
233
+ > {
234
+ const deferred = new Deferred<StandardResponse<{ result: 'SUCCESS' | 'FAILED' }>>();
235
+ let targetOrderUUID: string | undefined;
236
+
237
+ try {
238
+ console.log('[invokeSubscriptionPanel] requestSubscriptionSync params');
239
+ const response = invokeNative('invokeSubscriptionPanelSync', {
240
+ type: 'game'
241
+ });
242
+ targetOrderUUID = response.data?.subUUID;
243
+
244
+ if (!targetOrderUUID) {
245
+ throw createSubscriptionInternalError(
246
+ 'orderUUID is null',
247
+ SubscriptionErrorCodeMap.SubscriptionFailed
248
+ );
249
+ }
250
+
251
+ this.pendingSubscriptions.set(targetOrderUUID, deferred);
252
+ } catch (e) {
253
+ throw createSubscriptionInternalError(
254
+ JSON.stringify(e) ?? 'subscription panel failed',
255
+ SubscriptionErrorCodeMap.SubscriptionFailed
256
+ );
257
+ }
258
+
259
+ return deferred.promise;
260
+ }
261
+
262
+ // get tier by product id
263
+ async getTierByProductId(
264
+ productId: string
265
+ ): Promise<{ nativeProductId: string; appStoreProductId: string; basePlanId: string } | undefined> {
266
+ const serverSubInfos = await this.getSubInfo();
267
+ const serverSubInfo = serverSubInfos?.find((tier) => tier.productId === productId);
268
+
269
+ if (!serverSubInfo) {
270
+ return undefined;
271
+ }
272
+ const nativeSubInfos = SubscriptionRequestAdapter.getNativePriceCache(
273
+ (serverSubInfos ?? []).map((tier) => tier.appStoreProductId ?? '')
274
+ );
275
+
276
+ if (!nativeSubInfos) {
277
+ return undefined;
278
+ }
279
+ const nativeSubInfo = nativeSubInfos[serverSubInfo.appStoreProductId ?? ''];
280
+ if (!nativeSubInfo) {
281
+ return undefined;
282
+ }
283
+
284
+ console.log('[SubscriptionService] getTierByProductId nativeSubInfo', nativeSubInfo);
285
+ return {
286
+ nativeProductId: nativeSubInfo.productId,
287
+ appStoreProductId: serverSubInfo.appStoreProductId!,
288
+ basePlanId: nativeSubInfo.basePlanId
289
+ };
290
+ }
291
+
292
+ private listenSubscriptionPanelStateChange(): void {
293
+ onNative('onSubscriptionPanelStateChange', (data) => {
294
+ console.log('[onSubscriptionPanelStateChange] data', data);
295
+
296
+ const deferred = this.pendingSubscriptions.get(data.subUUID);
297
+ if (!deferred) {
298
+ return;
299
+ }
300
+ if (data.status === 'SUCCESS') {
301
+ deferred.resolve({
302
+ code: 'SUCCESS' as ResponseType,
303
+ message: 'subscription panel success',
304
+ data: { result: 'SUCCESS', subPlanId: data.orderResponse?.subPlanId }
305
+ });
306
+ } else {
307
+ deferred.resolve({
308
+ code: 'FAILED' as ResponseType,
309
+ message: 'subscription panel failed',
310
+ data: { result: 'FAILED' }
311
+ });
312
+ }
313
+ });
314
+ }
315
+ }
316
+
317
+ // Subscription Service implementation
318
+ export class SubscriptionService extends BaseSubscriptionService {
319
+ constructor() {
320
+ super('/api/subs/pricing');
321
+ }
322
+ }
323
+
324
+ // Service instance management
325
+ let subscriptionServiceInstance: SubscriptionService | null = null;
326
+
327
+ // Create subscription service instance
328
+ export const createSubscriptionService = (): SubscriptionService => {
329
+ if (subscriptionServiceInstance) {
330
+ return subscriptionServiceInstance;
331
+ }
332
+ subscriptionServiceInstance = new SubscriptionService();
333
+ return subscriptionServiceInstance;
334
+ };
335
+
336
+ // Export utility methods for testing
337
+ export const clearSubscriptionNativePriceCache = (): void => {
338
+ SubscriptionRequestAdapter.clearNativePriceCache();
339
+ };
@@ -0,0 +1,18 @@
1
+ export interface ISubscriptionPlanData {
2
+ basePlanId: string;
3
+ productId: string;
4
+ pricing: {
5
+ formattedPrice: string;
6
+ originalPrice?: string;
7
+ discountPercentage?: number;
8
+ };
9
+ trial?: {
10
+ duration: number;
11
+ description?: string;
12
+ };
13
+ promotion?: {
14
+ type: 'new_user' | 'limited_time' | 'winback';
15
+ badge: string;
16
+ description: string;
17
+ };
18
+ }