@jolibox/implement 1.1.11 → 1.1.13-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.
Files changed (42) hide show
  1. package/.rush/temp/package-deps_build.json +24 -20
  2. package/.rush/temp/shrinkwrap-deps.json +1 -1
  3. package/dist/common/ads/anti-cheating.d.ts +16 -53
  4. package/dist/common/ads/anti-cheating.test.d.ts +1 -0
  5. package/dist/common/context/index.d.ts +5 -0
  6. package/dist/common/context/url-parse.d.ts +8 -1
  7. package/dist/index.js +3 -3
  8. package/dist/index.native.js +110 -4
  9. package/dist/native/api/index.d.ts +1 -0
  10. package/dist/native/api/navigate.d.ts +1 -0
  11. package/dist/native/bootstrap/bridge.d.ts +1 -1
  12. package/dist/native/js-bridge/const.d.ts +1 -0
  13. package/dist/native/js-bridge/publish.d.ts +1 -0
  14. package/dist/native/js-bridge/subscribe.d.ts +2 -0
  15. package/dist/native/js-bridge/types.d.ts +4 -0
  16. package/dist/native/js-core/jolibox-js-core.d.ts +8 -3
  17. package/dist/native/network/create-fetch.d.ts +1 -0
  18. package/dist/native/ui/retention.d.ts +1 -0
  19. package/implement.build.log +2 -2
  20. package/package.json +4 -3
  21. package/src/common/ads/anti-cheating.test.ts +79 -0
  22. package/src/common/ads/anti-cheating.ts +222 -156
  23. package/src/common/ads/index.ts +21 -7
  24. package/src/common/context/index.ts +22 -3
  25. package/src/common/context/url-parse.ts +24 -1
  26. package/src/native/api/index.ts +1 -0
  27. package/src/native/api/lifecycle.ts +16 -4
  28. package/src/native/api/navigate.ts +61 -0
  29. package/src/native/api/request.ts +19 -10
  30. package/src/native/bootstrap/bridge.ts +10 -1
  31. package/src/native/bootstrap/index.ts +100 -23
  32. package/src/native/js-bridge/const.ts +2 -0
  33. package/src/native/js-bridge/js-bridge.ts +7 -2
  34. package/src/native/js-bridge/publish.ts +44 -0
  35. package/src/native/js-bridge/subscribe.ts +25 -1
  36. package/src/native/js-bridge/types.ts +10 -0
  37. package/src/native/js-core/jolibox-js-core.ts +30 -26
  38. package/src/native/network/create-fetch.ts +1 -0
  39. package/src/native/network/utils.ts +13 -6
  40. package/src/native/types/global.d.ts +1 -0
  41. package/src/native/types/native-method-map.d.ts +29 -0
  42. package/src/native/ui/retention.ts +152 -0
@@ -4,5 +4,7 @@ export interface Subscribes {
4
4
  onNative: On;
5
5
  offNative: Off;
6
6
  subscribeHandler: AnyFunction;
7
+ subscribe: On;
8
+ unsubscribe: Off;
7
9
  }
8
10
  export declare function createSubscribe(jsCore: jsb.JSCore): Subscribes;
@@ -2,6 +2,7 @@ export type Listener = (...args: any[]) => any;
2
2
  export type On = <T extends string>(event: T, handler: Listener) => void;
3
3
  export type Off = <T extends string>(event: T, handler: Listener) => void;
4
4
  export type InvokeHandler = (callbackId: string | number, data: string | Record<string, unknown>) => void;
5
+ export type Publish = (event: string, data: Record<string, unknown>, webviewId?: number, force?: boolean) => void;
5
6
  export type SubscribeHandler = (event: string, data: string | Record<string, unknown>, webviewId?: number) => void;
6
7
  export type AnyFunction = (...args: any[]) => any;
7
8
  export interface JSBridge {
@@ -11,4 +12,7 @@ export interface JSBridge {
11
12
  applyNative: jsb.service.ApplyNative;
12
13
  onNative: jsb.service.OnNative;
13
14
  offNative: jsb.service.OffNative;
15
+ publish: Publish;
16
+ subscribe: On;
17
+ unsubscribe: Off;
14
18
  }
@@ -5,6 +5,11 @@ declare global {
5
5
  invoke: (invokeString: string) => void;
6
6
  onDocumentReady: (paramsstr: string) => void;
7
7
  doExit: (uuidString: string) => void;
8
+ publish: (params: {
9
+ event: string;
10
+ webviewIds: number[] | '*';
11
+ paramsString: string;
12
+ }) => void;
8
13
  };
9
14
  webkit?: {
10
15
  messageHandlers: {
@@ -18,7 +23,7 @@ declare global {
18
23
  publish: {
19
24
  postMessage: (params: {
20
25
  event: string;
21
- webviewIds: string;
26
+ webviewIds: number[] | '*';
22
27
  paramsString: string;
23
28
  }) => void;
24
29
  };
@@ -39,7 +44,7 @@ declare global {
39
44
  }
40
45
  export declare const RuntimeLoader: {
41
46
  trigger(): void;
42
- exit(): boolean;
47
+ exit(): Promise<boolean>;
43
48
  onReady(fn: () => void): void;
44
- doExit(fn: () => boolean): void;
49
+ doExit(fn: () => Promise<boolean>): void;
45
50
  };
@@ -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.11 clean
3
+ > @jolibox/implement@1.1.13-beta.1 clean
4
4
  > rimraf ./dist
5
5
 
6
6
 
7
- > @jolibox/implement@1.1.11 build:esm
7
+ > @jolibox/implement@1.1.13-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,14 +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.1.11",
4
+ "version": "1.1.13-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.1.11",
10
- "@jolibox/types": "1.1.11",
9
+ "@jolibox/common": "1.1.13-beta.1",
10
+ "@jolibox/types": "1.1.13-beta.1",
11
11
  "localforage": "1.10.0",
12
+ "@jolibox/ui": "1.0.0",
12
13
  "web-vitals": "4.2.4"
13
14
  },
14
15
  "devDependencies": {
@@ -0,0 +1,79 @@
1
+ import { AdsAntiCheating, IAdsAntiCheatingConfig } from './anti-cheating';
2
+
3
+ describe('createLeadingNotifiableDebounce', () => {
4
+ jest.setTimeout(3000); // increase timeout for tes
5
+
6
+ const track = jest.fn() as any;
7
+ const http = {
8
+ get: jest.fn(),
9
+ post: jest.fn()
10
+ };
11
+
12
+ const config: IAdsAntiCheatingConfig = {
13
+ initialThreshold: 50,
14
+ highFreqThreshold: 50,
15
+ maxAllowedAdsForTime: 2,
16
+ banForTimeThreshold: 500, // 500ms
17
+ bannedReleaseTime: 500, // 500ms
18
+ banForSessionThreshold: 3000, // 4s
19
+ maxAllowedAdsForSession: 7 // 7 ads
20
+ };
21
+
22
+ it('should ban initial call', async () => {
23
+ localStorage.clear();
24
+
25
+ const adsAntiCheating = new AdsAntiCheating(track, http, () => true, config);
26
+ await new Promise((resolve) => setTimeout(resolve, 10));
27
+ const a = adsAntiCheating.checkAdsDisplayPermission('reward');
28
+ expect(a.reason).toBe('BLOCK_INITIAL');
29
+ await new Promise((resolve) => setTimeout(resolve, 100));
30
+ const b = adsAntiCheating.checkAdsDisplayPermission('reward');
31
+ expect(b.reason).toBe('ALLOWED');
32
+ });
33
+
34
+ it('should ban continous calling', async () => {
35
+ localStorage.clear();
36
+
37
+ const adsAntiCheating = new AdsAntiCheating(track, http, () => true, config);
38
+ await new Promise((resolve) => setTimeout(resolve, 100));
39
+ const a = adsAntiCheating.checkAdsDisplayPermission('reward');
40
+ expect(a.reason).toBe('ALLOWED');
41
+
42
+ // wait 10 ms
43
+ await new Promise((resolve) => setTimeout(resolve, 10));
44
+ const b = adsAntiCheating.checkAdsDisplayPermission('reward');
45
+ expect(b.reason).toBe('HOLDING_HIGH_FREQ');
46
+ expect(b.info?.count).toBe(1);
47
+
48
+ // wait another 10 ms
49
+ await new Promise((resolve) => setTimeout(resolve, 10));
50
+ const d = adsAntiCheating.checkAdsDisplayPermission('reward');
51
+ expect(d.reason).toBe('HOLDING_HIGH_FREQ');
52
+ expect(d.info?.count).toBe(2);
53
+
54
+ // wait another 100ms to reset the high freq timer, but trigger banned for time
55
+ await new Promise((resolve) => setTimeout(resolve, 60)); // wait 100ms
56
+ const c = adsAntiCheating.checkAdsDisplayPermission('reward');
57
+ expect(c.reason).toBe('BANNED_FOR_TIME');
58
+
59
+ // wait another 100ms
60
+ await new Promise((resolve) => setTimeout(resolve, 100)); // wait 100ms
61
+ const e = adsAntiCheating.checkAdsDisplayPermission('reward');
62
+ expect(e.reason).toBe('WAITING_BANNED_RELEASE');
63
+ const isBannedForTime1 = window.localStorage.getItem('jolibox-sdk-ads-ban-for-time');
64
+ expect(isBannedForTime1).toBe('true');
65
+
66
+ // wait another 400ms to reset the banned for time timer and allow display
67
+ await new Promise((resolve) => setTimeout(resolve, 400)); // wait 400ms
68
+ const f = adsAntiCheating.checkAdsDisplayPermission('reward');
69
+ expect(f.reason).toBe('ALLOWED');
70
+
71
+ // 500ms
72
+ await new Promise((resolve) => setTimeout(resolve, 500)); // wait 500ms
73
+ const g = adsAntiCheating.checkAdsDisplayPermission('reward');
74
+ expect(g.reason).toBe('BANNED_FOR_SESSION');
75
+
76
+ const isBannedForTime = window.localStorage.getItem('jolibox-sdk-ads-ban-for-time');
77
+ expect(isBannedForTime).toBe('false'); // should be false after the timer resets
78
+ });
79
+ });
@@ -4,6 +4,7 @@ import type { Track } from '../report';
4
4
  export type AdsDisplayPermission =
5
5
  | 'BLOCK_INITIAL'
6
6
  | 'BANNED_FOR_SESSION'
7
+ | 'HOLDING_HIGH_FREQ'
7
8
  | 'NETWORK_NOT_OK'
8
9
  | 'WAITING_BANNED_RELEASE'
9
10
  | 'BANNED_FOR_TIME'
@@ -13,35 +14,21 @@ interface CallAdsHistory {
13
14
  type: 'reward' | 'interstitial';
14
15
  }
15
16
 
16
- const TIMESTAMP_STORAGE_KEY = 'jolibox-sdk-ads-callbreak-timestamps';
17
-
18
- interface IAdsAntiCheatingConfig {
17
+ export interface IAdsAntiCheatingConfig {
19
18
  maxAllowedAdsForTime?: number; // default 8
20
- bannedForTimeThreshold?: number; // default 60000 (1 minute)
21
- bannedReleaseTime?: number; // default 60000 (1 minute), must be >= checkingTimeThreashold
19
+ banForTimeThreshold?: number; // default 60000 (1 minute)
20
+ bannedReleaseTime?: number; // default 60000 (1 minute), must be >= checkingTimeThreshold
22
21
  initialThreshold?: number; // default 2000
23
22
  maxAllowedAdsForSession?: number; // default 200
24
- bannedForSessionThreshold?: number; // default 600000 (10 minutes)
23
+ banForSessionThreshold?: number; // default 600000 (10 minutes)
24
+ highFreqThreshold?: number; // default 2000 (2s)
25
25
  }
26
26
 
27
27
  export class AdsAntiCheating {
28
- private maxAllowedAdsForTime = 8;
29
- private bannedForTimeThreshold = 60000;
30
- private bannedReleaseTime = 60000;
31
- private isBanningForTime = false;
32
- private releaseBannedForTimeTimeout: number | null = null;
33
-
34
- private maxAllowedAdsForSession = 200;
35
- private bannedForSessionThreshold = 600000;
36
- private isBanningForSession = false;
37
-
38
- private initialThreshold = 2000;
39
-
40
- private _callAdsTimestampsForTime: CallAdsHistory[] = []; // max x timestamps for time
41
- private callAdsTimestampsForSession: CallAdsHistory[] = []; // max x timestamps for session
42
-
43
- private initTimestamp = 0;
44
- // private context: JoliboxSDKPipeExecutor;
28
+ private checkShouldBanInitial: ReturnType<typeof createInitialStrategy>;
29
+ private checkShouldBanHighFreq: ReturnType<typeof createHighFreqStrategy>;
30
+ private checkShouldBanForTime: ReturnType<typeof createBanForTimeStrategy>;
31
+ private checkShouldBanForSession: ReturnType<typeof createBanForSessionStrategy>;
45
32
 
46
33
  constructor(
47
34
  readonly track: Track,
@@ -49,217 +36,296 @@ export class AdsAntiCheating {
49
36
  readonly checkNetwork: () => boolean,
50
37
  config?: IAdsAntiCheatingConfig
51
38
  ) {
52
- this.maxAllowedAdsForTime = config?.maxAllowedAdsForTime ?? 8;
53
- this.bannedForTimeThreshold = config?.bannedForTimeThreshold ?? 60000;
54
- this.bannedReleaseTime = config?.bannedReleaseTime ?? 60000;
55
-
56
- this.maxAllowedAdsForSession = config?.maxAllowedAdsForSession ?? 200;
57
- this.bannedForSessionThreshold = config?.bannedForSessionThreshold ?? 600000;
58
-
59
- this.initialThreshold = config?.initialThreshold ?? 2000;
39
+ const maxAllowedAdsForTime = config?.maxAllowedAdsForTime ?? 8;
40
+ const banForTimeThreshold = config?.banForTimeThreshold ?? 60000;
41
+ const bannedReleaseTime = config?.bannedReleaseTime ?? 60000;
42
+
43
+ const maxAllowedAdsForSession = config?.maxAllowedAdsForSession ?? 200;
44
+ const banForSessionThreshold = config?.banForSessionThreshold ?? 600000;
45
+
46
+ const initialThreshold = config?.initialThreshold ?? 2000;
47
+ const highFreqThreshold = config?.highFreqThreshold ?? 2000;
48
+
49
+ this.checkShouldBanInitial = createInitialStrategy(initialThreshold);
50
+ this.checkShouldBanHighFreq = createHighFreqStrategy(highFreqThreshold);
51
+ this.checkShouldBanForTime = createBanForTimeStrategy(
52
+ maxAllowedAdsForTime,
53
+ banForTimeThreshold,
54
+ bannedReleaseTime
55
+ );
56
+ this.checkShouldBanForSession = createBanForSessionStrategy(
57
+ maxAllowedAdsForSession,
58
+ banForSessionThreshold
59
+ );
60
+ }
60
61
 
61
- if (this.maxAllowedAdsForTime <= 1) {
62
- throw new Error('maxAllowedAdsForTime must be greater than 1');
62
+ public checkAdsDisplayPermission: (type: 'reward' | 'interstitial') => {
63
+ reason: AdsDisplayPermission;
64
+ info?: { count: number };
65
+ } = (type) => {
66
+ if (!this.checkNetwork()) {
67
+ return { reason: 'NETWORK_NOT_OK' };
63
68
  }
64
69
 
65
- if (this.bannedForTimeThreshold < 0) {
66
- throw new Error('bannedForTimeThreshold must be greater than or equal to 0');
70
+ const shouldBanInitial = this.checkShouldBanInitial();
71
+ if (shouldBanInitial) {
72
+ return { reason: shouldBanInitial };
67
73
  }
68
74
 
69
- if (this.bannedReleaseTime < this.bannedForTimeThreshold) {
70
- throw new Error('bannedReleaseTime must be greater than or equal to bannedForTimeThreshold');
75
+ const shouldBanForSession = this.checkShouldBanForSession(type);
76
+ if (shouldBanForSession) {
77
+ return { reason: shouldBanForSession };
71
78
  }
72
79
 
73
- if (this.initialThreshold < 0) {
74
- throw new Error('initialThreshold must be greater than or equal to 0');
80
+ const shouldBanForHighFreq = this.checkShouldBanHighFreq(type);
81
+ if (shouldBanForHighFreq) {
82
+ return shouldBanForHighFreq;
75
83
  }
76
84
 
77
- this.initCallAdsTimestampsForTime();
78
- this.initTimestamp = Date.now();
79
- // this.context = context;
80
- }
81
-
82
- /**
83
- * Restore call ads timestamps for time from local storage
84
- */
85
- private initCallAdsTimestampsForTime = () => {
86
- try {
87
- const fromStorage = JSON.parse(window.localStorage.getItem(TIMESTAMP_STORAGE_KEY) ?? '[]');
88
- if (Array.isArray(fromStorage)) {
89
- this._callAdsTimestampsForTime = fromStorage;
90
- } else {
91
- this._callAdsTimestampsForTime = [];
92
- }
93
- } catch {
94
- this._callAdsTimestampsForTime = [];
85
+ const shouldBanForTime = this.checkShouldBanForTime(type);
86
+ if (shouldBanForTime) {
87
+ return { reason: shouldBanForTime };
95
88
  }
96
- };
97
89
 
98
- /**
99
- * Get call ads timestamps for time
100
- */
101
- private get callAdsTimestampsForTime() {
102
- return this._callAdsTimestampsForTime;
103
- }
90
+ return { reason: 'ALLOWED' };
91
+ };
104
92
 
105
93
  /**
106
- * Set call ads timestamps for time, this will also save to local storage
94
+ * Report an ad display permission issue.
95
+ * Report to both event tracking system and the business backend system.
96
+ * @param reason
107
97
  */
108
- private set callAdsTimestampsForTime(timestamps: CallAdsHistory[]) {
109
- try {
110
- window.localStorage.setItem(TIMESTAMP_STORAGE_KEY, JSON.stringify(timestamps));
111
- } catch {
112
- console.error('Failed to save timestamps');
113
- }
114
- this._callAdsTimestampsForTime = timestamps;
98
+ public report(reason: AdsDisplayPermission, count?: number) {
99
+ const info = count ? { reason, count } : { reason };
100
+ this.track('PreventAdsCheating', info);
101
+ this.httpClient.post('/api/base/app-event', {
102
+ data: {
103
+ eventType: 'PREVENT_ADS_CHEATING',
104
+ adsInfo: info
105
+ }
106
+ });
115
107
  }
108
+ }
116
109
 
117
- /**
118
- * Set a timeout to release the banned for time status
119
- */
120
- private setReleaseBannedForTime = () => {
121
- if (this.releaseBannedForTimeTimeout) {
122
- window.clearTimeout(this.releaseBannedForTimeTimeout);
123
- this.releaseBannedForTimeTimeout = null;
124
- }
125
- this.releaseBannedForTimeTimeout = window.setTimeout(() => {
126
- this.isBanningForTime = false;
127
- this.releaseBannedForTimeTimeout = null;
128
- }, this.bannedReleaseTime);
129
- };
130
-
131
- /**
132
- * Check if we should ban for initial call. By default, if user makes a call less than 2 seconds after the first call, block.
133
- * @returns
134
- */
135
- private checkShouldBannedInitial = () => {
136
- const now = Date.now();
137
- const diffFromInit = now - this.initTimestamp;
138
- // if initial call less than x seconds, block
139
- if (diffFromInit <= this.initialThreshold) {
140
- return 'BLOCK_INITIAL';
141
- }
142
- };
110
+ const createBanForSessionStrategy = (maxAllowedAdsForSession: number, banForSessionThreshold: number) => {
111
+ let isBannedForSession = false;
112
+ let callAdsTimestampsForSession: CallAdsHistory[] = [];
143
113
 
144
114
  /**
145
115
  * Check if we should ban this user for the whole session. By default, if user makes more than 200 calls in 10 minutes, ban for the session.
146
116
  * @param type
147
117
  * @returns
148
118
  */
149
- private checkShouldBannedForSession = (type: 'reward' | 'interstitial') => {
150
- if (this.isBanningForSession) {
119
+ const check = (type: 'reward' | 'interstitial') => {
120
+ if (isBannedForSession) {
151
121
  return 'BANNED_FOR_SESSION';
152
122
  }
153
123
 
154
124
  const now = Date.now();
155
125
 
156
126
  // keep track of call timestamps
157
- this.callAdsTimestampsForSession = this.callAdsTimestampsForSession.concat({
127
+ callAdsTimestampsForSession = callAdsTimestampsForSession.concat({
158
128
  timestamp: now,
159
129
  type
160
130
  });
161
131
 
162
132
  // if only 1 call, no need to check
163
- if (this.callAdsTimestampsForSession.length === 1) {
133
+ if (callAdsTimestampsForSession.length === 1) {
164
134
  return undefined;
165
135
  }
166
136
 
167
137
  // keep last x timestamps
168
- if (this.callAdsTimestampsForSession.length > this.maxAllowedAdsForSession) {
169
- this.callAdsTimestampsForSession = this.callAdsTimestampsForSession.slice(
170
- -this.maxAllowedAdsForSession
171
- );
138
+ if (callAdsTimestampsForSession.length > maxAllowedAdsForSession) {
139
+ callAdsTimestampsForSession = callAdsTimestampsForSession.slice(-maxAllowedAdsForSession);
172
140
  }
173
141
 
174
142
  // if less than x seconds, ban for session
175
- if (this.callAdsTimestampsForSession.length === this.maxAllowedAdsForSession) {
176
- const diffFromFirst = now - this.callAdsTimestampsForSession[0].timestamp;
177
- if (diffFromFirst <= this.bannedForSessionThreshold) {
178
- this.isBanningForSession = true;
143
+ if (callAdsTimestampsForSession.length === maxAllowedAdsForSession) {
144
+ const diffFromFirst = now - callAdsTimestampsForSession[0].timestamp;
145
+ if (diffFromFirst <= banForSessionThreshold) {
146
+ isBannedForSession = true;
179
147
  return 'BANNED_FOR_SESSION';
180
148
  }
181
149
  }
182
150
  };
183
151
 
152
+ return check;
153
+ };
154
+
155
+ const createBanForTimeStrategy = (
156
+ maxAllowedAdsForTime: number,
157
+ banForTimeThreshold: number,
158
+ bannedReleaseTime: number
159
+ ) => {
160
+ let releaseBannedForTimeTimeout: number | null = null;
161
+
162
+ const setReleaseBannedForTime = (timeout: number) => {
163
+ if (releaseBannedForTimeTimeout) {
164
+ window.clearTimeout(releaseBannedForTimeTimeout);
165
+ releaseBannedForTimeTimeout = null;
166
+ }
167
+ releaseBannedForTimeTimeout = window.setTimeout(() => {
168
+ isBannedForTime.value = false;
169
+ releaseBannedForTimeTimeout = null;
170
+ }, timeout);
171
+ };
172
+
173
+ const TIMESTAMP_STORAGE_KEY = 'jolibox-sdk-ads-callbreak-timestamps';
174
+ const BAN_FOR_TIME_KEY = 'jolibox-sdk-ads-ban-for-time';
175
+
176
+ const callAdsHistory = {
177
+ _records: [] as CallAdsHistory[],
178
+ init() {
179
+ try {
180
+ const fromStorage = JSON.parse(window.localStorage.getItem(TIMESTAMP_STORAGE_KEY) ?? '[]');
181
+ if (Array.isArray(fromStorage)) {
182
+ this._records = fromStorage;
183
+ } else {
184
+ this._records = [];
185
+ }
186
+ } catch {
187
+ this._records = [];
188
+ }
189
+ },
190
+ get values() {
191
+ return this._records;
192
+ },
193
+ set values(value: CallAdsHistory[]) {
194
+ try {
195
+ window.localStorage.setItem(TIMESTAMP_STORAGE_KEY, JSON.stringify(value));
196
+ } catch {
197
+ // console.error('Failed to save timestamps');
198
+ // do nothing
199
+ }
200
+ this._records = value;
201
+ }
202
+ };
203
+
204
+ const isBannedForTime = {
205
+ _record: false,
206
+ init() {
207
+ try {
208
+ const fromStorage = JSON.parse(window.localStorage.getItem(BAN_FOR_TIME_KEY) ?? 'false');
209
+ if (typeof fromStorage === 'boolean') {
210
+ this._record = fromStorage;
211
+ }
212
+ } catch {
213
+ this._record = false; // default value
214
+ }
215
+ if (this._record) {
216
+ const lastCall: CallAdsHistory | undefined = callAdsHistory.values[callAdsHistory.values.length - 1];
217
+ const timeDiff = lastCall ? Date.now() - lastCall.timestamp : bannedReleaseTime;
218
+ if (timeDiff <= bannedReleaseTime && timeDiff > 0) {
219
+ // within the banned release time window
220
+ setReleaseBannedForTime(timeDiff);
221
+ } else {
222
+ this._record = false;
223
+ }
224
+ }
225
+ },
226
+ get value() {
227
+ return this._record;
228
+ },
229
+ set value(value: boolean) {
230
+ try {
231
+ window.localStorage.setItem(BAN_FOR_TIME_KEY, JSON.stringify(value));
232
+ } catch {
233
+ // do nothing
234
+ }
235
+ this._record = value;
236
+ }
237
+ };
238
+
239
+ callAdsHistory.init();
240
+ isBannedForTime.init();
241
+
184
242
  /**
185
243
  * Check if we should ban this user for a period of time. By default, if user makes more than 8 calls in 1 minute, ban for 1 minute.
186
244
  * After 1 minute, user can make calls again.
187
245
  * @param type
188
246
  * @returns
189
247
  */
190
- private checkShouldBannedForTime = (type: 'reward' | 'interstitial') => {
191
- if (this.isBanningForTime) {
248
+ const check = (type: 'reward' | 'interstitial') => {
249
+ if (isBannedForTime.value) {
192
250
  return 'WAITING_BANNED_RELEASE';
193
251
  }
194
252
 
195
253
  const now = Date.now();
196
254
 
197
255
  // keep track of call timestamps
198
- this.callAdsTimestampsForTime = this.callAdsTimestampsForTime.concat({
256
+ callAdsHistory.values = callAdsHistory.values.concat({
199
257
  timestamp: now,
200
258
  type
201
259
  });
202
260
 
203
261
  // if only 1 call, no need to check
204
- if (this.callAdsTimestampsForTime.length === 1) {
262
+ if (callAdsHistory.values.length === 1) {
205
263
  return undefined;
206
264
  }
207
265
 
208
266
  // keep last x timestamps
209
- if (this.callAdsTimestampsForTime.length > this.maxAllowedAdsForTime) {
210
- this.callAdsTimestampsForTime = this.callAdsTimestampsForTime.slice(-this.maxAllowedAdsForTime);
267
+ if (callAdsHistory.values.length > maxAllowedAdsForTime) {
268
+ callAdsHistory.values = callAdsHistory.values.slice(-maxAllowedAdsForTime);
211
269
  }
212
270
 
213
271
  // if less than x seconds, ban for time
214
- if (this.callAdsTimestampsForTime.length === this.maxAllowedAdsForTime) {
215
- const diffFromFirst = now - this.callAdsTimestampsForTime[0].timestamp;
216
- if (diffFromFirst <= this.bannedForTimeThreshold) {
217
- this.isBanningForTime = true;
218
- this.setReleaseBannedForTime();
272
+ if (callAdsHistory.values.length === maxAllowedAdsForTime) {
273
+ const diffFromFirst = now - callAdsHistory.values[0].timestamp;
274
+ if (diffFromFirst <= banForTimeThreshold) {
275
+ isBannedForTime.value = true;
276
+ setReleaseBannedForTime(bannedReleaseTime);
219
277
  return 'BANNED_FOR_TIME';
220
278
  }
221
279
  }
222
280
  };
281
+ return check;
282
+ };
223
283
 
224
- public checkAdsDisplayPermission: (type: 'reward' | 'interstitial') => AdsDisplayPermission = (type) => {
225
- if (!this.checkNetwork()) {
226
- return 'NETWORK_NOT_OK';
227
- }
228
-
229
- const shouldBannedInitial = this.checkShouldBannedInitial();
230
- if (shouldBannedInitial) {
231
- return shouldBannedInitial;
232
- }
233
-
234
- const shouldBannedForSession = this.checkShouldBannedForSession(type);
235
- if (shouldBannedForSession) {
236
- return shouldBannedForSession;
237
- }
284
+ const createInitialStrategy = (initialThreshold: number) => {
285
+ const initTimestamp = Date.now();
238
286
 
239
- const shouldBannedForTime = this.checkShouldBannedForTime(type);
240
- if (shouldBannedForTime) {
241
- return shouldBannedForTime;
287
+ /**
288
+ * Check if we should ban for initial call. By default, if user makes a call less than 2 seconds after the first call, block.
289
+ * @returns
290
+ */
291
+ const check = () => {
292
+ const now = Date.now();
293
+ const diffFromInit = now - initTimestamp;
294
+ // if initial call less than x seconds, block
295
+ if (diffFromInit <= initialThreshold) {
296
+ return 'BLOCK_INITIAL';
242
297
  }
243
-
244
- return 'ALLOWED';
245
298
  };
299
+ return check;
300
+ };
301
+
302
+ const createHighFreqStrategy = (highFreqThreshold = 2000) => {
303
+ let highFreqCounter = 0;
304
+ let highFreqTimer: number | null = null;
246
305
 
247
306
  /**
248
- * Report an ad display permission issue.
249
- * Report to both event tracking system and the business backend system.
250
- * @param reason
307
+ * Check if we should ban for high frequency calling. If user makes a call more than 1 times in x seconds, block.
308
+ * This only works for reward ads.
309
+ *
310
+ * @param type
311
+ * @returns
251
312
  */
252
- public report(reason: AdsDisplayPermission) {
253
- this.track('PreventAdsCheating', {
254
- reason
255
- });
256
- this.httpClient.post('/api/base/app-event', {
257
- data: {
258
- eventType: 'PREVENT_ADS_CHEATING',
259
- adsInfo: {
260
- reason
261
- }
313
+ const check = (type: 'reward' | 'interstitial') => {
314
+ if (type === 'reward') {
315
+ if (highFreqTimer) {
316
+ highFreqCounter += 1;
317
+ return { reason: 'HOLDING_HIGH_FREQ' as const, info: { count: highFreqCounter } };
318
+ } else {
319
+ highFreqTimer = window.setTimeout(() => {
320
+ if (highFreqTimer) {
321
+ window.clearTimeout(highFreqTimer);
322
+ highFreqTimer = 0;
323
+ highFreqCounter = 0;
324
+ }
325
+ }, highFreqThreshold);
262
326
  }
263
- });
264
- }
265
- }
327
+ }
328
+ };
329
+
330
+ return check;
331
+ };