@jolibox/implement 1.1.10 → 1.1.11-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 (40) hide show
  1. package/.rush/temp/package-deps_build.json +22 -18
  2. package/.rush/temp/shrinkwrap-deps.json +1 -1
  3. package/dist/common/ads/anti-cheating.d.ts +24 -51
  4. package/dist/common/ads/anti-cheating.test.d.ts +1 -0
  5. package/dist/common/ads/index.d.ts +4 -9
  6. package/dist/common/context/index.d.ts +4 -0
  7. package/dist/common/context/url-parse.d.ts +8 -1
  8. package/dist/index.js +3 -3
  9. package/dist/index.native.js +110 -4
  10. package/dist/native/api/index.d.ts +1 -0
  11. package/dist/native/api/navigate.d.ts +1 -0
  12. package/dist/native/bootstrap/bridge.d.ts +1 -1
  13. package/dist/native/js-bridge/const.d.ts +1 -0
  14. package/dist/native/js-bridge/publish.d.ts +1 -0
  15. package/dist/native/js-bridge/subscribe.d.ts +2 -0
  16. package/dist/native/js-bridge/types.d.ts +4 -0
  17. package/dist/native/js-core/jolibox-js-core.d.ts +8 -3
  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 +228 -139
  23. package/src/common/ads/index.ts +33 -20
  24. package/src/common/context/index.ts +19 -3
  25. package/src/common/context/url-parse.ts +24 -1
  26. package/src/native/api/ads.ts +7 -0
  27. package/src/native/api/index.ts +1 -0
  28. package/src/native/api/lifecycle.ts +16 -4
  29. package/src/native/api/navigate.ts +61 -0
  30. package/src/native/bootstrap/bridge.ts +10 -1
  31. package/src/native/bootstrap/index.ts +32 -2
  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/types/global.d.ts +1 -0
  39. package/src/native/types/native-method-map.d.ts +29 -0
  40. package/src/native/ui/retention.ts +152 -0
@@ -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
  };
@@ -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.10 clean
3
+ > @jolibox/implement@1.1.11-beta.10 clean
4
4
  > rimraf ./dist
5
5
 
6
6
 
7
- > @jolibox/implement@1.1.10 build:esm
7
+ > @jolibox/implement@1.1.11-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,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.10",
4
+ "version": "1.1.11-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.10",
10
- "@jolibox/types": "1.1.10",
9
+ "@jolibox/common": "1.1.11-beta.10",
10
+ "@jolibox/types": "1.1.11-beta.10",
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
+ });
@@ -1,6 +1,10 @@
1
+ import type { IHttpClient } from '../http';
2
+ import type { Track } from '../report';
3
+
1
4
  export type AdsDisplayPermission =
2
5
  | 'BLOCK_INITIAL'
3
6
  | 'BANNED_FOR_SESSION'
7
+ | 'HOLDING_HIGH_FREQ'
4
8
  | 'NETWORK_NOT_OK'
5
9
  | 'WAITING_BANNED_RELEASE'
6
10
  | 'BANNED_FOR_TIME'
@@ -10,233 +14,318 @@ interface CallAdsHistory {
10
14
  type: 'reward' | 'interstitial';
11
15
  }
12
16
 
13
- const TIMESTAMP_STORAGE_KEY = 'jolibox-sdk-ads-callbreak-timestamps';
14
-
15
- interface IAdsAntiCheatingConfig {
17
+ export interface IAdsAntiCheatingConfig {
16
18
  maxAllowedAdsForTime?: number; // default 8
17
- bannedForTimeThreshold?: number; // default 60000 (1 minute)
18
- 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
19
21
  initialThreshold?: number; // default 2000
20
22
  maxAllowedAdsForSession?: number; // default 200
21
- bannedForSessionThreshold?: number; // default 600000 (10 minutes)
23
+ banForSessionThreshold?: number; // default 600000 (10 minutes)
24
+ highFreqThreshold?: number; // default 2000 (2s)
22
25
  }
23
26
 
24
27
  export class AdsAntiCheating {
25
- private maxAllowedAdsForTime = 8;
26
- private bannedForTimeThreshold = 60000;
27
- private bannedReleaseTime = 60000;
28
- private isBanningForTime = false;
29
- private releaseBannedForTimeTimeout: number | null = null;
30
-
31
- private maxAllowedAdsForSession = 200;
32
- private bannedForSessionThreshold = 600000;
33
- private isBanningForSession = false;
34
-
35
- private initialThreshold = 2000;
36
-
37
- private _callAdsTimestampsForTime: CallAdsHistory[] = []; // max x timestamps for time
38
- private callAdsTimestampsForSession: CallAdsHistory[] = []; // max x timestamps for session
39
-
40
- private initTimestamp = 0;
41
- // 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>;
42
32
 
43
33
  constructor(
44
- // context: JoliboxSDKPipeExecutor,
34
+ readonly track: Track,
35
+ readonly httpClient: IHttpClient,
45
36
  readonly checkNetwork: () => boolean,
46
37
  config?: IAdsAntiCheatingConfig
47
38
  ) {
48
- this.maxAllowedAdsForTime = config?.maxAllowedAdsForTime ?? 8;
49
- this.bannedForTimeThreshold = config?.bannedForTimeThreshold ?? 60000;
50
- this.bannedReleaseTime = config?.bannedReleaseTime ?? 60000;
51
-
52
- this.maxAllowedAdsForSession = config?.maxAllowedAdsForSession ?? 200;
53
- this.bannedForSessionThreshold = config?.bannedForSessionThreshold ?? 600000;
54
-
55
- 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
+ }
56
61
 
57
- if (this.maxAllowedAdsForTime <= 1) {
58
- 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' };
59
68
  }
60
69
 
61
- if (this.bannedForTimeThreshold < 0) {
62
- throw new Error('bannedForTimeThreshold must be greater than or equal to 0');
70
+ const shouldBanInitial = this.checkShouldBanInitial();
71
+ if (shouldBanInitial) {
72
+ return { reason: shouldBanInitial };
63
73
  }
64
74
 
65
- if (this.bannedReleaseTime < this.bannedForTimeThreshold) {
66
- 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 };
67
78
  }
68
79
 
69
- if (this.initialThreshold < 0) {
70
- throw new Error('initialThreshold must be greater than or equal to 0');
80
+ const shouldBanForHighFreq = this.checkShouldBanHighFreq(type);
81
+ if (shouldBanForHighFreq) {
82
+ return shouldBanForHighFreq;
71
83
  }
72
84
 
73
- this.initCallAdsTimestampsForTime();
74
- this.initTimestamp = Date.now();
75
- // this.context = context;
76
- }
77
-
78
- /**
79
- * Restore call ads timestamps for time from local storage
80
- */
81
- private initCallAdsTimestampsForTime = () => {
82
- try {
83
- const fromStorage = JSON.parse(window.localStorage.getItem(TIMESTAMP_STORAGE_KEY) ?? '[]');
84
- if (Array.isArray(fromStorage)) {
85
- this._callAdsTimestampsForTime = fromStorage;
86
- } else {
87
- this._callAdsTimestampsForTime = [];
88
- }
89
- } catch {
90
- this._callAdsTimestampsForTime = [];
85
+ const shouldBanForTime = this.checkShouldBanForTime(type);
86
+ if (shouldBanForTime) {
87
+ return { reason: shouldBanForTime };
91
88
  }
92
- };
93
89
 
94
- /**
95
- * Get call ads timestamps for time
96
- */
97
- private get callAdsTimestampsForTime() {
98
- return this._callAdsTimestampsForTime;
99
- }
90
+ return { reason: 'ALLOWED' };
91
+ };
100
92
 
101
93
  /**
102
- * 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
103
97
  */
104
- private set callAdsTimestampsForTime(timestamps: CallAdsHistory[]) {
105
- try {
106
- window.localStorage.setItem(TIMESTAMP_STORAGE_KEY, JSON.stringify(timestamps));
107
- } catch {
108
- console.error('Failed to save timestamps');
109
- }
110
- 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
+ });
111
107
  }
108
+ }
112
109
 
113
- /**
114
- * Set a timeout to release the banned for time status
115
- */
116
- private setReleaseBannedForTime = () => {
117
- if (this.releaseBannedForTimeTimeout) {
118
- window.clearTimeout(this.releaseBannedForTimeTimeout);
119
- this.releaseBannedForTimeTimeout = null;
120
- }
121
- this.releaseBannedForTimeTimeout = window.setTimeout(() => {
122
- this.isBanningForTime = false;
123
- this.releaseBannedForTimeTimeout = null;
124
- }, this.bannedReleaseTime);
125
- };
126
-
127
- /**
128
- * Check if we should ban for initial call. By default, if user makes a call less than 2 seconds after the first call, block.
129
- * @returns
130
- */
131
- private checkShouldBannedInitial = () => {
132
- const now = Date.now();
133
- const diffFromInit = now - this.initTimestamp;
134
- // if initial call less than x seconds, block
135
- if (diffFromInit <= this.initialThreshold) {
136
- return 'BLOCK_INITIAL';
137
- }
138
- };
110
+ const createBanForSessionStrategy = (maxAllowedAdsForSession: number, banForSessionThreshold: number) => {
111
+ let isBannedForSession = false;
112
+ let callAdsTimestampsForSession: CallAdsHistory[] = [];
139
113
 
140
114
  /**
141
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.
142
116
  * @param type
143
117
  * @returns
144
118
  */
145
- private checkShouldBannedForSession = (type: 'reward' | 'interstitial') => {
146
- if (this.isBanningForSession) {
119
+ const check = (type: 'reward' | 'interstitial') => {
120
+ if (isBannedForSession) {
147
121
  return 'BANNED_FOR_SESSION';
148
122
  }
149
123
 
150
124
  const now = Date.now();
151
125
 
152
126
  // keep track of call timestamps
153
- this.callAdsTimestampsForSession = this.callAdsTimestampsForSession.concat({
127
+ callAdsTimestampsForSession = callAdsTimestampsForSession.concat({
154
128
  timestamp: now,
155
129
  type
156
130
  });
157
131
 
158
132
  // if only 1 call, no need to check
159
- if (this.callAdsTimestampsForSession.length === 1) {
133
+ if (callAdsTimestampsForSession.length === 1) {
160
134
  return undefined;
161
135
  }
162
136
 
163
137
  // keep last x timestamps
164
- if (this.callAdsTimestampsForSession.length > this.maxAllowedAdsForSession) {
165
- this.callAdsTimestampsForSession = this.callAdsTimestampsForSession.slice(
166
- -this.maxAllowedAdsForSession
167
- );
138
+ if (callAdsTimestampsForSession.length > maxAllowedAdsForSession) {
139
+ callAdsTimestampsForSession = callAdsTimestampsForSession.slice(-maxAllowedAdsForSession);
168
140
  }
169
141
 
170
142
  // if less than x seconds, ban for session
171
- if (this.callAdsTimestampsForSession.length === this.maxAllowedAdsForSession) {
172
- const diffFromFirst = now - this.callAdsTimestampsForSession[0].timestamp;
173
- if (diffFromFirst <= this.bannedForSessionThreshold) {
174
- this.isBanningForSession = true;
143
+ if (callAdsTimestampsForSession.length === maxAllowedAdsForSession) {
144
+ const diffFromFirst = now - callAdsTimestampsForSession[0].timestamp;
145
+ if (diffFromFirst <= banForSessionThreshold) {
146
+ isBannedForSession = true;
175
147
  return 'BANNED_FOR_SESSION';
176
148
  }
177
149
  }
178
150
  };
179
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
+
180
242
  /**
181
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.
182
244
  * After 1 minute, user can make calls again.
183
245
  * @param type
184
246
  * @returns
185
247
  */
186
- private checkShouldBannedForTime = (type: 'reward' | 'interstitial') => {
187
- if (this.isBanningForTime) {
248
+ const check = (type: 'reward' | 'interstitial') => {
249
+ if (isBannedForTime.value) {
188
250
  return 'WAITING_BANNED_RELEASE';
189
251
  }
190
252
 
191
253
  const now = Date.now();
192
254
 
193
255
  // keep track of call timestamps
194
- this.callAdsTimestampsForTime = this.callAdsTimestampsForTime.concat({
256
+ callAdsHistory.values = callAdsHistory.values.concat({
195
257
  timestamp: now,
196
258
  type
197
259
  });
198
260
 
199
261
  // if only 1 call, no need to check
200
- if (this.callAdsTimestampsForTime.length === 1) {
262
+ if (callAdsHistory.values.length === 1) {
201
263
  return undefined;
202
264
  }
203
265
 
204
266
  // keep last x timestamps
205
- if (this.callAdsTimestampsForTime.length > this.maxAllowedAdsForTime) {
206
- this.callAdsTimestampsForTime = this.callAdsTimestampsForTime.slice(-this.maxAllowedAdsForTime);
267
+ if (callAdsHistory.values.length > maxAllowedAdsForTime) {
268
+ callAdsHistory.values = callAdsHistory.values.slice(-maxAllowedAdsForTime);
207
269
  }
208
270
 
209
271
  // if less than x seconds, ban for time
210
- if (this.callAdsTimestampsForTime.length === this.maxAllowedAdsForTime) {
211
- const diffFromFirst = now - this.callAdsTimestampsForTime[0].timestamp;
212
- if (diffFromFirst <= this.bannedForTimeThreshold) {
213
- this.isBanningForTime = true;
214
- 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);
215
277
  return 'BANNED_FOR_TIME';
216
278
  }
217
279
  }
218
280
  };
281
+ return check;
282
+ };
219
283
 
220
- public checkAdsDisplayPermission: (type: 'reward' | 'interstitial') => AdsDisplayPermission = (type) => {
221
- if (!this.checkNetwork()) {
222
- return 'NETWORK_NOT_OK';
223
- }
284
+ const createInitialStrategy = (initialThreshold: number) => {
285
+ const initTimestamp = Date.now();
224
286
 
225
- const shouldBannedInitial = this.checkShouldBannedInitial();
226
- if (shouldBannedInitial) {
227
- return shouldBannedInitial;
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';
228
297
  }
298
+ };
299
+ return check;
300
+ };
229
301
 
230
- const shouldBannedForSession = this.checkShouldBannedForSession(type);
231
- if (shouldBannedForSession) {
232
- return shouldBannedForSession;
233
- }
302
+ const createHighFreqStrategy = (highFreqThreshold = 2000) => {
303
+ let highFreqCounter = 0;
304
+ let highFreqTimer: number | null = null;
234
305
 
235
- const shouldBannedForTime = this.checkShouldBannedForTime(type);
236
- if (shouldBannedForTime) {
237
- return shouldBannedForTime;
306
+ /**
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
312
+ */
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);
326
+ }
238
327
  }
239
-
240
- return 'ALLOWED';
241
328
  };
242
- }
329
+
330
+ return check;
331
+ };