@jolibox/implement 1.2.2 → 1.2.4

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.
@@ -0,0 +1,279 @@
1
+ import { IPaymentChoice } from '@/common/rewards/reward-emitter';
2
+ import { paymentHelper } from './index';
3
+ import { isUndefinedOrNull } from '@jolibox/common';
4
+ import { StandardResponse } from '@jolibox/types';
5
+ import { applyNative } from '@jolibox/native-bridge';
6
+ import { innerFetch as fetch } from '@/native/network';
7
+ import type { PaymentResult } from './payment-helper';
8
+
9
+ type PaymentPurchaseType = 'JOLI_COIN' | 'JOLI_GEM';
10
+
11
+ const CoinFetchUrlMap: Record<PaymentPurchaseType, string> = {
12
+ JOLI_COIN: '/api/joli-coin/balance-detail',
13
+ JOLI_GEM: '/api/joli-gem/balance-detail'
14
+ };
15
+
16
+ // 缓存数据接口
17
+ export interface CachedPaymentChoices {
18
+ choices: IPaymentChoice[];
19
+ timestamp: number;
20
+ expiresAt: number;
21
+ productIds: string[];
22
+ }
23
+
24
+ export class PaymentService {
25
+ private productInfoCache: Record<PaymentPurchaseType, IPaymentChoice[]> = {
26
+ JOLI_COIN: [],
27
+ JOLI_GEM: []
28
+ };
29
+
30
+ // 新增:paymentChoices 缓存
31
+ private static paymentChoicesCache = new Map<string, CachedPaymentChoices>();
32
+ private static readonly PAYMENT_CHOICES_CACHE_DURATION = 10 * 60 * 1000; // 10分钟缓存
33
+
34
+ async getJolicoinProductsInfo(type: PaymentPurchaseType) {
35
+ if (this.productInfoCache[type].length > 0) {
36
+ return this.productInfoCache[type];
37
+ }
38
+ const productsInfo = await PaymentService.getProductsInfo(CoinFetchUrlMap[type]);
39
+ this.productInfoCache[type] = productsInfo?.paymentChoices ?? [];
40
+ return this.productInfoCache[type];
41
+ }
42
+
43
+ async purchase(type: PaymentPurchaseType, productId: string) {
44
+ const productsInfo = await this.getJolicoinProductsInfo(type);
45
+ const appStoreProductId = productsInfo.find(
46
+ (choice) => choice.productId === productId
47
+ )?.appStoreProductId;
48
+ if (!appStoreProductId) {
49
+ throw new Error('appStoreProductId not found');
50
+ }
51
+
52
+ const result = (await paymentHelper.invokePayment(
53
+ type === 'JOLI_COIN' ? 'JOLI_COIN_IAP' : 'JOLI_GEM_IAP',
54
+ {
55
+ productId,
56
+ appStoreProductId
57
+ }
58
+ )) as PaymentResult<{ totalAmount: string }>;
59
+
60
+ return result;
61
+ }
62
+
63
+ private static mergeResponseData(
64
+ responseData: { paymentChoices: IPaymentChoice[] },
65
+ data: { [appStoreProductId: string]: { price: string } }
66
+ ) {
67
+ Object.keys(data).forEach((key) => {
68
+ const choice = responseData.paymentChoices.find((choice) => choice.appStoreProductId === key);
69
+ if (choice) {
70
+ choice.totalAmountStr = data[key].price;
71
+ }
72
+ });
73
+
74
+ responseData.paymentChoices = responseData.paymentChoices.filter(
75
+ (choice) => !isUndefinedOrNull(choice.totalAmountStr)
76
+ ) as IPaymentChoice[];
77
+ }
78
+
79
+ static async getProductsInfo(apiEndpoint: string): Promise<
80
+ | {
81
+ balance: number;
82
+ enableAutoDeduct: boolean;
83
+ paymentChoices: IPaymentChoice[];
84
+ }
85
+ | undefined
86
+ > {
87
+ return Promise.race([
88
+ this.getProductsInfoInternal(apiEndpoint),
89
+ new Promise<never>((_, reject) => {
90
+ setTimeout(() => {
91
+ reject(new Error('[PaymentService] Request timeout after 3 seconds'));
92
+ }, 3000);
93
+ })
94
+ ]);
95
+ }
96
+
97
+ private static async getProductsInfoInternal(apiEndpoint: string): Promise<
98
+ | {
99
+ balance: number;
100
+ enableAutoDeduct: boolean;
101
+ paymentChoices: IPaymentChoice[];
102
+ }
103
+ | undefined
104
+ > {
105
+ // 首先获取服务端数据(余额等实时数据)
106
+ const { response } = await fetch<
107
+ StandardResponse<{
108
+ balance: number;
109
+ enableAutoDeduct: boolean;
110
+ paymentChoices: IPaymentChoice[];
111
+ }>
112
+ >(apiEndpoint, {
113
+ method: 'GET',
114
+ appendHostCookie: true,
115
+ responseType: 'json'
116
+ });
117
+
118
+ if (!response.data?.data) {
119
+ throw new Error('get products info failed');
120
+ }
121
+
122
+ const serverData = response.data.data;
123
+
124
+ // 检查 paymentChoices 缓存
125
+ const cachedChoices = this.getPaymentChoicesFromCache(apiEndpoint, serverData.paymentChoices);
126
+
127
+ if (cachedChoices) {
128
+ console.log('[PaymentService] Using cached paymentChoices');
129
+
130
+ // 使用缓存的 paymentChoices,但保持服务端的余额等实时数据
131
+ return {
132
+ balance: serverData.balance,
133
+ enableAutoDeduct: serverData.enableAutoDeduct,
134
+ paymentChoices: cachedChoices
135
+ };
136
+ }
137
+
138
+ console.log('[PaymentService] Fetching fresh paymentChoices from native');
139
+
140
+ // 缓存未命中,请求原生数据
141
+ const { data } = await applyNative('requestProductDetailsAsync', {
142
+ appStoreProductIds:
143
+ serverData.paymentChoices
144
+ ?.filter((choice) => typeof choice.appStoreProductId === 'string')
145
+ .map((choice) => choice.appStoreProductId as string) ?? []
146
+ });
147
+
148
+ if (data) {
149
+ this.mergeResponseData(serverData, data);
150
+ this.cachePaymentChoices(apiEndpoint, serverData.paymentChoices);
151
+ }
152
+
153
+ if (serverData.paymentChoices.length === 0) {
154
+ throw new Error('paymentChoices is empty');
155
+ }
156
+
157
+ return serverData;
158
+ }
159
+
160
+ // read paymentChoices from cache
161
+ private static getPaymentChoicesFromCache(
162
+ apiEndpoint: string,
163
+ serverChoices: IPaymentChoice[]
164
+ ): IPaymentChoice[] | null {
165
+ const cached = this.paymentChoicesCache.get(apiEndpoint);
166
+
167
+ if (!cached) {
168
+ return null;
169
+ }
170
+
171
+ if (Date.now() > cached.expiresAt) {
172
+ this.paymentChoicesCache.delete(apiEndpoint);
173
+ return null;
174
+ }
175
+
176
+ const serverProductIds = this.extractProductIds(serverChoices);
177
+ const cachedProductIds = cached.productIds;
178
+
179
+ if (!this.arraysEqual(serverProductIds, cachedProductIds)) {
180
+ console.log('[PaymentService] Server paymentChoices changed, invalidating cache');
181
+ this.paymentChoicesCache.delete(apiEndpoint);
182
+ return null;
183
+ }
184
+
185
+ const updatedChoices = this.updateChoicesWithServerData(cached.choices, serverChoices);
186
+
187
+ return updatedChoices;
188
+ }
189
+
190
+ private static cachePaymentChoices(apiEndpoint: string, choices: IPaymentChoice[]): void {
191
+ this.paymentChoicesCache.set(apiEndpoint, {
192
+ choices: JSON.parse(JSON.stringify(choices)), // deep copy
193
+ timestamp: Date.now(),
194
+ expiresAt: Date.now() + this.PAYMENT_CHOICES_CACHE_DURATION,
195
+ productIds: this.extractProductIds(choices)
196
+ });
197
+
198
+ console.log(`[PaymentService] Cached ${choices.length} payment choices for ${apiEndpoint}`);
199
+ }
200
+
201
+ private static extractProductIds(choices: IPaymentChoice[]): string[] {
202
+ return choices
203
+ .map((choice) => choice.productId)
204
+ .filter(Boolean)
205
+ .sort();
206
+ }
207
+
208
+ private static arraysEqual(arr1: string[], arr2: string[]): boolean {
209
+ if (arr1.length !== arr2.length) {
210
+ return false;
211
+ }
212
+
213
+ return arr1.every((item, index) => item === arr2[index]);
214
+ }
215
+
216
+ private static updateChoicesWithServerData(
217
+ cachedChoices: IPaymentChoice[],
218
+ serverChoices: IPaymentChoice[]
219
+ ): IPaymentChoice[] {
220
+ const serverChoiceMap = new Map(serverChoices.map((choice) => [choice.productId, choice]));
221
+
222
+ return cachedChoices.map((cachedChoice) => {
223
+ const serverChoice = serverChoiceMap.get(cachedChoice.productId);
224
+
225
+ if (serverChoice) {
226
+ return {
227
+ ...cachedChoice,
228
+ quantity: serverChoice.quantity
229
+ };
230
+ }
231
+
232
+ return cachedChoice;
233
+ });
234
+ }
235
+
236
+ // clear paymentChoices cache
237
+ public static clearPaymentChoicesCache(apiEndpoint?: string): void {
238
+ if (apiEndpoint) {
239
+ this.paymentChoicesCache.delete(apiEndpoint);
240
+ console.log(`[PaymentService] Cleared cache for ${apiEndpoint}`);
241
+ } else {
242
+ this.paymentChoicesCache.clear();
243
+ console.log('[PaymentService] Cleared all payment choices cache');
244
+ }
245
+ }
246
+
247
+ public static getPaymentChoicesCacheStats(): {
248
+ cacheCount: number;
249
+ validCount: number;
250
+ expiredCount: number;
251
+ } {
252
+ const now = Date.now();
253
+ let validCount = 0;
254
+ let expiredCount = 0;
255
+
256
+ for (const cached of this.paymentChoicesCache.values()) {
257
+ if (now > cached.expiresAt) {
258
+ expiredCount++;
259
+ } else {
260
+ validCount++;
261
+ }
262
+ }
263
+
264
+ return {
265
+ cacheCount: this.paymentChoicesCache.size,
266
+ validCount,
267
+ expiredCount
268
+ };
269
+ }
270
+
271
+ public static clearExpiredPaymentChoicesCache(): void {
272
+ const now = Date.now();
273
+ for (const [key, cached] of this.paymentChoicesCache) {
274
+ if (now > cached.expiresAt) {
275
+ this.paymentChoicesCache.delete(key);
276
+ }
277
+ }
278
+ }
279
+ }
@@ -75,7 +75,7 @@ onNative('onPaymentStateChange', (data) => {
75
75
  pendingPayments.delete(orderUUID);
76
76
  });
77
77
 
78
- const payInApp = async (params: { appStoreProductId: string; appAccountToken?: string }) => {
78
+ const purchaseGem = async (params: { appStoreProductId: string; appAccountToken?: string }) => {
79
79
  const deferred = new Deferred<StandardResponse<{ totalAmount: string }>>();
80
80
  let targetOrderUUID: string | undefined;
81
81
 
@@ -88,7 +88,7 @@ const payInApp = async (params: { appStoreProductId: string; appAccountToken?: s
88
88
  });
89
89
  targetOrderUUID = response.data?.orderUUID;
90
90
 
91
- console.info('---payInApp---', response);
91
+ console.info('---purchaseGem---', response);
92
92
  if (!targetOrderUUID) {
93
93
  throw createPaymentInternalError(
94
94
  'orderUUID is null',
@@ -134,7 +134,7 @@ class JolicoinIAPAndroidPaymentResiter extends BasePaymentRegister<
134
134
  };
135
135
 
136
136
  pay = async (): Promise<StandardResponse<{ totalAmount: string }>> => {
137
- return await payInApp({ appStoreProductId: this.appStoreProductId });
137
+ return await purchaseGem({ appStoreProductId: this.appStoreProductId });
138
138
  };
139
139
  }
140
140
 
@@ -180,7 +180,7 @@ class JolicoinIAPIOSPaymentResiter extends BasePaymentRegister<
180
180
  };
181
181
 
182
182
  pay = async (): Promise<StandardResponse<{ totalAmount: string }>> => {
183
- return await payInApp({
183
+ return await purchaseGem({
184
184
  appStoreProductId: this.appStoreProductId,
185
185
  appAccountToken: this.appAccountToken
186
186
  });
@@ -13,12 +13,15 @@ const reportNative: ReportHandler = (event, data, webviewId) => {
13
13
  : isString(_data.extra)
14
14
  ? JSON.parse(_data.extra)
15
15
  : {};
16
+ const { mpName, mpVersion } = context.mpInfo ?? {}; //
16
17
  const extra = {
17
18
  ...originExtra,
18
19
  mp_id: (_data.mp_id as string) ?? '',
19
20
  mp_version: (_data.mp_version as string) ?? '',
20
21
  session_id: context.sessionId,
21
- user_id: context.hostUserInfo?.uid ?? ''
22
+ user_id: context.hostUserInfo?.uid ?? '',
23
+ ...(mpName ? { mp_name: mpName } : {}),
24
+ ...(mpVersion ? { mp_info_version: mpVersion } : {})
22
25
  };
23
26
  const eventType = (_data.eventType ?? EventType.Other) as number;
24
27
 
@@ -1,14 +1,16 @@
1
1
  /**
2
2
  * 任务上报
3
3
  */
4
-
5
4
  import { context } from '@/common/context';
6
5
  import { TaskTracker, TaskPoint } from '@/common/report/task-track';
7
- import { EventEmitter, getApiHost } from '@jolibox/common';
6
+ import { EventEmitter, getApiHost, isBoolean } from '@jolibox/common';
8
7
  import { innerFetch as fetch } from '../network';
9
8
  import { applyNative } from '@jolibox/native-bridge';
10
9
  import type { Track } from '.';
11
10
  import type { TrackEvent } from '@jolibox/types';
11
+ import { getGlobalStorage, setGlobalStorage } from '../api/storage';
12
+
13
+ const REPORT_FIRST_OPEN_GAME = 'REPORT_FIRST_OPEN_GAME';
12
14
 
13
15
  type NativeTaskPointEvent =
14
16
  | 'OpenGame'
@@ -52,6 +54,7 @@ export class NativeTaskTracker extends TaskTracker {
52
54
  if (extraParams) {
53
55
  Object.assign(reportBody, extraParams);
54
56
  }
57
+
55
58
  const reportTask =
56
59
  context.platform === 'android'
57
60
  ? fetch(`/api/base/app-event`, {
@@ -79,4 +82,18 @@ export class NativeTaskTracker extends TaskTracker {
79
82
  tracker(event: TrackEvent, info: Record<string, unknown> | null = null) {
80
83
  this.track(event, info);
81
84
  }
85
+
86
+ start(duration?: number) {
87
+ super.start(duration);
88
+ this.tryReportOpenGamePlus();
89
+ }
90
+
91
+ private tryReportOpenGamePlus() {
92
+ getGlobalStorage(REPORT_FIRST_OPEN_GAME).then((res) => {
93
+ if (isBoolean(res.data) && res.data) {
94
+ this.tracker('OpenGame_2Plus');
95
+ }
96
+ setGlobalStorage(REPORT_FIRST_OPEN_GAME, true);
97
+ });
98
+ }
82
99
  }
@@ -23,12 +23,12 @@ import {
23
23
  import { paymentHelper } from '@/native/payment';
24
24
  import { createLoading } from '@jolibox/ui';
25
25
  import { canIUseNative } from '@/native/api/base';
26
- import { applyNative } from '@jolibox/native-bridge';
27
- import { isUndefinedOrNull } from '@jolibox/common';
26
+
28
27
  import { track } from '@/native/report';
29
28
  import { updateAutoDeductConfig } from './utils';
30
29
  import { createEventPromiseHandler } from '@/common/rewards/registers/utils/event-listener';
31
30
  import { TrackEvent } from '@jolibox/types';
31
+ import { PaymentService } from '@/native/payment/payment-service';
32
32
 
33
33
  // 货币配置映射
34
34
  interface CurrencyPaymentConfig {
@@ -134,7 +134,7 @@ rewardsEmitter.on(
134
134
  await loading.show({
135
135
  duration: 3000
136
136
  });
137
- const balenceDetails = await getBalenceDetails(paymentConfig.apiEndpoint);
137
+ const balenceDetails = await PaymentService.getProductsInfo(paymentConfig.apiEndpoint);
138
138
  loading.hide();
139
139
 
140
140
  if (!balenceDetails) {
@@ -183,7 +183,7 @@ rewardsEmitter.on(
183
183
  return;
184
184
  }
185
185
 
186
- const balenceDetails = await getBalenceDetails(paymentConfig.apiEndpoint);
186
+ const balenceDetails = await PaymentService.getProductsInfo(paymentConfig.apiEndpoint);
187
187
  if ((balenceDetails?.balance ?? 0) >= params.quantity) {
188
188
  rewardsEmitter.emit(PaymentResultEventName, {
189
189
  paymentResult: 'SUCCESS',
@@ -278,58 +278,11 @@ rewardsEmitter.on(
278
278
  }
279
279
  );
280
280
 
281
- const mergeResponseData = (
282
- responseData: { paymentChoices: IPaymentChoice[] },
283
- data: { [appStoreProductId: string]: { price: string } }
284
- ) => {
285
- Object.keys(data).forEach((key) => {
286
- const choice = responseData.paymentChoices.find((choice) => choice.appStoreProductId === key);
287
- if (choice) {
288
- choice.totalAmountStr = data[key].price;
289
- }
290
- });
291
-
292
- responseData.paymentChoices = responseData.paymentChoices.filter(
293
- (choice) => !isUndefinedOrNull(choice.totalAmountStr)
294
- ) as IPaymentChoice[];
295
- };
296
-
297
- const getBalenceDetails = async (
298
- apiEndpoint: string
299
- ): Promise<
300
- | {
301
- balance: number;
302
- enableAutoDeduct: boolean;
303
- paymentChoices: IPaymentChoice[];
304
- }
305
- | undefined
306
- > => {
307
- const { response } = await fetch<
308
- StandardResponse<{
309
- balance: number;
310
- enableAutoDeduct: boolean;
311
- paymentChoices: IPaymentChoice[];
312
- }>
313
- >(apiEndpoint, {
314
- method: 'GET',
315
- appendHostCookie: true,
316
- responseType: 'json'
317
- });
318
-
319
- console.info('getBalenceDetails', response);
320
- const { data } = await applyNative('requestProductDetailsAsync', {
321
- appStoreProductIds:
322
- response.data?.data?.paymentChoices
323
- ?.filter((choice) => typeof choice.appStoreProductId === 'string')
324
- .map((choice) => choice.appStoreProductId as string) ?? []
325
- });
281
+ /** preload payment details */
326
282
 
327
- if (response.data?.data && data) {
328
- mergeResponseData(response.data?.data, data);
329
- }
330
- console.info('productDetails', response.data?.data);
331
- if (response.data?.data?.paymentChoices.length === 0) {
332
- throw new Error('paymentChoices is empty');
333
- }
334
- return response.data?.data;
335
- };
283
+ PaymentService.getProductsInfo(CURRENCY_PAYMENT_CONFIG.JOLI_COIN.apiEndpoint).catch((e) => {
284
+ console.error('preload joli coin payment details failed', e);
285
+ });
286
+ PaymentService.getProductsInfo(CURRENCY_PAYMENT_CONFIG.JOLI_GEM.apiEndpoint).catch((e) => {
287
+ console.error('preload joli gem payment details failed', e);
288
+ });