@jolibox/implement 1.2.3 → 1.2.5-beta.3

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 (74) hide show
  1. package/.rush/temp/package-deps_build.json +44 -28
  2. package/.rush/temp/shrinkwrap-deps.json +2 -1
  3. package/CHANGELOG.json +11 -0
  4. package/CHANGELOG.md +9 -0
  5. package/dist/common/context/index.d.ts +5 -0
  6. package/dist/common/report/base-tracker.d.ts +2 -1
  7. package/dist/common/rewards/cached-fetch-reward.d.ts +46 -0
  8. package/dist/common/rewards/cached-reward-service.d.ts +24 -0
  9. package/dist/common/rewards/fetch-reward.d.ts +2 -3
  10. package/dist/common/rewards/index.d.ts +3 -0
  11. package/dist/common/rewards/registers/use-subscription.d.ts +7 -0
  12. package/dist/common/rewards/registers/utils/coins/jolicoin/cached-fetch-balance.d.ts +34 -0
  13. package/dist/common/rewards/registers/utils/coins/jolicoin/fetch-balance.d.ts +2 -1
  14. package/dist/common/rewards/registers/utils/coins/joligem/cached-fetch-gem-balance.d.ts +34 -0
  15. package/dist/common/rewards/registers/utils/coins/joligem/fetch-gem-balance.d.ts +2 -1
  16. package/dist/common/rewards/registers/utils/subscription/commands/index.d.ts +1 -0
  17. package/dist/common/rewards/registers/utils/subscription/commands/use-subscription.d.ts +4 -0
  18. package/dist/common/rewards/registers/utils/subscription/sub-handler.d.ts +13 -0
  19. package/dist/common/rewards/reward-emitter.d.ts +7 -0
  20. package/dist/common/rewards/reward-helper.d.ts +2 -1
  21. package/dist/common/utils/index.d.ts +18 -0
  22. package/dist/h5/api/platformAdsHandle/JoliboxAdsHandler.d.ts +1 -0
  23. package/dist/h5/bootstrap/auth/__tests__/auth.test.d.ts +1 -0
  24. package/dist/h5/bootstrap/auth/index.d.ts +2 -0
  25. package/dist/h5/bootstrap/auth/sub.d.ts +2 -0
  26. package/dist/index.js +9 -13
  27. package/dist/index.native.js +49 -53
  28. package/dist/native/api/index.d.ts +1 -0
  29. package/dist/native/api/payment.d.ts +1 -0
  30. package/dist/native/payment/__tests__/payment-service-simple.test.d.ts +1 -0
  31. package/dist/native/payment/payment-helper.d.ts +8 -5
  32. package/dist/native/payment/payment-service.d.ts +44 -0
  33. package/implement.build.log +2 -2
  34. package/package.json +8 -7
  35. package/src/common/context/index.ts +12 -0
  36. package/src/common/report/base-tracker.ts +2 -2
  37. package/src/common/rewards/cached-fetch-reward.ts +258 -0
  38. package/src/common/rewards/cached-reward-service.ts +255 -0
  39. package/src/common/rewards/fetch-reward.ts +17 -93
  40. package/src/common/rewards/index.ts +4 -0
  41. package/src/common/rewards/registers/use-subscription.ts +34 -0
  42. package/src/common/rewards/registers/utils/coins/jolicoin/cached-fetch-balance.ts +177 -0
  43. package/src/common/rewards/registers/utils/coins/jolicoin/fetch-balance.ts +13 -1
  44. package/src/common/rewards/registers/utils/coins/jolicoin/jolicoin-handler.ts +2 -0
  45. package/src/common/rewards/registers/utils/coins/joligem/cached-fetch-gem-balance.ts +181 -0
  46. package/src/common/rewards/registers/utils/coins/joligem/fetch-gem-balance.ts +13 -1
  47. package/src/common/rewards/registers/utils/coins/joligem/gem-handler.ts +2 -0
  48. package/src/common/rewards/registers/utils/subscription/commands/index.ts +1 -0
  49. package/src/common/rewards/registers/utils/subscription/commands/use-subscription.ts +29 -0
  50. package/src/common/rewards/registers/utils/subscription/sub-handler.ts +88 -0
  51. package/src/common/rewards/reward-emitter.ts +8 -0
  52. package/src/common/rewards/reward-helper.ts +8 -1
  53. package/src/common/utils/index.ts +23 -0
  54. package/src/h5/api/ads.ts +18 -12
  55. package/src/h5/api/platformAdsHandle/JoliboxAdsHandler.ts +25 -1
  56. package/src/h5/api/storage.ts +2 -2
  57. package/src/h5/bootstrap/auth/__tests__/auth.test.ts +308 -0
  58. package/src/h5/bootstrap/auth/index.ts +20 -0
  59. package/src/h5/bootstrap/auth/sub.ts +56 -0
  60. package/src/h5/bootstrap/index.ts +4 -19
  61. package/src/h5/http/index.ts +2 -2
  62. package/src/h5/report/event-tracker.ts +2 -2
  63. package/src/h5/rewards/index.ts +18 -1
  64. package/src/native/api/ads.ts +7 -1
  65. package/src/native/api/call-host-method.ts +1 -1
  66. package/src/native/api/index.ts +1 -0
  67. package/src/native/api/navigate.ts +10 -1
  68. package/src/native/api/payment.ts +56 -0
  69. package/src/native/payment/__tests__/payment-service-simple.test.ts +274 -0
  70. package/src/native/payment/payment-helper.ts +10 -4
  71. package/src/native/payment/payment-service.ts +293 -0
  72. package/src/native/payment/registers/jolicoin-iap.ts +4 -4
  73. package/src/native/report/index.ts +4 -1
  74. package/src/native/rewards/ui/payment-modal.ts +20 -60
@@ -0,0 +1,56 @@
1
+ import { onCustomEvent, notifyCustomEvent } from '@/common/utils';
2
+ import { uuidv4 as v4 } from '@jolibox/common';
3
+ // global event listener manager
4
+ interface PendingRequest {
5
+ resolve: (value: boolean) => void;
6
+ reject: (error: Error) => void;
7
+ timeoutId: NodeJS.Timeout;
8
+ }
9
+
10
+ const pendingRequests = new Map<string, PendingRequest>();
11
+ let isListenerInitialized = false;
12
+
13
+ // initialize global event listener
14
+ const initializeEventListener = () => {
15
+ if (isListenerInitialized) return;
16
+
17
+ onCustomEvent('ON_GET_USER_SUB_STATUS', (data) => {
18
+ const pendingRequest = pendingRequests.get(data.sequenceId);
19
+ if (pendingRequest) {
20
+ clearTimeout(pendingRequest.timeoutId);
21
+ pendingRequest.resolve(data.isSubUser);
22
+ pendingRequests.delete(data.sequenceId);
23
+ }
24
+ });
25
+
26
+ isListenerInitialized = true;
27
+ };
28
+
29
+ // Reset function for testing
30
+ export const resetSubState = () => {
31
+ isListenerInitialized = false;
32
+ pendingRequests.clear();
33
+ };
34
+
35
+ export async function getUserSubStatus(): Promise<boolean> {
36
+ initializeEventListener();
37
+
38
+ return new Promise((resolve, reject) => {
39
+ const sequenceId = v4();
40
+
41
+ const timeoutId = setTimeout(() => {
42
+ pendingRequests.delete(sequenceId);
43
+ reject(new Error('Timeout waiting for user sub status response'));
44
+ }, 3000); // 3 second timeout
45
+
46
+ pendingRequests.set(sequenceId, {
47
+ resolve,
48
+ reject,
49
+ timeoutId
50
+ });
51
+
52
+ notifyCustomEvent('JOLIBOX_GET_USER_SUB_STATUS', {
53
+ sequenceId
54
+ });
55
+ });
56
+ }
@@ -1,10 +1,8 @@
1
- import { hostEmitter, InternalGlobalJSError, UserCustomError } from '@jolibox/common';
1
+ import { hostEmitter } from '@jolibox/common';
2
2
  import { taskTracker, track } from '../report';
3
3
  import { onFCP, onLCP, onTTFB } from 'web-vitals';
4
4
  import { context } from '@/common/context';
5
- import { httpClientManager } from '../http';
6
- import { StandardResponse } from '@jolibox/types';
7
- import { reportError } from '@/common/report/errors/report';
5
+ import { checkSession, getUserSubStatus } from './auth';
8
6
 
9
7
  function trackPerformance() {
10
8
  onFCP((metric) => {
@@ -32,24 +30,11 @@ function trackPerformance() {
32
30
  });
33
31
  }
34
32
 
35
- async function checkSession(): Promise<boolean> {
36
- const httpClient = httpClientManager.create();
37
- try {
38
- const response = await httpClient.get<StandardResponse<void>>('/api/users/info');
39
- if (response.code !== 'SUCCESS' || !response.data) {
40
- return false;
41
- }
42
- return true;
43
- } catch (error) {
44
- reportError(new InternalGlobalJSError(error as Error));
45
- return false;
46
- }
47
- }
48
-
49
33
  function addDomContentLoaded() {
50
34
  hostEmitter.on('onDocumentReady', async (startTime: number) => {
51
35
  const isLogin = await checkSession();
52
- context.onEnvConfigChanged({ hostUserInfo: { isLogin } });
36
+ const isSubUser = await getUserSubStatus().catch(() => false);
37
+ context.onEnvConfigChanged({ hostUserInfo: { isLogin, isSubUser } });
53
38
  hostEmitter.emit('LifecycleEvent.onReady', {
54
39
  ...(context.hostUserInfo ? context.hostUserInfo : { isLogin: false })
55
40
  });
@@ -1,7 +1,7 @@
1
1
  import { IHttpClient } from '@/common/http';
2
2
  import { xUserAgent } from '../../common/http/xua';
3
3
  import { context } from '@/common/context';
4
- import { getApiHost, platform } from '@jolibox/common';
4
+ import { getH5ApiHost, platform } from '@jolibox/common';
5
5
 
6
6
  declare global {
7
7
  interface Window {
@@ -68,7 +68,7 @@ export class JoliboxHttpClient implements IHttpClient {
68
68
  };
69
69
 
70
70
  constructor(config?: IHttpClientInitParams) {
71
- const defaultUrl = getApiHost(context.testMode);
71
+ const defaultUrl = getH5ApiHost(context.testMode);
72
72
  this.baseUrl = config?.baseUrl ?? defaultUrl;
73
73
  }
74
74
 
@@ -1,7 +1,7 @@
1
1
  import { context } from '@/common/context';
2
2
  import { EventTracker } from '@/common/report';
3
3
  import { httpClientManager } from '@/h5/http';
4
- import { getCollectHost } from '@jolibox/common';
4
+ import { getCollectHost, getH5ApiHost } from '@jolibox/common';
5
5
 
6
6
  export class H5EventTracker extends EventTracker {
7
7
  private get apiBaseURL() {
@@ -24,4 +24,4 @@ export class H5EventTracker extends EventTracker {
24
24
  }
25
25
  }
26
26
 
27
- export const tracker = new H5EventTracker();
27
+ export const tracker = new H5EventTracker(getH5ApiHost(context.testMode));
@@ -13,7 +13,9 @@ import {
13
13
  IInvokePaymentEvent,
14
14
  IUseModalResultEvent,
15
15
  UseUnloginModalResultEventName,
16
- InvokeUnloginModalEventName
16
+ InvokeUnloginModalEventName,
17
+ InvokeSubscriptionEventName,
18
+ UseSubscriptionResultEventName
17
19
  } from '@/common/rewards/reward-emitter';
18
20
  import { notifyCustomEvent, onCustomEvent } from '@/common/utils';
19
21
  import { uuidv4 as v4 } from '@jolibox/common';
@@ -108,3 +110,18 @@ rewardsEmitter.on(InvokeUnloginModalEventName, async (type) => {
108
110
  type: type
109
111
  });
110
112
  });
113
+
114
+ // subscription
115
+ onCustomEvent('ON_JOLIBOX_SUB_RESULT_EVENT', (params) => {
116
+ console.log('----on custom event ON_JOLIBOX_SUB_RESULT_EVENT-----', params);
117
+ rewardsEmitter.emit(UseSubscriptionResultEventName, {
118
+ result: params.result
119
+ });
120
+ });
121
+
122
+ rewardsEmitter.on(InvokeSubscriptionEventName, async (type) => {
123
+ console.log('----notify custom event-----', type);
124
+ notifyCustomEvent('JOLIBOX_SUB_EVENT', {
125
+ sequenceId: v4()
126
+ });
127
+ });
@@ -25,9 +25,12 @@ import {
25
25
  createJolicoinRewardHandler,
26
26
  createJolicoinOnlyRewardHandler,
27
27
  createGemRewardHandler,
28
- createGemOnlyRewardHandler
28
+ createGemOnlyRewardHandler,
29
+ warmupGemBalanceCache,
30
+ warmupBalanceCache
29
31
  } from '@/common/rewards';
30
32
  import { adEventEmitter } from '@/common/ads';
33
+ import { warmupRewardCache } from '@/common/rewards/fetch-reward';
31
34
 
32
35
  const checkNetworkStatus = () => {
33
36
  const { data } = invokeNative('getNetworkStatusSync');
@@ -168,6 +171,9 @@ const showUnlockSuccessToast = (params: { quantity: number; balance: number }) =
168
171
  });
169
172
  };
170
173
 
174
+ warmupRewardCache(httpClient);
175
+ warmupGemBalanceCache(httpClient);
176
+
171
177
  export const showGemSuccessToast = (params: { quantity: number; balance: number }) => {
172
178
  const { quantity } = params;
173
179
  const toastTemplate = `{slot-correct} ${quantity * -1} {slot-gem}`;
@@ -1,4 +1,4 @@
1
- import { canIUseNative, createAPI, registerCanIUse, t, createSyncAPI } from './base';
1
+ import { createAPI, registerCanIUse, t } from './base';
2
2
  import { invokeNative } from '@jolibox/native-bridge';
3
3
  import { hostEmitter, UserCustomError } from '@jolibox/common';
4
4
  import { createCommands } from '@jolibox/common';
@@ -10,3 +10,4 @@ import './navigate';
10
10
  import './runtime';
11
11
  import './is-native-support';
12
12
  import './call-host-method';
13
+ import './payment';
@@ -56,7 +56,16 @@ const interceptSystemExitSync = createSyncAPI('interceptSystemExitSync', {
56
56
 
57
57
  const navigateToNativePage = createSyncAPI('navigateToNativePage', {
58
58
  paramsSchema: t.tuple(
59
- t.enum('openHistory', 'openDiscover', 'openDiscover', 'openGame', 'openDrama', 'openTopup', 'openFeed'),
59
+ t.enum(
60
+ 'openHistory',
61
+ 'openDiscover',
62
+ 'openDiscover',
63
+ 'openGame',
64
+ 'openDrama',
65
+ 'openTopup',
66
+ 'openFeed',
67
+ 'openRewards'
68
+ ),
60
69
  t.object()
61
70
  ),
62
71
  implement: (path, params) => {
@@ -0,0 +1,56 @@
1
+ import { createCommands } from '@jolibox/common';
2
+ import { createAPI, registerCanIUse, t } from './base';
3
+ import { createJoliGemPaymentService } from '../payment/payment-service';
4
+ import { createAPIError } from '@/common/report/errors';
5
+ import { IPaymentChoice } from '@jolibox/ui/dist/bridge/coin';
6
+
7
+ const commands = createCommands();
8
+ const paymentService = createJoliGemPaymentService();
9
+
10
+ const purchaseGem = createAPI('purchaseGem', {
11
+ paramsSchema: t.tuple(
12
+ t.object({
13
+ productId: t.string()
14
+ })
15
+ ),
16
+ implement: async (params: { productId: string }): Promise<{ totalAmount: string }> => {
17
+ const result = await paymentService.purchase(params.productId);
18
+
19
+ if (result.code !== 'SUCCESS' || !result.data?.totalAmount) {
20
+ throw createAPIError({
21
+ code: -1,
22
+ msg: '[JoliboxSDK]: purchase gem failed: ' + result.message
23
+ });
24
+ }
25
+
26
+ return {
27
+ totalAmount: result.data?.totalAmount
28
+ };
29
+ }
30
+ });
31
+
32
+ const getGemProducts = createAPI('getGemProducts', {
33
+ paramsSchema: t.tuple(),
34
+ implement: async (): Promise<{ products: IPaymentChoice[] }> => {
35
+ const choices = (await paymentService.getProductsInfo())?.paymentChoices ?? [];
36
+ console.info('choices', choices);
37
+ return {
38
+ products: choices.map((choice) => ({
39
+ productId: choice.productId,
40
+ totalAmountStr: choice.totalAmountStr ?? '',
41
+ quantity: choice.quantity
42
+ }))
43
+ };
44
+ }
45
+ });
46
+
47
+ commands.registerCommand('PaymentSDK.purchaseGem', purchaseGem);
48
+ commands.registerCommand('PaymentSDK.getGemProducts', getGemProducts);
49
+
50
+ registerCanIUse('payment.purchaseGem', {
51
+ version: '1.2.3'
52
+ });
53
+
54
+ registerCanIUse('payment.getGemProducts', {
55
+ version: '1.2.3'
56
+ });
@@ -0,0 +1,274 @@
1
+ // 导入类以便访问静态方法
2
+ import {
3
+ JoliGemPaymentService,
4
+ clearNativePriceCache,
5
+ resetFailureCounters,
6
+ getFailureCount
7
+ } from '../payment-service';
8
+
9
+ // Mock dependencies
10
+ jest.mock('@jolibox/native-bridge', () => ({
11
+ applyNative: jest.fn(),
12
+ onNative: jest.fn()
13
+ }));
14
+
15
+ jest.mock('@/native/network', () => ({
16
+ innerFetch: jest.fn()
17
+ }));
18
+
19
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
20
+ const { applyNative } = require('@jolibox/native-bridge');
21
+
22
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
23
+ const { innerFetch: fetch } = require('@/native/network');
24
+
25
+ const mockApplyNative = applyNative;
26
+ const mockFetch = fetch;
27
+
28
+ describe('PaymentService - Basic Tests', () => {
29
+ const mockApiEndpoint = '/api/joli-gem/balance-detail';
30
+ let paymentService: JoliGemPaymentService;
31
+
32
+ const mockServerData = {
33
+ balance: 100,
34
+ enableAutoDeduct: true,
35
+ paymentChoices: [
36
+ {
37
+ productId: 'coin_100',
38
+ quantity: 100,
39
+ appStoreProductId: 'com.joli.coin.100'
40
+ }
41
+ ]
42
+ };
43
+
44
+ const mockNativeData = {
45
+ 'com.joli.coin.100': { price: '$0.99' }
46
+ };
47
+
48
+ beforeEach(() => {
49
+ jest.clearAllMocks();
50
+ jest.clearAllTimers();
51
+ jest.useFakeTimers();
52
+ paymentService = new JoliGemPaymentService(); // 每次创建新实例
53
+ clearNativePriceCache(); // Clear native price cache
54
+ resetFailureCounters(); // Clear failure counters
55
+ });
56
+
57
+ afterEach(() => {
58
+ jest.useRealTimers();
59
+ jest.restoreAllMocks();
60
+ });
61
+
62
+ it('should fetch and cache payment choices on first call', async () => {
63
+ mockFetch.mockResolvedValue({
64
+ response: {
65
+ data: {
66
+ data: mockServerData
67
+ }
68
+ }
69
+ });
70
+
71
+ mockApplyNative.mockResolvedValue({
72
+ data: mockNativeData
73
+ });
74
+
75
+ const result = await paymentService.getProductsInfo();
76
+
77
+ expect(mockFetch).toHaveBeenCalledTimes(1);
78
+ expect(mockApplyNative).toHaveBeenCalledTimes(1);
79
+ expect(result).toEqual({
80
+ balance: 100,
81
+ enableAutoDeduct: true,
82
+ paymentChoices: [
83
+ {
84
+ productId: 'coin_100',
85
+ quantity: 100,
86
+ appStoreProductId: 'com.joli.coin.100',
87
+ totalAmountStr: '$0.99'
88
+ }
89
+ ]
90
+ });
91
+
92
+ // Check cache stats using RequestCacheService methods
93
+ const stats = paymentService.getCacheStats();
94
+ expect(stats.cacheCount).toBe(1);
95
+ expect(stats.validCount).toBe(1);
96
+ });
97
+
98
+ it('should use cache on second call', async () => {
99
+ mockFetch.mockResolvedValue({
100
+ response: {
101
+ data: {
102
+ data: mockServerData
103
+ }
104
+ }
105
+ });
106
+
107
+ mockApplyNative.mockResolvedValue({
108
+ data: mockNativeData
109
+ });
110
+
111
+ // First call
112
+ await paymentService.getProductsInfo();
113
+
114
+ // Second call - should use cache for native prices but still fetch real-time data
115
+ const result = await paymentService.getProductsInfo();
116
+
117
+ expect(mockFetch).toHaveBeenCalledTimes(1); // Still fetch for real-time data (balance, enableAutoDeduct)
118
+ expect(mockApplyNative).toHaveBeenCalledTimes(1); // Only called once due to native price caching
119
+
120
+ expect(result).toEqual({
121
+ balance: 0,
122
+ enableAutoDeduct: false,
123
+ paymentChoices: [
124
+ {
125
+ productId: 'coin_100',
126
+ quantity: 100,
127
+ appStoreProductId: 'com.joli.coin.100',
128
+ totalAmountStr: '$0.99'
129
+ }
130
+ ]
131
+ });
132
+ });
133
+
134
+ it('should timeout after 1 second', async () => {
135
+ // Mock a slow server response
136
+ mockFetch.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 2000)));
137
+
138
+ const promise = paymentService.getProductsInfo();
139
+
140
+ // Fast forward past timeout (1 second)
141
+ jest.advanceTimersByTime(1000);
142
+
143
+ await expect(promise).rejects.toThrow('[RequestCacheService] Request timeout after 1 seconds');
144
+ });
145
+
146
+ it('should handle cache expiration', async () => {
147
+ mockFetch.mockResolvedValue({
148
+ response: {
149
+ data: {
150
+ data: mockServerData
151
+ }
152
+ }
153
+ });
154
+
155
+ mockApplyNative.mockResolvedValue({
156
+ data: mockNativeData
157
+ });
158
+
159
+ // First call
160
+ await paymentService.getProductsInfo();
161
+
162
+ // Fast forward time beyond cache duration (10 minutes)
163
+ jest.advanceTimersByTime(40 * 60 * 1000);
164
+
165
+ // Second call - cache should be expired
166
+ await paymentService.getProductsInfo();
167
+
168
+ expect(mockApplyNative).toHaveBeenCalledTimes(2); // Called again due to expiration
169
+ });
170
+
171
+ it('should throw error when mockApplyNative fails', async () => {
172
+ mockFetch.mockResolvedValue({
173
+ response: {
174
+ data: {
175
+ data: mockServerData
176
+ }
177
+ }
178
+ });
179
+
180
+ // Mock applyNative to reject
181
+ mockApplyNative.mockRejectedValue(new Error('Native call failed'));
182
+
183
+ await expect(paymentService.getProductsInfo()).rejects.toThrow('Native call failed');
184
+ });
185
+
186
+ it('should throw error when mockFetch fails', async () => {
187
+ // Mock fetch to reject
188
+ mockFetch.mockRejectedValue(new Error('Network error'));
189
+
190
+ await expect(paymentService.getProductsInfo()).rejects.toThrow('Network error');
191
+ });
192
+
193
+ it('should throw error when request exceeds 1 second timeout', async () => {
194
+ // Mock a very slow fetch request that never resolves within 1 second
195
+ mockFetch.mockImplementation(
196
+ () =>
197
+ new Promise((resolve) => {
198
+ // This will resolve after 2 seconds, but timeout should trigger at 1 second
199
+ setTimeout(() => {
200
+ resolve({
201
+ response: {
202
+ data: {
203
+ data: mockServerData
204
+ }
205
+ }
206
+ });
207
+ }, 2000);
208
+ })
209
+ );
210
+
211
+ const promise = paymentService.getProductsInfo();
212
+
213
+ // Advance timers to 1 second to trigger timeout
214
+ jest.advanceTimersByTime(1000);
215
+
216
+ await expect(promise).rejects.toThrow('[RequestCacheService] Request timeout after 1 seconds');
217
+ });
218
+
219
+ it('should increment failure count on failed requests', async () => {
220
+ // Mock fetch to reject
221
+ mockFetch.mockRejectedValue(new Error('Network error'));
222
+
223
+ // First failure
224
+ await expect(paymentService.getProductsInfo()).rejects.toThrow('Network error');
225
+ expect(getFailureCount('/api/joli-gem/balance-detail')).toBe(1);
226
+
227
+ // Second failure
228
+ await expect(paymentService.getProductsInfo()).rejects.toThrow('Network error');
229
+ expect(getFailureCount('/api/joli-gem/balance-detail')).toBe(2);
230
+ });
231
+
232
+ it('should block requests after 2 failures', async () => {
233
+ // Mock fetch to reject
234
+ mockFetch.mockRejectedValue(new Error('Network error'));
235
+
236
+ // First two failures
237
+ await expect(paymentService.getProductsInfo()).rejects.toThrow('Network error');
238
+ await expect(paymentService.getProductsInfo()).rejects.toThrow('Network error');
239
+
240
+ // Third call should be blocked immediately (failure count is now 2, which >= MAX_FAILURE_COUNT)
241
+ await expect(paymentService.getProductsInfo()).rejects.toThrow(
242
+ 'getProductsInfo has failed more than 2 times for /api/joli-gem/balance-detail'
243
+ );
244
+
245
+ // Verify fetch was only called twice (not three times)
246
+ expect(mockFetch).toHaveBeenCalledTimes(2);
247
+ });
248
+
249
+ it('should reset failure count on successful request', async () => {
250
+ // Mock first call to fail
251
+ mockFetch.mockRejectedValueOnce(new Error('Network error'));
252
+
253
+ // First failure
254
+ await expect(paymentService.getProductsInfo()).rejects.toThrow('Network error');
255
+ expect(getFailureCount('/api/joli-gem/balance-detail')).toBe(1);
256
+
257
+ // Mock second call to succeed
258
+ mockFetch.mockResolvedValue({
259
+ response: {
260
+ data: {
261
+ data: mockServerData
262
+ }
263
+ }
264
+ });
265
+
266
+ mockApplyNative.mockResolvedValue({
267
+ data: mockNativeData
268
+ });
269
+
270
+ // Successful call should reset counter
271
+ await paymentService.getProductsInfo();
272
+ expect(getFailureCount('/api/joli-gem/balance-detail')).toBe(0);
273
+ });
274
+ });
@@ -4,12 +4,18 @@ import { StandardResponse } from '@jolibox/types';
4
4
  import { reportError } from '@/common/report/errors/report';
5
5
  import { BaseError } from '@jolibox/common';
6
6
 
7
- type PaymentResult = StandardResponse<void>;
7
+ export type PaymentResult<T> = StandardResponse<T>;
8
8
 
9
9
  export interface PaymentHandlerMap {
10
- JOLI_COIN: (productId: string) => Promise<PaymentResult>; // jolicoin
11
- JOLI_COIN_IAP: (params: { productId: string; appStoreProductId: string }) => Promise<PaymentResult>; // jolicoin iap
12
- JOLI_GEM_IAP: (params: { productId: string; appStoreProductId: string }) => Promise<PaymentResult>; // gem iap
10
+ JOLI_COIN: (productId: string) => Promise<PaymentResult<void>>; // jolicoin
11
+ JOLI_COIN_IAP: (params: {
12
+ productId: string;
13
+ appStoreProductId: string;
14
+ }) => Promise<PaymentResult<{ totalAmount: string }>>; // jolicoin iap
15
+ JOLI_GEM_IAP: (params: {
16
+ productId: string;
17
+ appStoreProductId: string;
18
+ }) => Promise<PaymentResult<{ totalAmount: string }>>; // gem iap
13
19
  }
14
20
 
15
21
  export type PaymentHandler<T extends PaymentType> = PaymentHandlerMap[T];