@jolibox/implement 1.2.8 → 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 (65) hide show
  1. package/.rush/temp/package-deps_build.json +37 -27
  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/registers/use-gem-only.ts +5 -2
  32. package/src/common/rewards/registers/use-gem.ts +5 -2
  33. package/src/common/rewards/registers/use-jolicoin-only.ts +5 -2
  34. package/src/common/rewards/registers/use-jolicoin.ts +5 -2
  35. package/src/common/rewards/registers/use-subscription.ts +5 -2
  36. package/src/common/rewards/registers/utils/coins/jolicoin/jolicoin-handler.ts +33 -11
  37. package/src/common/rewards/registers/utils/coins/joligem/gem-handler.ts +34 -13
  38. package/src/common/rewards/registers/utils/common.ts +9 -0
  39. package/src/common/rewards/registers/utils/subscription/commands/use-subscription.ts +16 -0
  40. package/src/common/rewards/registers/utils/subscription/sub-handler.ts +22 -6
  41. package/src/common/rewards/reward-emitter.ts +8 -0
  42. package/src/common/rewards/type.ts +1 -1
  43. package/src/common/utils/index.ts +1 -1
  44. package/src/h5/api/ads.ts +6 -3
  45. package/src/h5/bootstrap/auth/__tests__/auth.test.ts +15 -9
  46. package/src/h5/bootstrap/auth/sub.ts +1 -1
  47. package/src/native/api/ads.ts +43 -6
  48. package/src/native/api/call-host-method.ts +5 -61
  49. package/src/native/api/login.ts +22 -7
  50. package/src/native/api/payment.ts +78 -3
  51. package/src/native/payment/__tests__/payment-service-simple.test.ts +14 -1
  52. package/src/native/payment/payment-service.ts +26 -27
  53. package/src/native/payment/utils/__tests__/cache-with-storage.test.ts +414 -0
  54. package/src/native/payment/utils/cache-with-storage.ts +112 -0
  55. package/src/native/rewards/check-frequency.ts +6 -0
  56. package/src/native/rewards/index.ts +1 -0
  57. package/src/native/rewards/ui/payment-modal.ts +2 -2
  58. package/src/native/rewards/ui/subscription-modal.ts +81 -0
  59. package/src/native/subscription/index.ts +12 -0
  60. package/src/native/subscription/registers/base.ts +88 -0
  61. package/src/native/subscription/registers/sub-app.ts +258 -0
  62. package/src/native/subscription/registers/type.ts +13 -0
  63. package/src/native/subscription/subscription-helper.ts +53 -0
  64. package/src/native/subscription/subscription-service.ts +339 -0
  65. package/src/native/subscription/type.ts +18 -0
@@ -8,7 +8,7 @@ export interface IGem {
8
8
  enableAutoDeduct: boolean;
9
9
  }
10
10
 
11
- export type IUnlockOptionType = 'JOLI_COIN' | 'ADS' | 'JOLI_GEM';
11
+ export type IUnlockOptionType = 'JOLI_COIN' | 'ADS' | 'JOLI_GEM' | 'SUBSCRIPTION';
12
12
 
13
13
  interface IJoliCoinChoice {
14
14
  joliCoinQuantity: number;
@@ -78,7 +78,7 @@ const ON_JOLIBOX_JOLI_COIN_USE_RESULT = 'ON_JOLIBOX_JOLI_COIN_USE_RESULT';
78
78
  const ON_JOLIBOX_JOLI_UNLOGIN_MODAL_RESULT_EVENT = 'ON_JOLIBOX_JOLI_UNLOGIN_MODAL_RESULT_EVENT';
79
79
 
80
80
  // subscription
81
- const ON_GET_USER_SUB_STATUS = 'ON_GET_USER_SUB_STATUS';
81
+ const ON_GET_USER_SUB_STATUS = 'ON_JOLIBOX_GET_USER_SUB_STATUS';
82
82
  const ON_JOLIBOX_SUB_RESULT_EVENT = 'ON_JOLIBOX_SUB_RESULT_EVENT';
83
83
 
84
84
  interface ReceivedJoliboxCustomEvent {
package/src/h5/api/ads.ts CHANGED
@@ -150,21 +150,24 @@ rewardsHelper.registerRewardHandler('ADS', createAdsRewardHandler(adsHandler.get
150
150
  rewardsHelper.registerRewardHandler(
151
151
  'JOLI_COIN',
152
152
  createJolicoinRewardHandler(httpClient, {
153
- onUnlockSuccess: handleUnlockSuccess.bind(this)
153
+ onUnlockSuccess: handleUnlockSuccess.bind(this),
154
+ hasSubscription: () => true
154
155
  }) as unknown as (params?: unknown) => Promise<boolean>
155
156
  );
156
157
 
157
158
  rewardsHelper.registerRewardHandler(
158
159
  'JOLI_COIN_ONLY',
159
160
  createJolicoinOnlyRewardHandler(httpClient, {
160
- onUnlockSuccess: handleUnlockSuccess.bind(this)
161
+ onUnlockSuccess: handleUnlockSuccess.bind(this),
162
+ hasSubscription: () => true
161
163
  }) as unknown as (params?: unknown) => Promise<boolean>
162
164
  );
163
165
 
164
166
  rewardsHelper.registerRewardHandler(
165
167
  'SUBSCRIPTION',
166
168
  createSubscriptionRewardHandler(httpClient, {
167
- onSubSuccess: handleUnlockWithSubscriptionSuccess.bind(this)
169
+ onSubSuccess: handleUnlockWithSubscriptionSuccess.bind(this),
170
+ hasSubscription: () => true
168
171
  }) as unknown as (params?: unknown) => Promise<boolean>
169
172
  );
170
173
 
@@ -49,7 +49,7 @@ describe('getUserSubStatus', () => {
49
49
  it('should resolve with correct value when event is received', async () => {
50
50
  let eventHandler: ((data: EventData) => void) | null = null;
51
51
  mockOnCustomEvent.mockImplementation((eventName: string, handler: (data: EventData) => void) => {
52
- if (eventName === 'ON_GET_USER_SUB_STATUS') {
52
+ if (eventName === 'ON_JOLIBOX_GET_USER_SUB_STATUS') {
53
53
  eventHandler = handler;
54
54
  }
55
55
  return jest.fn();
@@ -79,7 +79,7 @@ describe('getUserSubStatus', () => {
79
79
  it('should resolve with false when isSubUser is false', async () => {
80
80
  let eventHandler: ((data: EventData) => void) | null = null;
81
81
  mockOnCustomEvent.mockImplementation((eventName: string, handler: (data: EventData) => void) => {
82
- if (eventName === 'ON_GET_USER_SUB_STATUS') {
82
+ if (eventName === 'ON_JOLIBOX_GET_USER_SUB_STATUS') {
83
83
  eventHandler = handler;
84
84
  }
85
85
  return jest.fn();
@@ -102,6 +102,8 @@ describe('getUserSubStatus', () => {
102
102
  });
103
103
 
104
104
  it('should reject on timeout', async () => {
105
+ jest.useFakeTimers();
106
+
105
107
  mockOnCustomEvent.mockImplementation(() => jest.fn());
106
108
  mockUuidv4.mockReturnValue('timeout-test-id');
107
109
 
@@ -111,12 +113,14 @@ describe('getUserSubStatus', () => {
111
113
  jest.advanceTimersByTime(3000);
112
114
 
113
115
  await expect(promise).rejects.toThrow('Timeout waiting for user sub status response');
116
+
117
+ jest.useRealTimers();
114
118
  });
115
119
 
116
120
  it('should initialize event listener only once', async () => {
117
121
  let eventHandler: ((data: EventData) => void) | null = null;
118
122
  mockOnCustomEvent.mockImplementation((eventName: string, handler: (data: EventData) => void) => {
119
- if (eventName === 'ON_GET_USER_SUB_STATUS') {
123
+ if (eventName === 'ON_JOLIBOX_GET_USER_SUB_STATUS') {
120
124
  eventHandler = handler;
121
125
  }
122
126
  return jest.fn();
@@ -133,7 +137,7 @@ describe('getUserSubStatus', () => {
133
137
 
134
138
  // Should only initialize listener once
135
139
  expect(mockOnCustomEvent).toHaveBeenCalledTimes(1);
136
- expect(mockOnCustomEvent).toHaveBeenCalledWith('ON_GET_USER_SUB_STATUS', expect.any(Function));
140
+ expect(mockOnCustomEvent).toHaveBeenCalledWith('ON_JOLIBOX_GET_USER_SUB_STATUS', expect.any(Function));
137
141
 
138
142
  // Should send notification for both calls
139
143
  expect(mockNotifyCustomEvent).toHaveBeenCalledTimes(2);
@@ -152,7 +156,7 @@ describe('getUserSubStatus', () => {
152
156
  it('should ignore responses with different sequenceId', async () => {
153
157
  let eventHandler: ((data: EventData) => void) | null = null;
154
158
  mockOnCustomEvent.mockImplementation((eventName: string, handler: (data: EventData) => void) => {
155
- if (eventName === 'ON_GET_USER_SUB_STATUS') {
159
+ if (eventName === 'ON_JOLIBOX_GET_USER_SUB_STATUS') {
156
160
  eventHandler = handler;
157
161
  }
158
162
  return jest.fn();
@@ -201,7 +205,7 @@ describe('getUserSubStatus', () => {
201
205
  it('should handle multiple concurrent requests with different sequenceIds', async () => {
202
206
  let eventHandler: ((data: EventData) => void) | null = null;
203
207
  mockOnCustomEvent.mockImplementation((eventName: string, handler: (data: EventData) => void) => {
204
- if (eventName === 'ON_GET_USER_SUB_STATUS') {
208
+ if (eventName === 'ON_JOLIBOX_GET_USER_SUB_STATUS') {
205
209
  eventHandler = handler;
206
210
  }
207
211
  return jest.fn();
@@ -248,7 +252,7 @@ describe('getUserSubStatus', () => {
248
252
  it('should clean up timeout when response is received', async () => {
249
253
  let eventHandler: ((data: EventData) => void) | null = null;
250
254
  mockOnCustomEvent.mockImplementation((eventName: string, handler: (data: EventData) => void) => {
251
- if (eventName === 'ON_GET_USER_SUB_STATUS') {
255
+ if (eventName === 'ON_JOLIBOX_GET_USER_SUB_STATUS') {
252
256
  eventHandler = handler;
253
257
  }
254
258
  return jest.fn();
@@ -278,9 +282,11 @@ describe('getUserSubStatus', () => {
278
282
  });
279
283
 
280
284
  it('should not resolve after timeout even if response comes later', async () => {
285
+ jest.useFakeTimers();
286
+
281
287
  let eventHandler: ((data: EventData) => void) | null = null;
282
288
  mockOnCustomEvent.mockImplementation((eventName: string, handler: (data: EventData) => void) => {
283
- if (eventName === 'ON_GET_USER_SUB_STATUS') {
289
+ if (eventName === 'ON_JOLIBOX_GET_USER_SUB_STATUS') {
284
290
  eventHandler = handler;
285
291
  }
286
292
  return jest.fn();
@@ -303,6 +309,6 @@ describe('getUserSubStatus', () => {
303
309
  });
304
310
  }
305
311
 
306
- // Promise should already be rejected, no additional effects
312
+ jest.useRealTimers();
307
313
  });
308
314
  });
@@ -14,7 +14,7 @@ let isListenerInitialized = false;
14
14
  const initializeEventListener = () => {
15
15
  if (isListenerInitialized) return;
16
16
 
17
- onCustomEvent('ON_GET_USER_SUB_STATUS', (data) => {
17
+ onCustomEvent('ON_JOLIBOX_GET_USER_SUB_STATUS', (data) => {
18
18
  const pendingRequest = pendingRequests.get(data.sequenceId);
19
19
  if (pendingRequest) {
20
20
  clearTimeout(pendingRequest.timeoutId);
@@ -1,5 +1,5 @@
1
1
  import { createCommands } from '@jolibox/common';
2
- import { createSyncAPI, registerCanIUse } from './base';
2
+ import { canIUseNative, createSyncAPI, registerCanIUse } from './base';
3
3
  import { createToast } from '@jolibox/ui';
4
4
  import '../rewards';
5
5
 
@@ -27,7 +27,8 @@ import {
27
27
  createGemRewardHandler,
28
28
  createGemOnlyRewardHandler,
29
29
  warmupGemBalanceCache,
30
- warmupBalanceCache
30
+ warmupBalanceCache,
31
+ createSubscriptionRewardHandler
31
32
  } from '@/common/rewards';
32
33
  import { adEventEmitter } from '@/common/ads';
33
34
  import { warmupRewardCache } from '@/common/rewards/fetch-reward';
@@ -107,7 +108,8 @@ rewardsHelper.registerRewardHandler(
107
108
  },
108
109
  onUnlockFailed: () => {
109
110
  console.log('onUnlockFailed');
110
- }
111
+ },
112
+ hasSubscription: () => canIUseNative('invokeSubscriptionPanelSync')
111
113
  }) as unknown as (params?: unknown) => Promise<boolean>
112
114
  );
113
115
 
@@ -123,7 +125,8 @@ rewardsHelper.registerRewardHandler(
123
125
  },
124
126
  onUnlockFailed: () => {
125
127
  console.log('onUnlockFailed');
126
- }
128
+ },
129
+ hasSubscription: () => canIUseNative('invokeSubscriptionPanelSync')
127
130
  }) as unknown as (params?: unknown) => Promise<boolean>
128
131
  );
129
132
 
@@ -136,7 +139,8 @@ rewardsHelper.registerRewardHandler(
136
139
  track('gem_unlock_success', {
137
140
  quantity: params.quantity
138
141
  });
139
- }
142
+ },
143
+ hasSubscription: () => canIUseNative('invokeSubscriptionPanelSync')
140
144
  }) as unknown as (params?: unknown) => Promise<boolean>
141
145
  );
142
146
 
@@ -149,7 +153,20 @@ rewardsHelper.registerRewardHandler(
149
153
  quantity: params.quantity,
150
154
  isOnly: true
151
155
  });
152
- }
156
+ },
157
+ hasSubscription: () => canIUseNative('invokeSubscriptionPanelSync')
158
+ }) as unknown as (params?: unknown) => Promise<boolean>
159
+ );
160
+
161
+ const handleUnlockWithSubscriptionSuccess = () => {
162
+ track('subscription_unlock_success', {});
163
+ };
164
+
165
+ rewardsHelper.registerRewardHandler(
166
+ 'SUBSCRIPTION',
167
+ createSubscriptionRewardHandler(httpClient, {
168
+ onSubSuccess: handleUnlockWithSubscriptionSuccess.bind(this),
169
+ hasSubscription: () => canIUseNative('invokeSubscriptionPanelSync')
153
170
  }) as unknown as (params?: unknown) => Promise<boolean>
154
171
  );
155
172
 
@@ -217,8 +234,28 @@ const adConfig = createSyncAPI('adConfig', {
217
234
  });
218
235
 
219
236
  let rewardLocked = false;
237
+
238
+ const wrapAdBreadDoneViewed = (params: IAdBreakParams) => {
239
+ params.adBreakDone?.({
240
+ breakType: params.type,
241
+ breakName: 'name' in params ? params.name ?? '' : '',
242
+ breakFormat: params.type === 'reward' ? 'reward' : 'interstitial',
243
+ breakStatus: 'viewed'
244
+ });
245
+ if ('adViewed' in params) {
246
+ params.adViewed?.();
247
+ }
248
+ };
249
+
220
250
  const adBreak = createSyncAPI('adBreak', {
221
251
  implement: (params: IAdBreakParams) => {
252
+ // check is sub user
253
+ const isSubUser = context.hostUserInfo?.isSubUser;
254
+ if (isSubUser) {
255
+ wrapAdBreadDoneViewed(params);
256
+ return;
257
+ }
258
+
222
259
  if (params.type === 'reward') {
223
260
  if (rewardLocked) {
224
261
  return;
@@ -1,65 +1,10 @@
1
- import { createAPI, registerCanIUse, t } from './base';
1
+ import { canIUseNative, createAPI, registerCanIUse, t } from './base';
2
2
  import { invokeNative } from '@jolibox/native-bridge';
3
- import { hostEmitter, UserCustomError } from '@jolibox/common';
3
+ import { UserCustomError } from '@jolibox/common';
4
4
  import { createCommands } from '@jolibox/common';
5
5
  import { context } from '@/common/context';
6
6
 
7
7
  const commands = createCommands();
8
- let supportedMethods: string[] = [];
9
- let isInitialized = false;
10
-
11
- const formatSupportedMethods = (
12
- methods: (
13
- | string
14
- | {
15
- method: string;
16
- platforms?: string[];
17
- nativeVersionCode?: number;
18
- }
19
- )[]
20
- ) => {
21
- const platform = context.platform;
22
- const nativeVersionCode = context.sdkInfo.nativeSDKVersionCode ?? 0;
23
- return methods.reduce((acc, method) => {
24
- if (typeof method === 'string') {
25
- acc.push(method);
26
- } else {
27
- const { method: platformMethod, platforms, nativeVersionCode: methodNativeVersionCode } = method;
28
- let canUse = true;
29
- if (platforms && !platforms.includes(platform)) {
30
- canUse = false;
31
- }
32
- if (methodNativeVersionCode && methodNativeVersionCode > nativeVersionCode) {
33
- canUse = false;
34
- }
35
- if (canUse) {
36
- acc.push(platformMethod);
37
- }
38
- }
39
- return acc;
40
- }, [] as string[]);
41
- };
42
-
43
- hostEmitter.on('onGlobalConfigChanged', (params) => {
44
- supportedMethods = formatSupportedMethods(params.globalConfig?.supportedHostMethods ?? []);
45
- isInitialized = true;
46
- });
47
-
48
- const getSupportedMethods = (): Promise<string[]> => {
49
- return new Promise((resolve) => {
50
- if (isInitialized) {
51
- resolve(supportedMethods);
52
- } else {
53
- const handler = (params: any) => {
54
- supportedMethods = formatSupportedMethods(params.globalConfig.supportedHostMethods ?? []);
55
- isInitialized = true;
56
- hostEmitter.off('onGlobalConfigChanged', handler);
57
- resolve(supportedMethods);
58
- };
59
- hostEmitter.on('onGlobalConfigChanged', handler);
60
- }
61
- });
62
- };
63
8
 
64
9
  const callHostMethodAsync = createAPI('callHostMethod', {
65
10
  paramsSchema: t.tuple(
@@ -69,10 +14,10 @@ const callHostMethodAsync = createAPI('callHostMethod', {
69
14
  })
70
15
  ),
71
16
  implement: async ({ method, params }) => {
72
- const supportedMethods = await getSupportedMethods();
73
- if (!supportedMethods.includes(method)) {
17
+ if (!canIUseNative(method)) {
74
18
  throw new UserCustomError('Method not supported', 10001);
75
19
  }
20
+
76
21
  const { errNo, errMsg, data } = await invokeNative('callHostMethodAsync', {
77
22
  method,
78
23
  params: {
@@ -106,8 +51,7 @@ const userTrackAsync = createAPI('userTrackAsync', {
106
51
  })
107
52
  ),
108
53
  implement: async ({ event, eventType, params }) => {
109
- const supportedMethods = await getSupportedMethods();
110
- if (!supportedMethods.includes('userTrackAsync')) {
54
+ if (!canIUseNative('userTrackAsync')) {
111
55
  throw new UserCustomError('Method not supported', 10001);
112
56
  }
113
57
  const { errNo, errMsg } = await invokeNative('trackAsync', {
@@ -19,19 +19,27 @@ registerCanIUse('login', {
19
19
  registerCanIUse('checkSession', {
20
20
  version: '1.0.0',
21
21
  success: {
22
- errMsg: '1.0.0'
22
+ errMsg: '1.0.0',
23
+ isLogin: '1.0.0',
24
+ isSubUser: '1.2.9'
23
25
  }
24
26
  });
25
27
 
26
28
  const loginDeferredMap = new Map<
27
29
  string,
28
- Deferred<{ isLogin: boolean; token?: string; isFirstLogin?: boolean; extra?: LoginExtra }>
30
+ Deferred<{
31
+ isLogin: boolean;
32
+ token?: string;
33
+ isFirstLogin?: boolean;
34
+ extra?: LoginExtra;
35
+ appAccountToken?: string;
36
+ }>
29
37
  >();
30
38
 
31
- onNative('onLoginStateChange', ({ isLogin, token, uuid, isFirstLogin, extra }) => {
39
+ onNative('onLoginStateChange', ({ isLogin, token, uuid, isFirstLogin, extra, appAccountToken }) => {
32
40
  const deferred = loginDeferredMap.get(uuid);
33
41
  if (deferred) {
34
- deferred.resolve({ isLogin, token, isFirstLogin, extra });
42
+ deferred.resolve({ isLogin, token, isFirstLogin, extra, appAccountToken });
35
43
  loginDeferredMap.delete(uuid);
36
44
  }
37
45
  });
@@ -81,12 +89,16 @@ export const loginImplement = async (
81
89
  isLogin: boolean;
82
90
  token?: string;
83
91
  isFirstLogin?: boolean;
92
+ appAccountToken?: string;
84
93
  extra?: LoginExtra;
85
94
  }>();
86
95
  loginDeferredMap.set(loginUUID, deferred);
87
96
  const loginRes = await deferred.promise;
88
- context.onEnvConfigChanged({ hostUserInfo: loginRes });
89
- const { extra, ...rest } = loginRes;
97
+ // update sub user status
98
+ const env = invokeNative('envSync');
99
+ const isSubUser = env?.data?.hostUserInfo?.isSubUser;
100
+ context.onEnvConfigChanged({ hostUserInfo: { ...loginRes, isSubUser } });
101
+ const { extra, appAccountToken, ...rest } = loginRes;
90
102
  hostEmitter.emit('onLoginComplete', rest);
91
103
  return needExtra ? loginRes : rest;
92
104
  };
@@ -110,8 +122,11 @@ const checkSession = createAPI('checkSession', {
110
122
  const {
111
123
  data: { isLogin }
112
124
  } = await applyNative('checkLoginAsync');
125
+
126
+ const isSubUser = context.hostUserInfo?.isSubUser;
113
127
  return {
114
- isLogin
128
+ isLogin,
129
+ isSubUser
115
130
  };
116
131
  }
117
132
  });
@@ -3,6 +3,8 @@ import { createAPI, registerCanIUse, t } from './base';
3
3
  import { createJoliGemPaymentService } from '../payment/payment-service';
4
4
  import { createAPIError } from '@/common/report/errors';
5
5
  import { IPaymentChoice } from '@jolibox/ui/dist/bridge/coin';
6
+ import { ISubscriptionTierData } from '@jolibox/types';
7
+ import { subscriptionService } from '@/native/subscription';
6
8
 
7
9
  const commands = createCommands();
8
10
  const paymentService = createJoliGemPaymentService();
@@ -13,10 +15,10 @@ const purchaseGem = createAPI('purchaseGem', {
13
15
  productId: t.string()
14
16
  })
15
17
  ),
16
- implement: async (params: { productId: string }): Promise<{ totalAmount: string }> => {
18
+ implement: async (params: { productId: string }): Promise<{ totalAmount?: string }> => {
17
19
  const result = await paymentService.purchase(params.productId);
18
20
 
19
- if (result.code !== 'SUCCESS' || !result.data?.totalAmount) {
21
+ if (result.code !== 'SUCCESS') {
20
22
  throw createAPIError({
21
23
  code: -1,
22
24
  msg: '[JoliboxSDK]: purchase gem failed: ' + result.message
@@ -24,7 +26,7 @@ const purchaseGem = createAPI('purchaseGem', {
24
26
  }
25
27
 
26
28
  return {
27
- totalAmount: result.data?.totalAmount
29
+ totalAmount: result.data?.totalAmount ?? ''
28
30
  };
29
31
  }
30
32
  });
@@ -44,9 +46,69 @@ const getGemProducts = createAPI('getGemProducts', {
44
46
  }
45
47
  });
46
48
 
49
+ const getSubscriptionPlans = createAPI('getSubscriptionPlans', {
50
+ paramsSchema: t.tuple(),
51
+ implement: async (): Promise<{ plans: ISubscriptionTierData[] } | undefined> => {
52
+ const subPlans = await subscriptionService.getSubInfo();
53
+
54
+ console.log('[PaymentAPI] getSubscriptionPlans result', subPlans);
55
+ if (!subPlans) {
56
+ throw createAPIError({
57
+ code: -1,
58
+ msg: '[JoliboxSDK]: get subscription plans failed'
59
+ });
60
+ }
61
+ return {
62
+ plans: subPlans
63
+ };
64
+ }
65
+ });
66
+
67
+ const flushSubInfoCache = createAPI('flushSubInfoCache', {
68
+ paramsSchema: t.tuple(),
69
+ implement: async (): Promise<void> => {
70
+ await subscriptionService.updateAllNativePriceCache();
71
+ }
72
+ });
73
+
74
+ const subscribe = createAPI('subscribe', {
75
+ paramsSchema: t.tuple(
76
+ t.object({
77
+ productId: t.string()
78
+ })
79
+ ),
80
+ implement: async (params: {
81
+ productId: string;
82
+ }): Promise<{
83
+ subPlanId: string;
84
+ errMsg: string;
85
+ result: 'SUCCESS' | 'FAILED' | 'FAILURE_SUBSCRIPTED';
86
+ }> => {
87
+ const tier = await subscriptionService.getTierByProductId(params.productId);
88
+ if (!tier) {
89
+ throw createAPIError({
90
+ code: -1,
91
+ msg: '[JoliboxSDK]: product not found'
92
+ });
93
+ }
94
+ const { basePlanId, appStoreProductId, nativeProductId } = tier;
95
+ const result = await subscriptionService.subscribe({ basePlanId, nativeProductId, appStoreProductId });
96
+ return {
97
+ result: result.result,
98
+ subPlanId: result.subPlanId,
99
+ errMsg: result.message ?? ''
100
+ };
101
+ }
102
+ });
103
+
47
104
  commands.registerCommand('PaymentSDK.purchaseGem', purchaseGem);
48
105
  commands.registerCommand('PaymentSDK.getGemProducts', getGemProducts);
49
106
 
107
+ commands.registerCommand('PaymentSDK.getSubscriptionPlans', getSubscriptionPlans);
108
+ commands.registerCommand('PaymentSDK.subscribe', subscribe);
109
+
110
+ commands.registerCommand('PaymentSDK.flushSubInfoCache', flushSubInfoCache);
111
+
50
112
  registerCanIUse('payment.purchaseGem', {
51
113
  version: '1.2.3'
52
114
  });
@@ -54,3 +116,16 @@ registerCanIUse('payment.purchaseGem', {
54
116
  registerCanIUse('payment.getGemProducts', {
55
117
  version: '1.2.3'
56
118
  });
119
+
120
+ // for test, correct version is 1.3.0
121
+ registerCanIUse('payment.getSubscriptionPlans', {
122
+ version: '1.2.5'
123
+ });
124
+
125
+ registerCanIUse('payment.subscribe', {
126
+ version: '1.2.5'
127
+ });
128
+
129
+ registerCanIUse('payment.flushSubInfoCache', {
130
+ version: '1.2.5'
131
+ });
@@ -9,13 +9,26 @@ import {
9
9
  // Mock dependencies
10
10
  jest.mock('@jolibox/native-bridge', () => ({
11
11
  applyNative: jest.fn(),
12
- onNative: jest.fn()
12
+ onNative: jest.fn(),
13
+ invokeNative: jest.fn()
13
14
  }));
14
15
 
15
16
  jest.mock('@/native/network', () => ({
16
17
  innerFetch: jest.fn()
17
18
  }));
18
19
 
20
+ jest.mock('@/native/api/storage', () => ({
21
+ setGlobalStorage: jest.fn().mockResolvedValue({
22
+ code: 'SUCCESS',
23
+ message: 'success'
24
+ }),
25
+ getGlobalStorage: jest.fn().mockResolvedValue({
26
+ code: 'SUCCESS',
27
+ message: 'success',
28
+ data: null
29
+ })
30
+ }));
31
+
19
32
  // eslint-disable-next-line @typescript-eslint/no-var-requires
20
33
  const { applyNative } = require('@jolibox/native-bridge');
21
34
 
@@ -6,7 +6,7 @@ import { applyNative } from '@jolibox/native-bridge';
6
6
  import { innerFetch as fetch } from '@/native/network';
7
7
  import type { PaymentResult } from './payment-helper';
8
8
  import { RequestCacheService, RequestAdapter } from '@jolibox/common';
9
-
9
+ import { executeWithPersistentCache } from './utils/cache-with-storage';
10
10
  type PaymentPurchaseType = 'JOLI_COIN' | 'JOLI_GEM';
11
11
 
12
12
  // Request/Response interfaces for RequestCacheService
@@ -51,7 +51,7 @@ class PaymentRequestAdapter
51
51
  {
52
52
  // applyNative 的缓存
53
53
  private static nativePriceCache = new Map<string, { [appStoreProductId: string]: { price: string } }>();
54
- private static readonly NATIVE_PRICE_CACHE_DURATION = 30 * 60 * 1000; // 30分钟缓存
54
+ private static readonly NATIVE_PRICE_CACHE_DURATION = 1 * 24 * 60 * 60 * 1000; // 1 day
55
55
  private static nativePriceCacheTimestamp = new Map<string, number>();
56
56
 
57
57
  async execute(endpoint: string, _options?: PaymentRequest): Promise<PaymentResponse> {
@@ -92,34 +92,33 @@ class PaymentRequestAdapter
92
92
  private async getNativePriceData(
93
93
  appStoreProductIds: string[]
94
94
  ): Promise<{ [appStoreProductId: string]: { price: string } } | null> {
95
- // 检查缓存
96
95
  const cacheKey = appStoreProductIds.sort().join(',');
97
- const cached = PaymentRequestAdapter.nativePriceCache.get(cacheKey);
98
- const cacheTime = PaymentRequestAdapter.nativePriceCacheTimestamp.get(cacheKey) || 0;
99
-
100
- if (cached && Date.now() - cacheTime < PaymentRequestAdapter.NATIVE_PRICE_CACHE_DURATION) {
101
- console.log('[PaymentService] Using cached native price data');
102
- return cached;
103
- }
104
96
 
105
- console.log('[PaymentService] Fetching fresh native price data');
106
-
107
- try {
108
- const { data } = await applyNative('requestProductDetailsAsync', {
109
- appStoreProductIds
110
- });
111
-
112
- if (data) {
113
- // cache
114
- PaymentRequestAdapter.nativePriceCache.set(cacheKey, data);
115
- PaymentRequestAdapter.nativePriceCacheTimestamp.set(cacheKey, Date.now());
97
+ return executeWithPersistentCache(
98
+ cacheKey,
99
+ async () => {
100
+ try {
101
+ const { data } = await applyNative('requestProductDetailsAsync', {
102
+ appStoreProductIds
103
+ });
104
+
105
+ if (data) {
106
+ // cache
107
+ PaymentRequestAdapter.nativePriceCache.set(cacheKey, data);
108
+ PaymentRequestAdapter.nativePriceCacheTimestamp.set(cacheKey, Date.now());
109
+ }
110
+
111
+ return data;
112
+ } catch (error) {
113
+ console.warn('[PaymentService] Failed to fetch native product details:', error);
114
+ throw error; // rethrow error, let upper layer handle it
115
+ }
116
+ },
117
+ {
118
+ cacheDuration: PaymentRequestAdapter.NATIVE_PRICE_CACHE_DURATION,
119
+ storageKeyPrefix: 'joli_native_price_'
116
120
  }
117
-
118
- return data;
119
- } catch (error) {
120
- console.warn('[PaymentService] Failed to fetch native product details:', error);
121
- throw error; // rethrow error, let upper layer handle it
122
- }
121
+ );
123
122
  }
124
123
 
125
124
  extractCacheableData(response: PaymentResponse): PaymentCacheData {