@jolibox/implement 1.1.12 → 1.1.13-beta.10

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 (85) hide show
  1. package/.rush/temp/package-deps_build.json +35 -41
  2. package/.rush/temp/shrinkwrap-deps.json +1 -1
  3. package/dist/common/ads/anti-cheating.d.ts +5 -0
  4. package/dist/common/context/index.d.ts +5 -0
  5. package/dist/common/context/url-parse.d.ts +8 -1
  6. package/dist/common/rewards/__tests__/can-use-jolicoin.test.d.ts +1 -0
  7. package/dist/common/rewards/fetch-reward.d.ts +2 -0
  8. package/dist/common/rewards/index.d.ts +18 -0
  9. package/dist/common/rewards/registers/use-ads.d.ts +3 -0
  10. package/dist/common/rewards/registers/use-jolicoin.d.ts +9 -0
  11. package/dist/common/rewards/registers/utils/index.d.ts +2 -0
  12. package/dist/common/rewards/reward-helper.d.ts +14 -0
  13. package/dist/common/rewards/type.d.ts +23 -0
  14. package/dist/common/utils/index.d.ts +7 -0
  15. package/dist/index.js +3 -3
  16. package/dist/index.native.js +131 -4
  17. package/dist/native/api/index.d.ts +1 -0
  18. package/dist/native/api/keyboard.d.ts +1 -1
  19. package/dist/native/api/navigate.d.ts +1 -0
  20. package/dist/native/network/create-fetch.d.ts +1 -0
  21. package/dist/native/ui/retention.d.ts +1 -0
  22. package/implement.build.log +2 -2
  23. package/package.json +5 -3
  24. package/src/common/ads/anti-cheating.test.ts +4 -2
  25. package/src/common/ads/anti-cheating.ts +12 -4
  26. package/src/common/ads/index.ts +51 -33
  27. package/src/common/context/index.ts +22 -3
  28. package/src/common/context/url-parse.ts +24 -1
  29. package/src/common/rewards/__tests__/can-use-jolicoin.test.ts +94 -0
  30. package/src/common/rewards/fetch-reward.ts +33 -0
  31. package/src/common/rewards/index.ts +20 -0
  32. package/src/common/rewards/registers/use-ads.ts +9 -0
  33. package/src/common/rewards/registers/use-jolicoin.ts +89 -0
  34. package/src/common/rewards/registers/utils/index.ts +11 -0
  35. package/src/common/rewards/reward-helper.ts +56 -0
  36. package/src/common/rewards/type.ts +25 -0
  37. package/src/common/utils/index.ts +7 -0
  38. package/src/h5/api/ads.ts +20 -1
  39. package/src/h5/api/get-system-info.ts +1 -1
  40. package/src/h5/http/utils/__tests__/xua.test.ts +1 -1
  41. package/src/native/api/ads.ts +31 -3
  42. package/src/native/api/get-system-info.ts +2 -2
  43. package/src/native/api/index.ts +1 -0
  44. package/src/native/api/keyboard.ts +1 -1
  45. package/src/native/api/lifecycle.ts +16 -4
  46. package/src/native/api/login.ts +1 -1
  47. package/src/native/api/navigate.ts +61 -0
  48. package/src/native/api/request.ts +19 -10
  49. package/src/native/api/storage.ts +1 -1
  50. package/src/native/bootstrap/index.ts +111 -27
  51. package/src/native/network/create-fetch.ts +7 -2
  52. package/src/native/network/utils.ts +13 -6
  53. package/src/native/report/errors/index.ts +1 -1
  54. package/src/native/report/index.ts +1 -1
  55. package/src/native/report/task-tracker.ts +1 -1
  56. package/src/native/ui/retention.ts +153 -0
  57. package/dist/native/bootstrap/bridge.d.ts +0 -4
  58. package/dist/native/js-bridge/const.d.ts +0 -5
  59. package/dist/native/js-bridge/index.d.ts +0 -2
  60. package/dist/native/js-bridge/invoke.d.ts +0 -21
  61. package/dist/native/js-bridge/js-bridge.d.ts +0 -6
  62. package/dist/native/js-bridge/report.d.ts +0 -63
  63. package/dist/native/js-bridge/subscribe.d.ts +0 -8
  64. package/dist/native/js-bridge/types.d.ts +0 -14
  65. package/dist/native/js-bridge/utils.d.ts +0 -17
  66. package/dist/native/js-core/index.d.ts +0 -3
  67. package/dist/native/js-core/jolibox-js-core.d.ts +0 -45
  68. package/dist/native/js-core/message-port.d.ts +0 -12
  69. package/dist/native/js-core/utils.d.ts +0 -7
  70. package/src/native/bootstrap/bridge.ts +0 -59
  71. package/src/native/js-bridge/const.ts +0 -11
  72. package/src/native/js-bridge/index.ts +0 -2
  73. package/src/native/js-bridge/invoke.ts +0 -208
  74. package/src/native/js-bridge/js-bridge.ts +0 -23
  75. package/src/native/js-bridge/report.ts +0 -311
  76. package/src/native/js-bridge/subscribe.ts +0 -50
  77. package/src/native/js-bridge/types.ts +0 -26
  78. package/src/native/js-bridge/utils.ts +0 -116
  79. package/src/native/js-core/index.ts +0 -4
  80. package/src/native/js-core/jolibox-js-core.ts +0 -188
  81. package/src/native/js-core/message-port.ts +0 -52
  82. package/src/native/js-core/utils.ts +0 -9
  83. package/src/native/types/global.d.ts +0 -26
  84. package/src/native/types/native-method-map.d.ts +0 -300
  85. package/src/native/types/native-method.d.ts +0 -30
@@ -6,3 +6,4 @@ import './keyboard';
6
6
  import './task';
7
7
  import './login';
8
8
  import './ads';
9
+ import './navigate';
@@ -1,7 +1,7 @@
1
1
  export declare const showKeyboard: (params: {
2
2
  defaultValue?: string | undefined;
3
- maxLength?: unknown;
4
3
  multiple?: boolean | undefined;
4
+ maxLength?: unknown;
5
5
  }) => import("@jolibox/types").StandardResponse<void>;
6
6
  export declare const updateKeyboard: (params: {
7
7
  value: string;
@@ -0,0 +1 @@
1
+ export {};
@@ -15,6 +15,7 @@ type PublicFetch = <T>(url: string, options?: FetchOptions) => Promise<{
15
15
  }>;
16
16
  export declare function createFetch(createMethod: 'createRequestTaskSync', operateMethod: 'operateRequestTaskSync', options?: {
17
17
  baseUrl?: string;
18
+ defaultHeaders?: Record<string, string>;
18
19
  type: 'public';
19
20
  }): PublicFetch;
20
21
  export declare function createFetch(createMethod: 'createRequestTaskSync', operateMethod: 'operateRequestTaskSync', options?: {
@@ -0,0 +1 @@
1
+ export declare function openRetentionSchema(): Promise<boolean>;
@@ -1,9 +1,9 @@
1
1
  Invoking: npm run clean && npm run build:esm && tsc
2
2
 
3
- > @jolibox/implement@1.1.12 clean
3
+ > @jolibox/implement@1.1.13-beta.10 clean
4
4
  > rimraf ./dist
5
5
 
6
6
 
7
- > @jolibox/implement@1.1.12 build:esm
7
+ > @jolibox/implement@1.1.13-beta.10 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,14 +1,16 @@
1
1
  {
2
2
  "name": "@jolibox/implement",
3
3
  "description": "This project is Jolibox JS-SDk implement for Native && H5",
4
- "version": "1.1.12",
4
+ "version": "1.1.13-beta.10",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
7
7
  "license": "MIT",
8
8
  "dependencies": {
9
- "@jolibox/common": "1.1.12",
10
- "@jolibox/types": "1.1.12",
9
+ "@jolibox/common": "1.1.13-beta.10",
10
+ "@jolibox/types": "1.1.13-beta.10",
11
+ "@jolibox/native-bridge": "1.1.13-beta.10",
11
12
  "localforage": "1.10.0",
13
+ "@jolibox/ui": "1.0.0",
12
14
  "web-vitals": "4.2.4"
13
15
  },
14
16
  "devDependencies": {
@@ -20,7 +20,9 @@ describe('createLeadingNotifiableDebounce', () => {
20
20
  };
21
21
 
22
22
  it('should ban initial call', async () => {
23
- localStorage.clear();
23
+ (window as any).__joliboxLocalStorage__ = window.localStorage;
24
+
25
+ (window as any).__joliboxLocalStorage__.clear();
24
26
 
25
27
  const adsAntiCheating = new AdsAntiCheating(track, http, () => true, config);
26
28
  await new Promise((resolve) => setTimeout(resolve, 10));
@@ -32,7 +34,7 @@ describe('createLeadingNotifiableDebounce', () => {
32
34
  });
33
35
 
34
36
  it('should ban continous calling', async () => {
35
- localStorage.clear();
37
+ (window as any).__joliboxLocalStorage__.clear();
36
38
 
37
39
  const adsAntiCheating = new AdsAntiCheating(track, http, () => true, config);
38
40
  await new Promise((resolve) => setTimeout(resolve, 100));
@@ -1,5 +1,7 @@
1
1
  import type { IHttpClient } from '../http';
2
2
  import type { Track } from '../report';
3
+ import { getOriginalLocalStorage } from '@jolibox/common';
4
+ getOriginalLocalStorage();
3
5
 
4
6
  export type AdsDisplayPermission =
5
7
  | 'BLOCK_INITIAL'
@@ -24,6 +26,12 @@ export interface IAdsAntiCheatingConfig {
24
26
  highFreqThreshold?: number; // default 2000 (2s)
25
27
  }
26
28
 
29
+ declare global {
30
+ interface Window {
31
+ __joliboxLocalStorage__: Storage;
32
+ }
33
+ }
34
+
27
35
  export class AdsAntiCheating {
28
36
  private checkShouldBanInitial: ReturnType<typeof createInitialStrategy>;
29
37
  private checkShouldBanHighFreq: ReturnType<typeof createHighFreqStrategy>;
@@ -177,7 +185,7 @@ const createBanForTimeStrategy = (
177
185
  _records: [] as CallAdsHistory[],
178
186
  init() {
179
187
  try {
180
- const fromStorage = JSON.parse(window.localStorage.getItem(TIMESTAMP_STORAGE_KEY) ?? '[]');
188
+ const fromStorage = JSON.parse(window.__joliboxLocalStorage__.getItem(TIMESTAMP_STORAGE_KEY) ?? '[]');
181
189
  if (Array.isArray(fromStorage)) {
182
190
  this._records = fromStorage;
183
191
  } else {
@@ -192,7 +200,7 @@ const createBanForTimeStrategy = (
192
200
  },
193
201
  set values(value: CallAdsHistory[]) {
194
202
  try {
195
- window.localStorage.setItem(TIMESTAMP_STORAGE_KEY, JSON.stringify(value));
203
+ window.__joliboxLocalStorage__.setItem(TIMESTAMP_STORAGE_KEY, JSON.stringify(value));
196
204
  } catch {
197
205
  // console.error('Failed to save timestamps');
198
206
  // do nothing
@@ -205,7 +213,7 @@ const createBanForTimeStrategy = (
205
213
  _record: false,
206
214
  init() {
207
215
  try {
208
- const fromStorage = JSON.parse(window.localStorage.getItem(BAN_FOR_TIME_KEY) ?? 'false');
216
+ const fromStorage = JSON.parse(window.__joliboxLocalStorage__.getItem(BAN_FOR_TIME_KEY) ?? 'false');
209
217
  if (typeof fromStorage === 'boolean') {
210
218
  this._record = fromStorage;
211
219
  }
@@ -228,7 +236,7 @@ const createBanForTimeStrategy = (
228
236
  },
229
237
  set value(value: boolean) {
230
238
  try {
231
- window.localStorage.setItem(BAN_FOR_TIME_KEY, JSON.stringify(value));
239
+ window.__joliboxLocalStorage__.setItem(BAN_FOR_TIME_KEY, JSON.stringify(value));
232
240
  } catch {
233
241
  // do nothing
234
242
  }
@@ -377,6 +377,55 @@ export class JoliboxAdsImpl {
377
377
  * @returns IAdBreakParams
378
378
  */
379
379
  private wrapAdBreakParams = (params: IAdBreakParams): IAdBreakParams => {
380
+ let googleHasResponse = false;
381
+
382
+ /* hook adBreakDone to track adBreakDone event -- start */
383
+ const originAdBreakDone = params.adBreakDone;
384
+ const adBreakDone = (placementInfo: IPlacementInfo) => {
385
+ googleHasResponse = true;
386
+ this.adsActionDetection.adBreakIsShowing = false;
387
+ this.track('CallAdBreakDone', {
388
+ breakType: placementInfo.breakType,
389
+ breakName: placementInfo.breakName ?? '',
390
+ breakFormat: placementInfo.breakFormat,
391
+ breakStatus: placementInfo.breakStatus
392
+ });
393
+ if (originAdBreakDone) {
394
+ originAdBreakDone(placementInfo);
395
+ }
396
+ };
397
+ params.adBreakDone = adBreakDone;
398
+ /* hook adBreakDone to track adBreakDone event -- end */
399
+
400
+ /* hook for reward -- start */
401
+ if (params.type === 'reward') {
402
+ const originBeforeReward = params.beforeReward;
403
+ const wrapShowAdFn = (originShowAdFn: () => void) => () => {
404
+ this.track('CallShowAdFn', null);
405
+ originShowAdFn();
406
+ };
407
+ const beforeReward = (originShowAdFn: () => void) => {
408
+ googleHasResponse = true;
409
+ if (originBeforeReward) {
410
+ originBeforeReward(wrapShowAdFn(originShowAdFn));
411
+ }
412
+ };
413
+ params.beforeReward = beforeReward;
414
+
415
+ window.setTimeout(() => {
416
+ if (!googleHasResponse) {
417
+ this.track('CallAdBreakTimeout', { type: params.type });
418
+ params.adBreakDone?.({
419
+ breakType: params.type,
420
+ breakName: params.name ?? '',
421
+ breakFormat: 'reward',
422
+ breakStatus: 'timeout'
423
+ });
424
+ }
425
+ }, 5000); // Timeout duration in milliseconds, 5 seconds
426
+ }
427
+ /* hook for reward -- end */
428
+
380
429
  const isPreroll = (params: IAdBreakParams): params is IPrerollParams => {
381
430
  return params.type === 'preroll';
382
431
  };
@@ -396,6 +445,7 @@ export class JoliboxAdsImpl {
396
445
  const originAfterAd = params.afterAd;
397
446
 
398
447
  const beforeAd = () => {
448
+ googleHasResponse = true;
399
449
  adEventEmitter.emit('isAdShowing', true);
400
450
  this.track('CallBeforeAd', {
401
451
  type: params.type,
@@ -407,6 +457,7 @@ export class JoliboxAdsImpl {
407
457
  };
408
458
 
409
459
  const afterAd = () => {
460
+ googleHasResponse = true;
410
461
  adEventEmitter.emit('isAdShowing', false);
411
462
  this.track('CallAfterAd', {
412
463
  type: params.type,
@@ -529,39 +580,6 @@ export class JoliboxAdsImpl {
529
580
 
530
581
  const type = params.type;
531
582
 
532
- /* hook adBreakDone to track adBreakDone event -- start */
533
- const originAdBreakDone = params.adBreakDone;
534
- const adBreakDone = (placementInfo: IPlacementInfo) => {
535
- this.adsActionDetection.adBreakIsShowing = false;
536
- this.track('CallAdBreakDone', {
537
- breakType: placementInfo.breakType,
538
- breakName: placementInfo.breakName ?? '',
539
- breakFormat: placementInfo.breakFormat,
540
- breakStatus: placementInfo.breakStatus
541
- });
542
- if (originAdBreakDone) {
543
- originAdBreakDone(placementInfo);
544
- }
545
- };
546
- params.adBreakDone = adBreakDone;
547
- /* hook adBreakDone to track adBreakDone event -- end */
548
-
549
- /* hook beforeAd to track beforeAd event -- start */
550
- if (params.type === 'reward') {
551
- const originBeforeReward = params.beforeReward;
552
- const wrapShowAdFn = (originShowAdFn: () => void) => () => {
553
- this.track('CallShowAdFn', null);
554
- originShowAdFn();
555
- };
556
- const beforeReward = (originShowAdFn: () => void) => {
557
- if (originBeforeReward) {
558
- originBeforeReward(wrapShowAdFn(originShowAdFn));
559
- }
560
- };
561
- params.beforeReward = beforeReward;
562
- }
563
- /* hook beforeAd to track beforeAd event -- end */
564
-
565
583
  let paramsToTrack;
566
584
  switch (type) {
567
585
  case 'preroll':
@@ -1,7 +1,7 @@
1
1
  import { mergeArray, mergeWith } from '@jolibox/common';
2
2
  import { DeviceInfo, HostInfo, HostUserInfo, SdkInfo } from './types';
3
3
  import { Env } from '@jolibox/types';
4
- import { parseUrlQuery } from './url-parse';
4
+ import { parseUrlQuery, encodeJoliSourceQuery, QueryParams } from './url-parse';
5
5
  import { getAppVersion } from '../http/xua';
6
6
  import { getDeviceId } from '@jolibox/common';
7
7
 
@@ -36,12 +36,13 @@ const env = Object.assign({}, nativeEnv?.() ?? defaultEnv);
36
36
  type MPType = 'game' | 'miniApp';
37
37
 
38
38
  const wrapContext = () => {
39
- const { payloadJson, headerJson } = env.schema.length ? parseUrlQuery(env.schema) : {};
39
+ const { payloadJson, headerJson, signature } = env.schema.length ? parseUrlQuery(env.schema) : {};
40
40
  const defaultSessionId = `${env.deviceInfo.did}-${new Date().getTime()}`;
41
41
  const url = new URL(env.schema.length ? env.schema : window.location.href);
42
42
  const urlParams = url.searchParams;
43
43
  const defaultGameID = urlParams.get('mpId') ?? urlParams.get('appId') ?? urlParams.get('gameId') ?? '';
44
- const sessionId = payloadJson?.sessionId ?? urlParams.get('sessionId') ?? defaultSessionId;
44
+ const sessionId =
45
+ env.clientSessionId ?? payloadJson?.sessionId ?? urlParams.get('sessionId') ?? defaultSessionId;
45
46
  const testAdsMode = !!(payloadJson?.testAdsMode ?? urlParams.get('testAdsMode') === 'true');
46
47
  const joliboxEnv = payloadJson?.joliboxEnv ?? urlParams.get('joliboxEnv') ?? 'production';
47
48
  const testMode = joliboxEnv === 'staging';
@@ -93,8 +94,26 @@ const wrapContext = () => {
93
94
  get webviewId(): number {
94
95
  return env.webviewId ?? -1;
95
96
  },
97
+ get shouldInterupt(): boolean | undefined {
98
+ return payloadJson?.__shouldInterupt;
99
+ },
100
+ get from(): number | undefined {
101
+ return payloadJson?.__from;
102
+ },
103
+ get baseApiHost(): string {
104
+ return testMode ? 'https://stg-api.jolibox.com' : 'https://api.jolibox.com';
105
+ },
96
106
  onEnvConfigChanged: (newConfig: Partial<Env>) => {
97
107
  mergeWith(env, newConfig, mergeArray);
108
+ },
109
+ encodeJoliSourceQuery: (newPayloadJson: QueryParams['payloadJson']) => {
110
+ if (headerJson && signature) {
111
+ return encodeJoliSourceQuery(
112
+ { ...payloadJson, ...newPayloadJson },
113
+ urlParams.get('joliSource') ?? ''
114
+ );
115
+ }
116
+ return urlParams.get('joliSource') ?? '';
98
117
  }
99
118
  };
100
119
  };
@@ -14,6 +14,12 @@ interface PayloadJson {
14
14
  joliboxEnv?: 'staging' | 'production';
15
15
  sessionId?: string;
16
16
  __mpType?: 'game' | 'miniApp';
17
+ __orientation?: 'HORIZONTAL' | 'VERTICAL';
18
+ __transparent?: boolean;
19
+ __entryPath?: string;
20
+ __showStatusBar?: boolean;
21
+ __shouldInterupt?: boolean;
22
+ __from?: number; // 从哪个小程序打开的
17
23
  }
18
24
 
19
25
  interface Signature {
@@ -21,7 +27,7 @@ interface Signature {
21
27
  payloadSig?: string;
22
28
  }
23
29
 
24
- interface QueryParams {
30
+ export interface QueryParams {
25
31
  headerJson: HeaderJson;
26
32
  payloadJson: PayloadJson;
27
33
  signature: Signature;
@@ -38,6 +44,12 @@ const base64UrlDecode = <T>(input: string): T => {
38
44
  }
39
45
  };
40
46
 
47
+ const base64UrlEncode = <T>(input: T): string => {
48
+ const jsonStr = JSON.stringify(input);
49
+ const base64 = btoa(jsonStr);
50
+ return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
51
+ };
52
+
41
53
  export const parseUrlQuery = (url: string): QueryParams => {
42
54
  try {
43
55
  const urlObj = new URL(url);
@@ -63,3 +75,14 @@ export const parseUrlQuery = (url: string): QueryParams => {
63
75
  };
64
76
  }
65
77
  };
78
+
79
+ export const encodeJoliSourceQuery = (payloadJson: PayloadJson, originJoliSource: string): string => {
80
+ const joli_source = originJoliSource.split('.');
81
+ if (joli_source && joli_source.length === 3) {
82
+ const headerJsonStr = joli_source[0];
83
+ const payloadJsonStr = base64UrlEncode(payloadJson);
84
+ const signatureJsonStr = joli_source[2];
85
+ return `${headerJsonStr}.${payloadJsonStr}.${signatureJsonStr}`;
86
+ }
87
+ return originJoliSource;
88
+ };
@@ -0,0 +1,94 @@
1
+ import { IUnlockOptionType } from '../type';
2
+ import { canUseJolicoin } from '../registers/utils';
3
+
4
+ describe('canUseJolicoin', () => {
5
+ it('should return true when joliCoin balance is sufficient and autoDeduct is enabled', () => {
6
+ const unlockOptions = [
7
+ {
8
+ type: 'JOLI_COIN' as IUnlockOptionType,
9
+ joliCoinChoices: [{ joliCoinQuantity: 100 }]
10
+ }
11
+ ];
12
+ const joliCoin = { balance: 100, enableAutoDeduct: true };
13
+
14
+ expect(canUseJolicoin(unlockOptions, joliCoin)).toBe(true);
15
+ });
16
+
17
+ it('should return true when joliCoin balance is more than required', () => {
18
+ const unlockOptions = [
19
+ {
20
+ type: 'JOLI_COIN' as IUnlockOptionType,
21
+ joliCoinChoices: [{ joliCoinQuantity: 50 }]
22
+ }
23
+ ];
24
+ const joliCoin = { balance: 100, enableAutoDeduct: true };
25
+
26
+ expect(canUseJolicoin(unlockOptions, joliCoin)).toBe(true);
27
+ });
28
+
29
+ it('should return false when joliCoin balance is insufficient', () => {
30
+ const unlockOptions = [
31
+ {
32
+ type: 'JOLI_COIN' as IUnlockOptionType,
33
+ joliCoinChoices: [{ joliCoinQuantity: 150 }]
34
+ }
35
+ ];
36
+ const joliCoin = { balance: 100, enableAutoDeduct: true };
37
+
38
+ expect(canUseJolicoin(unlockOptions, joliCoin)).toBe(false);
39
+ });
40
+
41
+ it('should return false when autoDeduct is disabled', () => {
42
+ const unlockOptions = [
43
+ {
44
+ type: 'JOLI_COIN' as IUnlockOptionType,
45
+ joliCoinChoices: [{ joliCoinQuantity: 50 }]
46
+ }
47
+ ];
48
+ const joliCoin = { balance: 100, enableAutoDeduct: false };
49
+
50
+ expect(canUseJolicoin(unlockOptions, joliCoin)).toBe(false);
51
+ });
52
+
53
+ it('should return false when there is no JOLI_COIN option', () => {
54
+ const unlockOptions = [
55
+ {
56
+ type: 'ADS' as IUnlockOptionType,
57
+ joliCoinChoices: []
58
+ }
59
+ ];
60
+ const joliCoin = { balance: 100, enableAutoDeduct: true };
61
+
62
+ expect(canUseJolicoin(unlockOptions, joliCoin)).toBe(false);
63
+ });
64
+
65
+ it('should return false with empty unlockOptions', () => {
66
+ const unlockOptions: any[] = [];
67
+ const joliCoin = { balance: 100, enableAutoDeduct: true };
68
+
69
+ expect(canUseJolicoin(unlockOptions, joliCoin)).toBe(false);
70
+ });
71
+
72
+ it('should return false when joliCoin is undefined', () => {
73
+ const unlockOptions = [
74
+ {
75
+ type: 'JOLI_COIN' as IUnlockOptionType,
76
+ joliCoinChoices: [{ joliCoinQuantity: 50 }]
77
+ }
78
+ ];
79
+
80
+ expect(canUseJolicoin(unlockOptions, undefined)).toBe(false);
81
+ });
82
+
83
+ it('should return true when at least one choice is valid', () => {
84
+ const unlockOptions = [
85
+ {
86
+ type: 'JOLI_COIN' as IUnlockOptionType,
87
+ joliCoinChoices: [{ joliCoinQuantity: 200 }, { joliCoinQuantity: 50 }]
88
+ }
89
+ ];
90
+ const joliCoin = { balance: 100, enableAutoDeduct: true };
91
+
92
+ expect(canUseJolicoin(unlockOptions, joliCoin)).toBe(true);
93
+ });
94
+ });
@@ -0,0 +1,33 @@
1
+ import { UnlockOptionsEventName, unlockOptionsEmitter } from '.';
2
+ import { IHttpClient } from '../http';
3
+ import { RewardsHelper, RewardType } from './reward-helper';
4
+ import { IJolicoinRewardOption } from './type';
5
+
6
+ const priority = () => {
7
+ return (a: RewardType, b: RewardType) => {
8
+ if (a === 'JOLI_COIN' && b === 'ADS') return -1;
9
+ if (a === 'ADS' && b === 'JOLI_COIN') return 1;
10
+ return 0;
11
+ };
12
+ };
13
+
14
+ export const createRewardFetcher = (rewardsHelper: RewardsHelper) => {
15
+ rewardsHelper.registerRewardsFetcher(async (httpClient: IHttpClient) => {
16
+ const defaultRewards: RewardType[] = [];
17
+ const res = await httpClient.post<IJolicoinRewardOption>('/api/games/unlock-options', {});
18
+ if (res.code !== 'SUCCESS') {
19
+ return defaultRewards;
20
+ }
21
+ unlockOptionsEmitter.emit(UnlockOptionsEventName, {
22
+ options: res.data?.unlockOptions || [],
23
+ userJoliCoin: res.extra?.joliCoin || {
24
+ balance: 0,
25
+ enableAutoDeduct: false
26
+ }
27
+ });
28
+
29
+ const rewardsTypes = res.data?.unlockOptions?.map((option) => option.type) || Array.from(defaultRewards);
30
+ // Sort reward types with JOLI_COIN having higher priority than ADS
31
+ return rewardsTypes.sort(priority());
32
+ });
33
+ };
@@ -0,0 +1,20 @@
1
+ import { createRewardsHelper } from './reward-helper';
2
+ import { createRewardFetcher } from './fetch-reward';
3
+ import { EventEmitter } from '@jolibox/common';
4
+ import { IUnlockOption, IJoliCoin } from './type';
5
+
6
+ export const rewardsHelper = createRewardsHelper();
7
+ createRewardFetcher(rewardsHelper);
8
+
9
+ export * from './registers/use-ads';
10
+ export * from './registers/use-jolicoin';
11
+
12
+ export interface IUnlockOptionsEvent {
13
+ options: IUnlockOption[];
14
+ userJoliCoin: IJoliCoin;
15
+ }
16
+
17
+ export const UnlockOptionsEventName = 'UNLOCK_OPTIONS_CHANGED' as const;
18
+ export const unlockOptionsEmitter = new EventEmitter<{
19
+ [UnlockOptionsEventName]: [IUnlockOptionsEvent];
20
+ }>();
@@ -0,0 +1,9 @@
1
+ import { JoliboxAdsImpl, IRewardParams } from '../../ads';
2
+
3
+ export type AdsRewardsHandler = (params: IRewardParams) => Promise<boolean>;
4
+ export const createAdsRewardHandler = (ads: JoliboxAdsImpl): AdsRewardsHandler => {
5
+ return async (params: IRewardParams) => {
6
+ ads.adBreak(params);
7
+ return true;
8
+ };
9
+ };
@@ -0,0 +1,89 @@
1
+ import { IHttpClient } from '@/common/http';
2
+ import { context } from '@/common/context';
3
+ import { uuidv4 } from '@jolibox/common';
4
+ import { canUseJolicoin } from './utils';
5
+ import { unlockOptionsEmitter, UnlockOptionsEventName, IUnlockOptionsEvent } from '..';
6
+
7
+ interface IJolicoinUnlockRes {
8
+ code: 'SUCCESS' | 'BALANCE_NOT_ENOUGH' | 'EPISODE_LOCK_JUMP' | 'EPISODE_UNLOCK_ALREADY';
9
+ message: string;
10
+ data: {
11
+ transactionId: string;
12
+ quantity: number;
13
+ balance: number;
14
+ };
15
+ }
16
+
17
+ export type JolicoinRewardsHandler = () => Promise<boolean>;
18
+ export const createJolicoinRewardHandler = (
19
+ httpClient: IHttpClient,
20
+ {
21
+ onUnlockSuccess,
22
+ onUnlockFailed
23
+ }: {
24
+ onUnlockSuccess?: (data: { quantity: number; balance: number }) => void;
25
+ onUnlockFailed?: () => void;
26
+ }
27
+ ): JolicoinRewardsHandler => {
28
+ let unlockOptionsPromise: Promise<IUnlockOptionsEvent> | null = null;
29
+ let resolveUnlockOptions: ((value: IUnlockOptionsEvent) => void) | null = null;
30
+ let cachedUnlockOptions: IUnlockOptionsEvent | null = null;
31
+
32
+ const createUnlockOptionsPromise = () => {
33
+ if (cachedUnlockOptions) {
34
+ unlockOptionsPromise = Promise.resolve(cachedUnlockOptions);
35
+ return;
36
+ }
37
+
38
+ unlockOptionsPromise = new Promise<IUnlockOptionsEvent>((resolve) => {
39
+ resolveUnlockOptions = resolve;
40
+ });
41
+ };
42
+
43
+ createUnlockOptionsPromise();
44
+
45
+ unlockOptionsEmitter.on(UnlockOptionsEventName, (options) => {
46
+ cachedUnlockOptions = options;
47
+ if (resolveUnlockOptions) {
48
+ resolveUnlockOptions(options);
49
+ resolveUnlockOptions = null;
50
+ }
51
+ });
52
+
53
+ return async () => {
54
+ try {
55
+ if (!unlockOptionsPromise) {
56
+ createUnlockOptionsPromise();
57
+ }
58
+ const unlockOptions = await unlockOptionsPromise!;
59
+ if (!canUseJolicoin(unlockOptions?.options || [], unlockOptions?.userJoliCoin)) {
60
+ onUnlockFailed?.();
61
+ return false;
62
+ }
63
+
64
+ const unlockWithJolicoin = await httpClient.post<IJolicoinUnlockRes>('/api/joli-coin/unlock', {
65
+ data: {
66
+ // TODO: support drama
67
+ type: 'GAME_REWARD',
68
+ reqId: `${uuidv4()}-${context.mpType}-${Date.now()}`,
69
+ gameInfo: {
70
+ gameId: context.mpId
71
+ }
72
+ }
73
+ });
74
+ if (unlockWithJolicoin.code == 'SUCCESS') {
75
+ onUnlockSuccess?.({
76
+ quantity: unlockWithJolicoin.data.quantity,
77
+ balance: unlockWithJolicoin.data.balance
78
+ });
79
+ return true;
80
+ }
81
+ onUnlockFailed?.();
82
+ return false;
83
+ } catch (e) {
84
+ console.error(`JolicoinRewardHandler error:`, e);
85
+ onUnlockFailed?.();
86
+ return false;
87
+ }
88
+ };
89
+ };
@@ -0,0 +1,11 @@
1
+ import { IJoliCoin, IUnlockOption } from '@/common/rewards/type';
2
+
3
+ export const canUseJolicoin = (unlockOptions: IUnlockOption[], joliCoin?: IJoliCoin) => {
4
+ return unlockOptions.some(
5
+ (option) =>
6
+ option.type === 'JOLI_COIN' &&
7
+ option.joliCoinChoices.some(
8
+ (choice) => choice.joliCoinQuantity <= (joliCoin?.balance ?? 0) && !!joliCoin?.enableAutoDeduct
9
+ )
10
+ );
11
+ };
@@ -0,0 +1,56 @@
1
+ export type RewardType = 'ADS' | 'JOLI_COIN';
2
+
3
+ import { context } from '../context';
4
+ import type { AdsRewardsHandler } from './registers/use-ads';
5
+
6
+ export interface RewardHandlerMap {
7
+ ADS: AdsRewardsHandler;
8
+ JOLI_COIN: (params?: unknown) => Promise<boolean>;
9
+ }
10
+
11
+ export type RewardHandler<T extends RewardType> = RewardHandlerMap[T];
12
+
13
+ const isTestMode = context.testMode;
14
+ export function createRewardsHelper() {
15
+ const rewardsHandlers = new Map<RewardType, RewardHandler<any>>();
16
+ let rewardFetcher: ((...args: any[]) => Promise<RewardType[]>) | undefined;
17
+
18
+ return {
19
+ registerRewardHandler<T extends RewardType>(type: T, handler: RewardHandler<T>) {
20
+ rewardsHandlers.set(type, handler);
21
+ },
22
+ async handleReward<T extends RewardType>(rewardsTypes: T[], ...args: Parameters<RewardHandler<T>>) {
23
+ const result = await rewardsTypes.reduce(async (prevPromise, type) => {
24
+ const prevResult = await prevPromise;
25
+ if (prevResult === true) return true;
26
+
27
+ isTestMode && console.log(`handleReward ${type}`);
28
+ const handler = rewardsHandlers.get(type);
29
+ const nextResult = handler ? await handler(...args) : prevResult;
30
+ isTestMode && console.log(`handleReward ${type} ${nextResult}`);
31
+ return nextResult;
32
+ }, Promise.resolve(false));
33
+
34
+ return result;
35
+ },
36
+ async registerRewardsFetcher<T extends RewardType>(fetcher: (...args: any[]) => Promise<T[]>) {
37
+ rewardFetcher = async (...args: unknown[]) => {
38
+ try {
39
+ const rewardsTypes = await fetcher(...args);
40
+ return rewardsTypes;
41
+ } catch (e) {
42
+ console.error(`getRewardOptions error:`, e);
43
+ return ['ADS'];
44
+ }
45
+ };
46
+ },
47
+ async getRewardsTypes(...args: unknown[]): Promise<RewardType[]> {
48
+ if (!rewardFetcher) {
49
+ return ['ADS'];
50
+ }
51
+ return await rewardFetcher(...args);
52
+ }
53
+ };
54
+ }
55
+
56
+ export type RewardsHelper = ReturnType<typeof createRewardsHelper>;