@jolibox/implement 1.2.3 → 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.
@@ -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';
@@ -0,0 +1 @@
1
+ export {};
@@ -1,16 +1,20 @@
1
1
  export type PaymentType = 'JOLI_COIN' | 'JOLI_COIN_IAP' | 'JOLI_GEM_IAP';
2
2
  import { StandardResponse } from '@jolibox/types';
3
- type PaymentResult = StandardResponse<void>;
3
+ export type PaymentResult<T> = StandardResponse<T>;
4
4
  export interface PaymentHandlerMap {
5
- JOLI_COIN: (productId: string) => Promise<PaymentResult>;
5
+ JOLI_COIN: (productId: string) => Promise<PaymentResult<void>>;
6
6
  JOLI_COIN_IAP: (params: {
7
7
  productId: string;
8
8
  appStoreProductId: string;
9
- }) => Promise<PaymentResult>;
9
+ }) => Promise<PaymentResult<{
10
+ totalAmount: string;
11
+ }>>;
10
12
  JOLI_GEM_IAP: (params: {
11
13
  productId: string;
12
14
  appStoreProductId: string;
13
- }) => Promise<PaymentResult>;
15
+ }) => Promise<PaymentResult<{
16
+ totalAmount: string;
17
+ }>>;
14
18
  }
15
19
  export type PaymentHandler<T extends PaymentType> = PaymentHandlerMap[T];
16
20
  export declare function createPaymentHelper(): {
@@ -18,4 +22,3 @@ export declare function createPaymentHelper(): {
18
22
  invokePayment<T extends PaymentType>(type: T, ...args: Parameters<PaymentHandler<T>>): Promise<any>;
19
23
  };
20
24
  export type PaymentHelper = ReturnType<typeof createPaymentHelper>;
21
- export {};
@@ -0,0 +1,38 @@
1
+ import { IPaymentChoice } from '@/common/rewards/reward-emitter';
2
+ import type { PaymentResult } from './payment-helper';
3
+ type PaymentPurchaseType = 'JOLI_COIN' | 'JOLI_GEM';
4
+ export interface CachedPaymentChoices {
5
+ choices: IPaymentChoice[];
6
+ timestamp: number;
7
+ expiresAt: number;
8
+ productIds: string[];
9
+ }
10
+ export declare class PaymentService {
11
+ private productInfoCache;
12
+ private static paymentChoicesCache;
13
+ private static readonly PAYMENT_CHOICES_CACHE_DURATION;
14
+ getJolicoinProductsInfo(type: PaymentPurchaseType): Promise<IPaymentChoice[]>;
15
+ purchase(type: PaymentPurchaseType, productId: string): Promise<PaymentResult<{
16
+ totalAmount: string;
17
+ }>>;
18
+ private static mergeResponseData;
19
+ static getProductsInfo(apiEndpoint: string): Promise<{
20
+ balance: number;
21
+ enableAutoDeduct: boolean;
22
+ paymentChoices: IPaymentChoice[];
23
+ } | undefined>;
24
+ private static getProductsInfoInternal;
25
+ private static getPaymentChoicesFromCache;
26
+ private static cachePaymentChoices;
27
+ private static extractProductIds;
28
+ private static arraysEqual;
29
+ private static updateChoicesWithServerData;
30
+ static clearPaymentChoicesCache(apiEndpoint?: string): void;
31
+ static getPaymentChoicesCacheStats(): {
32
+ cacheCount: number;
33
+ validCount: number;
34
+ expiredCount: number;
35
+ };
36
+ static clearExpiredPaymentChoicesCache(): void;
37
+ }
38
+ export {};
@@ -1,9 +1,9 @@
1
1
  Invoking: npm run clean && npm run build:esm && tsc
2
2
 
3
- > @jolibox/implement@1.2.3 clean
3
+ > @jolibox/implement@1.2.4 clean
4
4
  > rimraf ./dist
5
5
 
6
6
 
7
- > @jolibox/implement@1.2.3 build:esm
7
+ > @jolibox/implement@1.2.4 build:esm
8
8
  > BUILD_VERSION=$(node -p "require('./package.json').version") node esbuild.config.js --format=esm
9
9
 
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@jolibox/implement",
3
3
  "description": "This project is Jolibox JS-SDk implement for Native && H5",
4
- "version": "1.2.3",
4
+ "version": "1.2.4",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
7
7
  "license": "MIT",
8
8
  "dependencies": {
9
- "@jolibox/common": "1.2.3",
10
- "@jolibox/types": "1.2.3",
11
- "@jolibox/native-bridge": "1.2.3",
12
- "@jolibox/ads": "1.2.3",
9
+ "@jolibox/common": "1.2.4",
10
+ "@jolibox/types": "1.2.4",
11
+ "@jolibox/native-bridge": "1.2.4",
12
+ "@jolibox/ads": "1.2.4",
13
13
  "localforage": "1.10.0",
14
14
  "@jolibox/ui": "1.0.0",
15
15
  "web-vitals": "4.2.4"
@@ -17,6 +17,7 @@
17
17
  "devDependencies": {
18
18
  "typescript": "5.7.3",
19
19
  "@types/jest": "28.1.1",
20
+ "@types/node": "18.0.0",
20
21
  "rimraf": "6.0.1",
21
22
  "esbuild": "0.24.2",
22
23
  "@jolibox/eslint-config": "1.0.0"
@@ -49,6 +49,11 @@ type Viewport = {
49
49
  navigationBarHeight: number;
50
50
  };
51
51
 
52
+ type MPInfo = {
53
+ mpName: string;
54
+ mpVersion?: string;
55
+ };
56
+
52
57
  function hasMetaTag(name: string, content: string): boolean {
53
58
  return document?.head.querySelector(`meta[name="${name}"][content="${content}"]`) !== null;
54
59
  }
@@ -165,6 +170,13 @@ const wrapContext = () => {
165
170
  get abTests(): string[] {
166
171
  return env.abValues?.split(',') ?? [];
167
172
  },
173
+ get mpInfo(): MPInfo {
174
+ return (
175
+ env.mpInfo ?? {
176
+ mpName: 'unknown'
177
+ }
178
+ );
179
+ },
168
180
  onEnvConfigChanged: (newConfig: Partial<Env>) => {
169
181
  mergeWith(env, newConfig, mergeArray);
170
182
  },
@@ -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 { PaymentService } 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 = new PaymentService();
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('JOLI_GEM', 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.getJolicoinProductsInfo('JOLI_GEM');
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,208 @@
1
+ import { PaymentService } from '../payment-service';
2
+
3
+ // Mock dependencies
4
+ jest.mock('@jolibox/native-bridge', () => ({
5
+ applyNative: jest.fn(),
6
+ onNative: jest.fn()
7
+ }));
8
+
9
+ jest.mock('@/native/network', () => ({
10
+ innerFetch: jest.fn()
11
+ }));
12
+
13
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
14
+ const { applyNative } = require('@jolibox/native-bridge');
15
+
16
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
17
+ const { innerFetch: fetch } = require('@/native/network');
18
+
19
+ const mockApplyNative = applyNative;
20
+ const mockFetch = fetch;
21
+
22
+ describe('PaymentService - Basic Tests', () => {
23
+ const mockApiEndpoint = '/api/joli-coin/balance-detail';
24
+
25
+ const mockServerData = {
26
+ balance: 100,
27
+ enableAutoDeduct: true,
28
+ paymentChoices: [
29
+ {
30
+ productId: 'coin_100',
31
+ quantity: 100,
32
+ appStoreProductId: 'com.joli.coin.100'
33
+ }
34
+ ]
35
+ };
36
+
37
+ const mockNativeData = {
38
+ 'com.joli.coin.100': { price: '$0.99' }
39
+ };
40
+
41
+ beforeEach(() => {
42
+ jest.clearAllMocks();
43
+ jest.clearAllTimers();
44
+ jest.useFakeTimers();
45
+ PaymentService.clearPaymentChoicesCache();
46
+ });
47
+
48
+ afterEach(() => {
49
+ jest.useRealTimers();
50
+ jest.restoreAllMocks();
51
+ });
52
+
53
+ it('should fetch and cache payment choices on first call', async () => {
54
+ mockFetch.mockResolvedValue({
55
+ response: {
56
+ data: {
57
+ data: mockServerData
58
+ }
59
+ }
60
+ });
61
+
62
+ mockApplyNative.mockResolvedValue({
63
+ data: mockNativeData
64
+ });
65
+
66
+ const result = await PaymentService.getProductsInfo(mockApiEndpoint);
67
+
68
+ expect(mockFetch).toHaveBeenCalledTimes(1);
69
+ expect(mockApplyNative).toHaveBeenCalledTimes(1);
70
+ expect(result).toEqual({
71
+ balance: 100,
72
+ enableAutoDeduct: true,
73
+ paymentChoices: [
74
+ {
75
+ productId: 'coin_100',
76
+ quantity: 100,
77
+ appStoreProductId: 'com.joli.coin.100',
78
+ totalAmountStr: '$0.99'
79
+ }
80
+ ]
81
+ });
82
+
83
+ const stats = PaymentService.getPaymentChoicesCacheStats();
84
+ expect(stats.cacheCount).toBe(1);
85
+ expect(stats.validCount).toBe(1);
86
+ });
87
+
88
+ it('should use cache on second call', async () => {
89
+ mockFetch.mockResolvedValue({
90
+ response: {
91
+ data: {
92
+ data: mockServerData
93
+ }
94
+ }
95
+ });
96
+
97
+ mockApplyNative.mockResolvedValue({
98
+ data: mockNativeData
99
+ });
100
+
101
+ // First call
102
+ await PaymentService.getProductsInfo(mockApiEndpoint);
103
+
104
+ // Second call - should use cache
105
+ const result = await PaymentService.getProductsInfo(mockApiEndpoint);
106
+
107
+ expect(mockFetch).toHaveBeenCalledTimes(2); // Still fetch for real-time data
108
+ expect(mockApplyNative).toHaveBeenCalledTimes(1); // Only called once
109
+
110
+ expect(result).toEqual({
111
+ balance: 100,
112
+ enableAutoDeduct: true,
113
+ paymentChoices: [
114
+ {
115
+ productId: 'coin_100',
116
+ quantity: 100,
117
+ appStoreProductId: 'com.joli.coin.100',
118
+ totalAmountStr: '$0.99'
119
+ }
120
+ ]
121
+ });
122
+ });
123
+
124
+ it('should timeout after 3 seconds', async () => {
125
+ // Mock a slow server response
126
+ mockFetch.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 5000)));
127
+
128
+ const promise = PaymentService.getProductsInfo(mockApiEndpoint);
129
+
130
+ // Fast forward past timeout
131
+ jest.advanceTimersByTime(3000);
132
+
133
+ await expect(promise).rejects.toThrow('[PaymentService] Request timeout after 3 seconds');
134
+ });
135
+
136
+ it('should handle cache expiration', async () => {
137
+ mockFetch.mockResolvedValue({
138
+ response: {
139
+ data: {
140
+ data: mockServerData
141
+ }
142
+ }
143
+ });
144
+
145
+ mockApplyNative.mockResolvedValue({
146
+ data: mockNativeData
147
+ });
148
+
149
+ // First call
150
+ await PaymentService.getProductsInfo(mockApiEndpoint);
151
+
152
+ // Fast forward time beyond cache duration (10 minutes)
153
+ jest.advanceTimersByTime(11 * 60 * 1000);
154
+
155
+ // Second call - cache should be expired
156
+ await PaymentService.getProductsInfo(mockApiEndpoint);
157
+
158
+ expect(mockApplyNative).toHaveBeenCalledTimes(2); // Called again due to expiration
159
+ });
160
+
161
+ it('should throw error when mockApplyNative fails', async () => {
162
+ mockFetch.mockResolvedValue({
163
+ response: {
164
+ data: {
165
+ data: mockServerData
166
+ }
167
+ }
168
+ });
169
+
170
+ // Mock applyNative to reject
171
+ mockApplyNative.mockRejectedValue(new Error('Native call failed'));
172
+
173
+ await expect(PaymentService.getProductsInfo(mockApiEndpoint)).rejects.toThrow('Native call failed');
174
+ });
175
+
176
+ it('should throw error when mockFetch fails', async () => {
177
+ // Mock fetch to reject
178
+ mockFetch.mockRejectedValue(new Error('Network error'));
179
+
180
+ await expect(PaymentService.getProductsInfo(mockApiEndpoint)).rejects.toThrow('Network error');
181
+ });
182
+
183
+ it('should throw error when request exceeds 3 seconds timeout', async () => {
184
+ // Mock a very slow fetch request that never resolves within 3 seconds
185
+ mockFetch.mockImplementation(
186
+ () =>
187
+ new Promise((resolve) => {
188
+ // This will resolve after 5 seconds, but timeout should trigger at 3 seconds
189
+ setTimeout(() => {
190
+ resolve({
191
+ response: {
192
+ data: {
193
+ data: mockServerData
194
+ }
195
+ }
196
+ });
197
+ }, 5000);
198
+ })
199
+ );
200
+
201
+ const promise = PaymentService.getProductsInfo(mockApiEndpoint);
202
+
203
+ // Advance timers to 3 seconds to trigger timeout
204
+ jest.advanceTimersByTime(3000);
205
+
206
+ await expect(promise).rejects.toThrow('[PaymentService] Request timeout after 3 seconds');
207
+ });
208
+ });
@@ -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];