@jolibox/implement 1.3.1 → 1.3.3-beta.1

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.
@@ -11,3 +11,4 @@ import './runtime';
11
11
  import './is-native-support';
12
12
  import './call-host-method';
13
13
  import './payment';
14
+ import './rewards';
@@ -0,0 +1 @@
1
+ export {};
@@ -24,6 +24,7 @@ declare class BaseSubscriptionService extends RequestCacheService<SubscriptionRe
24
24
  message: string;
25
25
  result: 'SUCCESS' | 'FAILED' | 'FAILURE_SUBSCRIPTED';
26
26
  }>;
27
+ private pollSubscriptionStatus;
27
28
  invokeSubscriptionPanel(): Promise<StandardResponse<{
28
29
  result: 'SUCCESS' | 'FAILED';
29
30
  subPlanId?: string;
@@ -1,9 +1,9 @@
1
1
  Invoking: npm run clean && npm run build:esm && tsc
2
2
 
3
- > @jolibox/implement@1.3.1 clean
3
+ > @jolibox/implement@1.3.3-beta.1 clean
4
4
  > rimraf ./dist
5
5
 
6
6
 
7
- > @jolibox/implement@1.3.1 build:esm
7
+ > @jolibox/implement@1.3.3-beta.1 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,17 +1,17 @@
1
1
  {
2
2
  "name": "@jolibox/implement",
3
3
  "description": "This project is Jolibox JS-SDk implement for Native && H5",
4
- "version": "1.3.1",
4
+ "version": "1.3.3-beta.1",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
7
7
  "license": "MIT",
8
8
  "dependencies": {
9
- "@jolibox/common": "1.3.1",
10
- "@jolibox/types": "1.3.1",
11
- "@jolibox/native-bridge": "1.3.1",
12
- "@jolibox/ads": "1.3.1",
9
+ "@jolibox/common": "1.3.3-beta.1",
10
+ "@jolibox/types": "1.3.3-beta.1",
11
+ "@jolibox/native-bridge": "1.3.3-beta.1",
12
+ "@jolibox/ads": "1.3.3-beta.1",
13
13
  "localforage": "1.10.0",
14
- "@jolibox/ui": "1.0.0",
14
+ "@jolibox/ui": "1.3.3-beta.1",
15
15
  "web-vitals": "4.2.4"
16
16
  },
17
17
  "devDependencies": {
@@ -20,7 +20,7 @@
20
20
  "@types/node": "18.0.0",
21
21
  "rimraf": "6.0.1",
22
22
  "esbuild": "0.24.2",
23
- "@jolibox/eslint-config": "1.0.0"
23
+ "@jolibox/eslint-config": "1.0.1-beta.16"
24
24
  },
25
25
  "scripts": {
26
26
  "clean": "rimraf ./dist",
@@ -11,3 +11,4 @@ import './runtime';
11
11
  import './is-native-support';
12
12
  import './call-host-method';
13
13
  import './payment';
14
+ import './rewards';
@@ -34,7 +34,7 @@ const purchaseGem = createAPI('purchaseGem', {
34
34
  const getGemProducts = createAPI('getGemProducts', {
35
35
  paramsSchema: t.tuple(),
36
36
  implement: async (): Promise<{ products: IPaymentChoice[] }> => {
37
- const choices = (await paymentService.getProductsInfo())?.paymentChoices ?? [];
37
+ const choices = (await paymentService.getProductsInfoWithBalance())?.paymentChoices ?? [];
38
38
  console.info('choices', choices);
39
39
  return {
40
40
  products: choices.map((choice) => ({
@@ -92,7 +92,14 @@ const subscribe = createAPI('subscribe', {
92
92
  });
93
93
  }
94
94
  const { basePlanId, appStoreProductId, nativeProductId } = tier;
95
- const result = await subscriptionService.subscribe({ basePlanId, nativeProductId, appStoreProductId });
95
+
96
+ // Use subscription service with built-in compensation polling
97
+ const result = await subscriptionService.subscribe({
98
+ basePlanId,
99
+ nativeProductId,
100
+ appStoreProductId
101
+ });
102
+
96
103
  return {
97
104
  result: result.result,
98
105
  subPlanId: result.subPlanId,
@@ -0,0 +1,85 @@
1
+ import { BaseError, createCommands, wrapUserFunction } from '@jolibox/common';
2
+ import { canIUseNative, createAPI, registerCanIUse, t } from './base';
3
+ import { StandardResponse, ICoinDetailsData } from '@jolibox/types';
4
+ import { context } from '@/common/context';
5
+ import { createAPIError } from '@/common/report/errors';
6
+ import { invokeNative } from '@jolibox/native-bridge';
7
+
8
+ const commands = createCommands();
9
+
10
+ const safeCallbackWrapper = wrapUserFunction(reportError as (err: Error | BaseError) => void);
11
+
12
+ const getCoinDetails = createAPI('getCoinDetails', {
13
+ paramsSchema: t.tuple(t.object({ onUpdate: t.function().optional() })),
14
+ implement: async ({
15
+ onUpdate
16
+ }: {
17
+ onUpdate?: (data: ICoinDetailsData) => void | Promise<void>;
18
+ }): Promise<StandardResponse<ICoinDetailsData>> => {
19
+ try {
20
+ const currentType = context.mpType;
21
+ // check mp type
22
+ if (currentType !== 'miniApp') {
23
+ reportError(
24
+ createAPIError({
25
+ code: -1,
26
+ msg: '[JoliboxSDK]: rewards info not supported in games. Only supported in mini apps.'
27
+ })
28
+ );
29
+ return {
30
+ code: 'FAILURE',
31
+ message: '[JoliboxSDK]: rewards info not supported in games. Only supported in mini apps.'
32
+ };
33
+ }
34
+
35
+ // check native version
36
+ if (!canIUseNative('requestCoinDetailsSync')) {
37
+ reportError(
38
+ createAPIError({
39
+ code: -1,
40
+ msg: '[JoliboxSDK]: rewards info not supported in this platform.'
41
+ })
42
+ );
43
+ return {
44
+ code: 'FAILURE',
45
+ message: '[JoliboxSDK]: rewards info not supported in this platform. lower than 1.7.1'
46
+ };
47
+ }
48
+
49
+ const customOnUpdate = onUpdate
50
+ ? safeCallbackWrapper(onUpdate)
51
+ : (() => {
52
+ console.log('default update function called');
53
+ }).bind(this);
54
+ const { errNo, errMsg, data } = invokeNative('requestCoinDetailsSync');
55
+ if (errNo !== 0) {
56
+ throw createAPIError({
57
+ code: errNo ?? -1,
58
+ msg: errMsg
59
+ });
60
+ }
61
+
62
+ invokeNative('requestCoinDetailsAsync').then((res) => {
63
+ if (res.errNo == 0 && res.data) {
64
+ customOnUpdate(res.data);
65
+ }
66
+ });
67
+ return {
68
+ code: 'SUCCESS',
69
+ message: 'Successfully retrieved coin details',
70
+ data
71
+ };
72
+ } catch (error) {
73
+ return {
74
+ code: 'FAILURE',
75
+ message: '[JoliboxSDK]: Failed to get coin details: ' + (error as Error).message
76
+ };
77
+ }
78
+ }
79
+ });
80
+
81
+ commands.registerCommand('RewardsSDK.getCoinDetails', getCoinDetails);
82
+
83
+ registerCanIUse('rewards.getCoinDetails', {
84
+ version: '1.3.2'
85
+ });
@@ -151,9 +151,9 @@ describe('PaymentService - Basic Tests', () => {
151
151
  const promise = paymentService.getProductsInfo();
152
152
 
153
153
  // Fast forward past timeout (1 second)
154
- jest.advanceTimersByTime(1000);
154
+ jest.advanceTimersByTime(3000);
155
155
 
156
- await expect(promise).rejects.toThrow('[RequestCacheService] Request timeout after 1 seconds');
156
+ await expect(promise).rejects.toThrow('[RequestCacheService] Request timeout after 3 seconds');
157
157
  });
158
158
 
159
159
  it('should handle cache expiration', async () => {
@@ -217,16 +217,16 @@ describe('PaymentService - Basic Tests', () => {
217
217
  }
218
218
  }
219
219
  });
220
- }, 2000);
220
+ }, 5000);
221
221
  })
222
222
  );
223
223
 
224
224
  const promise = paymentService.getProductsInfo();
225
225
 
226
226
  // Advance timers to 1 second to trigger timeout
227
- jest.advanceTimersByTime(1000);
227
+ jest.advanceTimersByTime(3000);
228
228
 
229
- await expect(promise).rejects.toThrow('[RequestCacheService] Request timeout after 1 seconds');
229
+ await expect(promise).rejects.toThrow('[RequestCacheService] Request timeout after 3 seconds');
230
230
  });
231
231
 
232
232
  it('should increment failure count on failed requests', async () => {
@@ -167,7 +167,7 @@ class BasePaymentService extends RequestCacheService<
167
167
  constructor(private apiEndpoint: string, private paymentType: PaymentPurchaseType) {
168
168
  super(new PaymentRequestAdapter(), {
169
169
  duration: 10 * 60 * 1000, // 10分钟缓存 paymentChoices
170
- timeout: 1000 // 1秒超时
170
+ timeout: 3000 // 3秒超时
171
171
  });
172
172
  }
173
173
 
@@ -40,10 +40,6 @@ onNative('onPaymentStateChange', (data) => {
40
40
  }
41
41
 
42
42
  if (status === 'SUCCESS') {
43
- createToast(`{slot-success} {slot-i18n-jolicoin.unlockSuccess}`, {
44
- position: 'center',
45
- duration: 3000
46
- });
47
43
  deferred.resolve({
48
44
  code: 'SUCCESS' as ResponseType,
49
45
  message: 'jolicoin payment success',
@@ -7,7 +7,7 @@ import {
7
7
  UseModalFrequencyEventName,
8
8
  IUseModalFrequencyConfig
9
9
  } from '@/common/rewards/reward-emitter';
10
- import { createPaymentJolicoinModal } from '@jolibox/ui';
10
+ import { createPaymentJolicoinModal, createToast } from '@jolibox/ui';
11
11
  import { innerFetch as fetch } from '@/native/network';
12
12
  import { StandardResponse } from '@jolibox/types';
13
13
  import { context } from '@/common/context';
@@ -220,6 +220,7 @@ rewardsEmitter.on(
220
220
  productId,
221
221
  appStoreProductId
222
222
  });
223
+
223
224
  loading.hide();
224
225
  track(paymentConfig.trackEvents.payResult, {
225
226
  eventType: EventType.Other,
@@ -232,12 +233,15 @@ rewardsEmitter.on(
232
233
  ?.totalAmountStr ?? '',
233
234
  payResult: code
234
235
  });
235
- console.log('payment result', code);
236
236
  if (code !== 'SUCCESS') {
237
237
  /** add timeout for google panel closed */
238
238
  console.info('[JoliboxSDK] payment failed in payment.invokePaymet');
239
239
  return;
240
240
  }
241
+ createToast(`{slot-success} {slot-i18n-jolicoin.unlockSuccess}`, {
242
+ position: 'center',
243
+ duration: 3000
244
+ });
241
245
  rewardsEmitter.emit(PaymentResultEventName, {
242
246
  paymentResult: 'SUCCESS',
243
247
  currency: currencyType
@@ -1,6 +1,6 @@
1
1
  import { ISubscriptionPlanData } from './type';
2
2
  import { ISubscriptionTierData } from '@jolibox/types';
3
- import { Deferred, isUndefinedOrNull } from '@jolibox/common';
3
+ import { Deferred, isUndefinedOrNull, sleep } from '@jolibox/common';
4
4
  import { StandardResponse } from '@jolibox/types';
5
5
  import { applyNative, onNative } from '@jolibox/native-bridge';
6
6
  import { innerFetch as fetch } from '@/native/network';
@@ -12,6 +12,16 @@ import { createSubscriptionInternalError } from './registers/base';
12
12
  import { SubscriptionErrorCodeMap } from './registers/type';
13
13
  import { ResponseType } from '@jolibox/types';
14
14
 
15
+ interface IUserSubData {
16
+ subPlanId: string;
17
+ tier: string;
18
+ domain: string;
19
+ status: 'ACTIVE' | 'CANCELED' | 'IN_GRACE' | 'EXPIRED';
20
+ validUntil: number;
21
+ planType: string;
22
+ haveNextPayment: boolean;
23
+ }
24
+
15
25
  // Request/Response interfaces for RequestCacheService
16
26
  type SubscriptionRequest = Record<string, never>;
17
27
 
@@ -215,19 +225,85 @@ class BaseSubscriptionService extends RequestCacheService<
215
225
  appStoreProductId: string;
216
226
  }): Promise<{ subPlanId: string; message: string; result: 'SUCCESS' | 'FAILED' | 'FAILURE_SUBSCRIPTED' }> {
217
227
  console.log('[SubscriptionService] subscribe params', subscriptionHelper);
218
- const result = await subscriptionHelper.invokeSubscription('SUB_APP', {
228
+
229
+ // Start subscription and compensation polling in parallel
230
+ const abortController = new AbortController();
231
+ const subscribePromise = subscriptionHelper.invokeSubscription('SUB_APP', {
219
232
  productId: params.nativeProductId,
220
233
  appStoreProductId: params.appStoreProductId,
221
234
  planId: params.basePlanId
222
235
  });
236
+ const compensationPromise = this.pollSubscriptionStatus(abortController.signal);
237
+
238
+ // Wait for both to complete or first success
239
+ const result = (await Promise.race([
240
+ subscribePromise.then((subscribeResult) => {
241
+ // Cancel polling when subscribe completes
242
+ abortController.abort();
243
+ return {
244
+ result: subscribeResult.code as 'SUCCESS' | 'FAILED' | 'FAILURE_SUBSCRIPTED',
245
+ subPlanId: subscribeResult.data?.subPlanId,
246
+ message: subscribeResult.message
247
+ };
248
+ }),
249
+ compensationPromise.then((pollingResult) => {
250
+ if (pollingResult?.result === 'SUCCESS') {
251
+ abortController.abort();
252
+ return pollingResult;
253
+ }
254
+ // If polling fails or times out, wait forever (let subscribe resolve)
255
+ return new Promise(() => {
256
+ console.log('[SubscriptionPolling] Timeout for subscriptionService.subscribe completion');
257
+ });
258
+ })
259
+ ])) as { result: 'SUCCESS' | 'FAILED' | 'FAILURE_SUBSCRIPTED'; subPlanId: string; message?: string };
223
260
 
224
261
  return {
225
- result: result.code as 'SUCCESS' | 'FAILED' | 'FAILURE_SUBSCRIPTED',
226
- subPlanId: result.data?.subPlanId,
227
- message: result.message
262
+ result: result.result,
263
+ subPlanId: result.subPlanId,
264
+ message: result.message ?? ''
228
265
  };
229
266
  }
230
267
 
268
+ // Polling function for subscription status compensation
269
+ private async pollSubscriptionStatus(abortSignal: AbortSignal): Promise<{
270
+ result: 'SUCCESS' | 'FAILED' | 'FAILURE_SUBSCRIPTED';
271
+ subPlanId: string;
272
+ message?: string;
273
+ } | void> {
274
+ const maxAttempts = 10; // Poll for 50 seconds max
275
+ const interval = 5000; // 5 second intervals
276
+
277
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
278
+ if (abortSignal.aborted) {
279
+ console.log('[SubscriptionPolling] Aborted due to subscribe completion');
280
+ return;
281
+ }
282
+
283
+ try {
284
+ const { response } = await fetch<StandardResponse<IUserSubData>>('/api/subs/info', {
285
+ method: 'GET',
286
+ appendHostCookie: true,
287
+ responseType: 'json'
288
+ });
289
+
290
+ if (response.data?.data?.status === 'ACTIVE') {
291
+ return {
292
+ result: 'SUCCESS',
293
+ subPlanId: response.data.data.subPlanId,
294
+ message: 'Subscription activated via polling'
295
+ };
296
+ }
297
+ } catch (error) {
298
+ console.warn(`[SubscriptionPolling] Attempt ${attempt + 1} failed:`, error);
299
+ }
300
+
301
+ if (attempt < maxAttempts - 1 && !abortSignal.aborted) {
302
+ await sleep(interval);
303
+ }
304
+ }
305
+ }
306
+
231
307
  async invokeSubscriptionPanel(): Promise<
232
308
  StandardResponse<{ result: 'SUCCESS' | 'FAILED'; subPlanId?: string }>
233
309
  > {