@jolibox/implement 1.1.13-beta.7 → 1.1.13-beta.9

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.
@@ -1,9 +1,9 @@
1
1
  Invoking: npm run clean && npm run build:esm && tsc
2
2
 
3
- > @jolibox/implement@1.1.13-beta.7 clean
3
+ > @jolibox/implement@1.1.13-beta.9 clean
4
4
  > rimraf ./dist
5
5
 
6
6
 
7
- > @jolibox/implement@1.1.13-beta.7 build:esm
7
+ > @jolibox/implement@1.1.13-beta.9 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,14 @@
1
1
  {
2
2
  "name": "@jolibox/implement",
3
3
  "description": "This project is Jolibox JS-SDk implement for Native && H5",
4
- "version": "1.1.13-beta.7",
4
+ "version": "1.1.13-beta.9",
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.13-beta.7",
10
- "@jolibox/types": "1.1.13-beta.7",
11
- "@jolibox/native-bridge": "1.1.13-beta.7",
9
+ "@jolibox/common": "1.1.13-beta.9",
10
+ "@jolibox/types": "1.1.13-beta.9",
11
+ "@jolibox/native-bridge": "1.1.13-beta.9",
12
12
  "localforage": "1.10.0",
13
13
  "@jolibox/ui": "1.0.0",
14
14
  "web-vitals": "4.2.4"
@@ -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'
@@ -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>;
@@ -0,0 +1,25 @@
1
+ export interface IJoliCoin {
2
+ balance: number;
3
+ enableAutoDeduct: boolean;
4
+ }
5
+
6
+ export type IUnlockOptionType = 'JOLI_COIN' | 'ADS';
7
+
8
+ interface IJoliCoinChoice {
9
+ joliCoinQuantity: number;
10
+ }
11
+ export interface IUnlockOption {
12
+ type: IUnlockOptionType;
13
+ joliCoinChoices: IJoliCoinChoice[];
14
+ }
15
+
16
+ export interface IJolicoinRewardOption {
17
+ code: 'SUCCESS' | 'ERROR' | 'PARAMETER_ERROR' | 'EPISODE_LOCK_JUMP';
18
+ message: string;
19
+ data: {
20
+ unlockOptions?: IUnlockOption[];
21
+ };
22
+ extra: {
23
+ joliCoin: IJoliCoin;
24
+ };
25
+ }
@@ -1,9 +1,16 @@
1
1
  const JOLIBOX_CUSTOM_ADS_EVENT_TYPE = 'JOLIBOX_ADS_EVENT';
2
+ const JOLIBOX_CUSTOM_REWARDS_EVENT_TYPE = 'JOLIBOX_CUSTOM_REWARDS_EVENT';
2
3
 
3
4
  interface JoliboxCustomEvent {
4
5
  [JOLIBOX_CUSTOM_ADS_EVENT_TYPE]: {
5
6
  isAdShowing: boolean;
6
7
  };
8
+ [JOLIBOX_CUSTOM_REWARDS_EVENT_TYPE]: {
9
+ ['JOLI_COIN']?: {
10
+ quantity: number;
11
+ balance: number;
12
+ };
13
+ };
7
14
  }
8
15
 
9
16
  export const notifyCustomEvent = <T extends keyof JoliboxCustomEvent>(
package/src/h5/api/ads.ts CHANGED
@@ -3,10 +3,23 @@ import { track } from '../report';
3
3
  import { createCommands } from '@jolibox/common';
4
4
  import { httpClientManager } from '../http';
5
5
  import { notifyCustomEvent } from '@/common/utils';
6
+ import { createAdsRewardHandler, rewardsHelper, createJolicoinRewardHandler } from '@/common/rewards';
7
+
6
8
  const commands = createCommands();
7
9
 
8
10
  const httpClient = httpClientManager.create();
9
11
  const ads = new JoliboxAdsImpl(track, httpClient, () => httpClientManager.getNetworkStatus());
12
+ rewardsHelper.registerRewardHandler('ADS', createAdsRewardHandler(ads));
13
+ rewardsHelper.registerRewardHandler(
14
+ 'JOLI_COIN',
15
+ createJolicoinRewardHandler(ads.httpClient, {
16
+ onUnlockSuccess: (params: { quantity: number; balance: number }) => {
17
+ notifyCustomEvent('JOLIBOX_CUSTOM_REWARDS_EVENT', {
18
+ JOLI_COIN: params
19
+ });
20
+ }
21
+ })
22
+ );
10
23
 
11
24
  adEventEmitter.on('isAdShowing', (isAdShowing) => {
12
25
  notifyCustomEvent('JOLIBOX_ADS_EVENT', { isAdShowing });
@@ -21,7 +34,13 @@ commands.registerCommand('AdsSDK.adConfig', (params) => {
21
34
  });
22
35
 
23
36
  commands.registerCommand('AdsSDK.adBreak', (params) => {
24
- ads.adBreak(params);
37
+ if (params.type === 'reward') {
38
+ rewardsHelper.getRewardsTypes(ads.httpClient).then((rewardsTypes) => {
39
+ rewardsHelper.handleReward(rewardsTypes, params);
40
+ });
41
+ } else {
42
+ ads.adBreak(params);
43
+ }
25
44
  });
26
45
 
27
46
  commands.registerCommand('AdsSDK.adUnit', (params) => {
@@ -11,7 +11,7 @@ const API_GET_SYSTEM_SYNC = 'getSystemInfoSync';
11
11
  const getSystemInfo = () => {
12
12
  let config = {
13
13
  system: context.deviceInfo.system,
14
- platform: context.deviceInfo.platform,
14
+ platform: context.deviceInfo.platform.toLowerCase(),
15
15
  brand: context.deviceInfo.brand,
16
16
  pixelRatio: context.deviceInfo.pixelRatio,
17
17
  language: context.deviceInfo.lang,
@@ -1,5 +1,6 @@
1
1
  import { createCommands } from '@jolibox/common';
2
2
  import { createSyncAPI, registerCanIUse } from './base';
3
+ import { createToast } from '@jolibox/ui';
3
4
 
4
5
  const commands = createCommands();
5
6
 
@@ -8,6 +9,7 @@ import { track } from '../report';
8
9
 
9
10
  import { innerFetch as fetch } from '../network';
10
11
  import { invokeNative } from '@jolibox/native-bridge';
12
+ import { rewardsHelper, createAdsRewardHandler, createJolicoinRewardHandler } from '@/common/rewards';
11
13
 
12
14
  const checkNetworkStatus = () => {
13
15
  const { data } = invokeNative('getNetworkStatusSync');
@@ -34,6 +36,26 @@ const ads = new JoliboxAdsImpl(
34
36
  checkNetworkStatus
35
37
  );
36
38
 
39
+ rewardsHelper.registerRewardHandler('ADS', createAdsRewardHandler(ads));
40
+ rewardsHelper.registerRewardHandler(
41
+ 'JOLI_COIN',
42
+ createJolicoinRewardHandler(ads.httpClient, {
43
+ onUnlockSuccess: (params) => {
44
+ const { quantity, balance } = params;
45
+ const balanceStr = balance >= 1000 ? '999+' : balance;
46
+ createToast(`{slot-checkmark} −${quantity} {slot-coin} | Balance: ${balanceStr} {slot-coin}`, {
47
+ customStyle: {
48
+ mark: {
49
+ marginRight: '8px'
50
+ }
51
+ }
52
+ });
53
+ },
54
+ onUnlockFailed: () => {
55
+ console.log('onUnlockFailed');
56
+ }
57
+ })
58
+ );
37
59
  const adInit = createSyncAPI('adInit', {
38
60
  implement: (config?: IAdsInitParams) => {
39
61
  ads.init(config);
@@ -48,7 +70,13 @@ const adConfig = createSyncAPI('adConfig', {
48
70
 
49
71
  const adBreak = createSyncAPI('adBreak', {
50
72
  implement: (params: IAdBreakParams) => {
51
- ads.adBreak(params);
73
+ if (params.type === 'reward') {
74
+ rewardsHelper.getRewardsTypes(ads.httpClient).then((rewardsTypes) => {
75
+ rewardsHelper.handleReward(rewardsTypes, params);
76
+ });
77
+ } else {
78
+ ads.adBreak(params);
79
+ }
52
80
  }
53
81
  });
54
82
 
@@ -13,7 +13,7 @@ const getSystemInfoSync = createSyncAPI(API_GET_SYSTEM_SYNC, {
13
13
  const { data } = res;
14
14
  return {
15
15
  system: data.deviceInfo.system,
16
- platform: data.deviceInfo.platform,
16
+ platform: data.deviceInfo.platform.toLowerCase() as 'h5' | 'android' | 'ios',
17
17
  version: data.sdkInfo.jssdkVersion,
18
18
  pixelRatio: data.deviceInfo.pixelRatio,
19
19
  language: data.deviceInfo.lang,
@@ -82,7 +82,11 @@ export function createFetch(
82
82
 
83
83
  if (res.state === 'success') {
84
84
  const { requestTaskId, state, ...rest } = res;
85
- task.resolve(rest);
85
+ if (res.statusCode === 200) {
86
+ task.resolve(rest);
87
+ } else {
88
+ task.reject(rest);
89
+ }
86
90
  promiseMap.delete(requestTaskId);
87
91
  } else if (res.state === 'fail') {
88
92
  const { requestTaskId, state, ...rest } = res;
@@ -50,10 +50,11 @@ const openGameSchema = (game: IGame) => {
50
50
  // Parse the original URL to preserve its structure
51
51
 
52
52
  const url = new URL(data.schema);
53
+ const originalPath = url.pathname;
53
54
  const originalSearch = new URLSearchParams(url.search);
54
55
 
55
56
  // Set or replace gameId and joliSource parameters
56
- originalSearch.set('gameId', game.gameId);
57
+ originalSearch.set('gameId', context.mpId);
57
58
  originalSearch.set(
58
59
  'joliSource',
59
60
  context.encodeJoliSourceQuery({
@@ -64,7 +65,7 @@ const openGameSchema = (game: IGame) => {
64
65
 
65
66
  const host = `https://${game.gameId}.content.jolibox.com/`;
66
67
  // Construct the final schema URL
67
- const schema = `${host}index.html?${originalSearch.toString()}`;
68
+ const schema = `${host}${originalPath}?${originalSearch.toString()}`;
68
69
 
69
70
  invokeNative('openSchemaSync', {
70
71
  schema