@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.
- package/.rush/temp/package-deps_build.json +44 -28
- package/.rush/temp/shrinkwrap-deps.json +2 -1
- package/CHANGELOG.json +11 -0
- package/CHANGELOG.md +9 -0
- package/dist/common/context/index.d.ts +5 -0
- package/dist/common/report/base-tracker.d.ts +2 -1
- package/dist/common/rewards/cached-fetch-reward.d.ts +46 -0
- package/dist/common/rewards/cached-reward-service.d.ts +24 -0
- package/dist/common/rewards/fetch-reward.d.ts +2 -3
- package/dist/common/rewards/index.d.ts +3 -0
- package/dist/common/rewards/registers/use-subscription.d.ts +7 -0
- package/dist/common/rewards/registers/utils/coins/jolicoin/cached-fetch-balance.d.ts +34 -0
- package/dist/common/rewards/registers/utils/coins/jolicoin/fetch-balance.d.ts +2 -1
- package/dist/common/rewards/registers/utils/coins/joligem/cached-fetch-gem-balance.d.ts +34 -0
- package/dist/common/rewards/registers/utils/coins/joligem/fetch-gem-balance.d.ts +2 -1
- package/dist/common/rewards/registers/utils/subscription/commands/index.d.ts +1 -0
- package/dist/common/rewards/registers/utils/subscription/commands/use-subscription.d.ts +4 -0
- package/dist/common/rewards/registers/utils/subscription/sub-handler.d.ts +13 -0
- package/dist/common/rewards/reward-emitter.d.ts +7 -0
- package/dist/common/rewards/reward-helper.d.ts +2 -1
- package/dist/common/utils/index.d.ts +18 -0
- package/dist/h5/api/platformAdsHandle/JoliboxAdsHandler.d.ts +1 -0
- package/dist/h5/bootstrap/auth/__tests__/auth.test.d.ts +1 -0
- package/dist/h5/bootstrap/auth/index.d.ts +2 -0
- package/dist/h5/bootstrap/auth/sub.d.ts +2 -0
- package/dist/index.js +9 -13
- package/dist/index.native.js +49 -53
- package/dist/native/api/index.d.ts +1 -0
- package/dist/native/api/payment.d.ts +1 -0
- package/dist/native/payment/__tests__/payment-service-simple.test.d.ts +1 -0
- package/dist/native/payment/payment-helper.d.ts +8 -5
- package/dist/native/payment/payment-service.d.ts +44 -0
- package/implement.build.log +2 -2
- package/package.json +8 -7
- package/src/common/context/index.ts +12 -0
- package/src/common/report/base-tracker.ts +2 -2
- package/src/common/rewards/cached-fetch-reward.ts +258 -0
- package/src/common/rewards/cached-reward-service.ts +255 -0
- package/src/common/rewards/fetch-reward.ts +17 -93
- package/src/common/rewards/index.ts +4 -0
- package/src/common/rewards/registers/use-subscription.ts +34 -0
- package/src/common/rewards/registers/utils/coins/jolicoin/cached-fetch-balance.ts +177 -0
- package/src/common/rewards/registers/utils/coins/jolicoin/fetch-balance.ts +13 -1
- package/src/common/rewards/registers/utils/coins/jolicoin/jolicoin-handler.ts +2 -0
- package/src/common/rewards/registers/utils/coins/joligem/cached-fetch-gem-balance.ts +181 -0
- package/src/common/rewards/registers/utils/coins/joligem/fetch-gem-balance.ts +13 -1
- package/src/common/rewards/registers/utils/coins/joligem/gem-handler.ts +2 -0
- package/src/common/rewards/registers/utils/subscription/commands/index.ts +1 -0
- package/src/common/rewards/registers/utils/subscription/commands/use-subscription.ts +29 -0
- package/src/common/rewards/registers/utils/subscription/sub-handler.ts +88 -0
- package/src/common/rewards/reward-emitter.ts +8 -0
- package/src/common/rewards/reward-helper.ts +8 -1
- package/src/common/utils/index.ts +23 -0
- package/src/h5/api/ads.ts +18 -12
- package/src/h5/api/platformAdsHandle/JoliboxAdsHandler.ts +25 -1
- package/src/h5/api/storage.ts +2 -2
- package/src/h5/bootstrap/auth/__tests__/auth.test.ts +308 -0
- package/src/h5/bootstrap/auth/index.ts +20 -0
- package/src/h5/bootstrap/auth/sub.ts +56 -0
- package/src/h5/bootstrap/index.ts +4 -19
- package/src/h5/http/index.ts +2 -2
- package/src/h5/report/event-tracker.ts +2 -2
- package/src/h5/rewards/index.ts +18 -1
- package/src/native/api/ads.ts +7 -1
- package/src/native/api/call-host-method.ts +1 -1
- package/src/native/api/index.ts +1 -0
- package/src/native/api/navigate.ts +10 -1
- package/src/native/api/payment.ts +56 -0
- package/src/native/payment/__tests__/payment-service-simple.test.ts +274 -0
- package/src/native/payment/payment-helper.ts +10 -4
- package/src/native/payment/payment-service.ts +293 -0
- package/src/native/payment/registers/jolicoin-iap.ts +4 -4
- package/src/native/report/index.ts +4 -1
- 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
|
|
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 {
|
|
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
|
-
|
|
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
|
});
|
package/src/h5/http/index.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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));
|
package/src/h5/rewards/index.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/native/api/ads.ts
CHANGED
|
@@ -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 {
|
|
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';
|
package/src/native/api/index.ts
CHANGED
|
@@ -56,7 +56,16 @@ const interceptSystemExitSync = createSyncAPI('interceptSystemExitSync', {
|
|
|
56
56
|
|
|
57
57
|
const navigateToNativePage = createSyncAPI('navigateToNativePage', {
|
|
58
58
|
paramsSchema: t.tuple(
|
|
59
|
-
t.enum(
|
|
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<
|
|
7
|
+
export type PaymentResult<T> = StandardResponse<T>;
|
|
8
8
|
|
|
9
9
|
export interface PaymentHandlerMap {
|
|
10
|
-
JOLI_COIN: (productId: string) => Promise<PaymentResult
|
|
11
|
-
JOLI_COIN_IAP: (params: {
|
|
12
|
-
|
|
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];
|