@jolibox/implement 1.1.12 → 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.
- package/.rush/temp/package-deps_build.json +21 -18
- package/.rush/temp/shrinkwrap-deps.json +1 -1
- package/dist/common/context/index.d.ts +5 -0
- package/dist/common/context/url-parse.d.ts +8 -1
- package/dist/index.js +3 -3
- package/dist/index.native.js +110 -4
- package/dist/native/api/index.d.ts +1 -0
- package/dist/native/api/navigate.d.ts +1 -0
- package/dist/native/bootstrap/bridge.d.ts +1 -1
- package/dist/native/js-bridge/const.d.ts +1 -0
- package/dist/native/js-bridge/publish.d.ts +1 -0
- package/dist/native/js-bridge/subscribe.d.ts +2 -0
- package/dist/native/js-bridge/types.d.ts +4 -0
- package/dist/native/js-core/jolibox-js-core.d.ts +8 -3
- package/dist/native/network/create-fetch.d.ts +1 -0
- package/dist/native/ui/retention.d.ts +1 -0
- package/implement.build.log +2 -2
- package/package.json +4 -3
- package/src/common/context/index.ts +22 -3
- package/src/common/context/url-parse.ts +24 -1
- package/src/native/api/index.ts +1 -0
- package/src/native/api/lifecycle.ts +16 -4
- package/src/native/api/navigate.ts +61 -0
- package/src/native/api/request.ts +19 -10
- package/src/native/bootstrap/bridge.ts +10 -1
- package/src/native/bootstrap/index.ts +100 -23
- package/src/native/js-bridge/const.ts +2 -0
- package/src/native/js-bridge/js-bridge.ts +7 -2
- package/src/native/js-bridge/publish.ts +44 -0
- package/src/native/js-bridge/subscribe.ts +25 -1
- package/src/native/js-bridge/types.ts +10 -0
- package/src/native/js-core/jolibox-js-core.ts +30 -26
- package/src/native/network/create-fetch.ts +1 -0
- package/src/native/network/utils.ts +13 -6
- package/src/native/types/global.d.ts +1 -0
- package/src/native/types/native-method-map.d.ts +29 -0
- package/src/native/ui/retention.ts +152 -0
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import { EventEmitter } from '@jolibox/common';
|
|
1
|
+
import { EventEmitter, logger } from '@jolibox/common';
|
|
2
2
|
import { DataObj, unpack } from './utils';
|
|
3
3
|
import { Off, On } from './types';
|
|
4
4
|
import { AnyFunction } from '@jolibox/types';
|
|
5
5
|
import { MetricsMonitor } from './report';
|
|
6
|
+
import { BATCH_EVENT, CUSTOM_EVENT_PREFIX } from './const';
|
|
6
7
|
|
|
7
8
|
export interface Subscribes {
|
|
8
9
|
onNative: On;
|
|
9
10
|
offNative: Off;
|
|
10
11
|
subscribeHandler: AnyFunction;
|
|
12
|
+
subscribe: On;
|
|
13
|
+
unsubscribe: Off;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export function createSubscribe(jsCore: jsb.JSCore): Subscribes {
|
|
14
17
|
const nativeEmitter = new EventEmitter();
|
|
18
|
+
const customEmitter = new EventEmitter();
|
|
15
19
|
const publishMonitor = new MetricsMonitor({
|
|
16
20
|
eventName: 'jolibox_publish',
|
|
17
21
|
tagNameOrder: ['type', 'event'],
|
|
@@ -28,6 +32,8 @@ export function createSubscribe(jsCore: jsb.JSCore): Subscribes {
|
|
|
28
32
|
return {
|
|
29
33
|
onNative: nativeEmitter.on.bind(nativeEmitter),
|
|
30
34
|
offNative: nativeEmitter.off.bind(nativeEmitter),
|
|
35
|
+
subscribe: customEmitter.on.bind(customEmitter),
|
|
36
|
+
unsubscribe: customEmitter.off.bind(customEmitter),
|
|
31
37
|
subscribeHandler(event, data, webviewId) {
|
|
32
38
|
// ios: jsc 端基于系统方法,前端接受到的 data 总是 string 类型, webview 基于 evaluateJavascript,前端接受到的 data 总是 object 类型
|
|
33
39
|
// android: 传入为 string 则接收到 string,传入为可序列化成功的 object,则接收到 object
|
|
@@ -44,6 +50,24 @@ export function createSubscribe(jsCore: jsb.JSCore): Subscribes {
|
|
|
44
50
|
} else {
|
|
45
51
|
originalParams = unpackedData;
|
|
46
52
|
}
|
|
53
|
+
|
|
54
|
+
if (event === BATCH_EVENT) {
|
|
55
|
+
const list = originalParams as [event: string, data: unknown][];
|
|
56
|
+
list.forEach((item) => {
|
|
57
|
+
const [_event, _data] = item;
|
|
58
|
+
try {
|
|
59
|
+
customEmitter.emit(_event.slice(CUSTOM_EVENT_PREFIX.length), _data, webviewId);
|
|
60
|
+
} catch {
|
|
61
|
+
// 忽略
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (event.startsWith(CUSTOM_EVENT_PREFIX)) {
|
|
68
|
+
customEmitter.emit(event.slice(CUSTOM_EVENT_PREFIX.length), originalParams, webviewId);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
47
71
|
nativeEmitter.emit(event, originalParams, webviewId);
|
|
48
72
|
}
|
|
49
73
|
};
|
|
@@ -7,6 +7,13 @@ export type Off = <T extends string>(event: T, handler: Listener) => void;
|
|
|
7
7
|
|
|
8
8
|
export type InvokeHandler = (callbackId: string | number, data: string | Record<string, unknown>) => void;
|
|
9
9
|
|
|
10
|
+
export type Publish = (
|
|
11
|
+
event: string,
|
|
12
|
+
data: Record<string, unknown>,
|
|
13
|
+
webviewId?: number,
|
|
14
|
+
force?: boolean
|
|
15
|
+
) => void;
|
|
16
|
+
|
|
10
17
|
export type SubscribeHandler = (
|
|
11
18
|
event: string,
|
|
12
19
|
data: string | Record<string, unknown>,
|
|
@@ -23,4 +30,7 @@ export interface JSBridge {
|
|
|
23
30
|
applyNative: jsb.service.ApplyNative;
|
|
24
31
|
onNative: jsb.service.OnNative;
|
|
25
32
|
offNative: jsb.service.OffNative;
|
|
33
|
+
publish: Publish;
|
|
34
|
+
subscribe: On;
|
|
35
|
+
unsubscribe: Off;
|
|
26
36
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { InternalInvokeNativeError } from '@jolibox/common';
|
|
2
|
-
|
|
2
|
+
import { normalizeParams } from './utils';
|
|
3
3
|
import { Env } from '@jolibox/types';
|
|
4
4
|
import { reportError } from '@/common/report/errors/report';
|
|
5
5
|
|
|
@@ -23,6 +23,7 @@ declare global {
|
|
|
23
23
|
invoke: (invokeString: string) => void;
|
|
24
24
|
onDocumentReady: (paramsstr: string) => void;
|
|
25
25
|
doExit: (uuidString: string) => void;
|
|
26
|
+
publish: (params: { event: string; webviewIds: number[] | '*'; paramsString: string }) => void;
|
|
26
27
|
};
|
|
27
28
|
webkit?: {
|
|
28
29
|
messageHandlers: {
|
|
@@ -30,7 +31,7 @@ declare global {
|
|
|
30
31
|
postMessage: (params: { event: string; callbackId: number; paramsString: string }) => void;
|
|
31
32
|
};
|
|
32
33
|
publish: {
|
|
33
|
-
postMessage: (params: { event: string; webviewIds:
|
|
34
|
+
postMessage: (params: { event: string; webviewIds: number[] | '*'; paramsString: string }) => void;
|
|
34
35
|
};
|
|
35
36
|
onDocumentReady: {
|
|
36
37
|
postMessage: (params: { path: string }) => void;
|
|
@@ -47,14 +48,14 @@ export const RuntimeLoader = {
|
|
|
47
48
|
trigger() {
|
|
48
49
|
// noop
|
|
49
50
|
},
|
|
50
|
-
exit(): boolean {
|
|
51
|
+
exit(): Promise<boolean> {
|
|
51
52
|
//noop
|
|
52
|
-
return false; // 默认不打断退出
|
|
53
|
+
return Promise.resolve(false); // 默认不打断退出
|
|
53
54
|
},
|
|
54
55
|
onReady(fn: () => void) {
|
|
55
56
|
RuntimeLoader.trigger = fn;
|
|
56
57
|
},
|
|
57
|
-
doExit(fn: () => boolean) {
|
|
58
|
+
doExit(fn: () => Promise<boolean>) {
|
|
58
59
|
RuntimeLoader.exit = fn;
|
|
59
60
|
}
|
|
60
61
|
};
|
|
@@ -81,9 +82,9 @@ if (window.webkit) {
|
|
|
81
82
|
RuntimeLoader.trigger();
|
|
82
83
|
_joliboxJSCore.onDocumentReady.postMessage({ path });
|
|
83
84
|
};
|
|
84
|
-
const doExit = (uuid: string) => {
|
|
85
|
-
const shouldInterrupt = RuntimeLoader.exit();
|
|
86
|
-
_joliboxJSCore.doExit.postMessage({ uuid, shouldInterrupt });
|
|
85
|
+
const doExit = async (uuid: string) => {
|
|
86
|
+
const shouldInterrupt = await RuntimeLoader.exit();
|
|
87
|
+
_joliboxJSCore.doExit.postMessage({ uuid, shouldInterrupt: shouldInterrupt });
|
|
87
88
|
};
|
|
88
89
|
|
|
89
90
|
joliboxJSCore = {
|
|
@@ -96,14 +97,17 @@ if (window.webkit) {
|
|
|
96
97
|
callbackId
|
|
97
98
|
});
|
|
98
99
|
},
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
publish(event, params, webviewIds) {
|
|
101
|
+
const data = normalizeParams(params as Record<string, unknown>, 'joliboxJSCore');
|
|
102
|
+
window.prompt(
|
|
103
|
+
'publish',
|
|
104
|
+
JSON.stringify({
|
|
105
|
+
event,
|
|
106
|
+
paramsString: JSON.stringify(data),
|
|
107
|
+
webviewIds
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
},
|
|
107
111
|
call(method: string, params: Record<string, unknown>, callbackId) {
|
|
108
112
|
const res = window.prompt(
|
|
109
113
|
'invoke',
|
|
@@ -137,8 +141,8 @@ if (window.JoliAndroidSDKBridge) {
|
|
|
137
141
|
_joliboxJSCore.onDocumentReady(JSON.stringify({ path }));
|
|
138
142
|
};
|
|
139
143
|
|
|
140
|
-
const doExit = (uuid: string) => {
|
|
141
|
-
const shouldInterrupt = RuntimeLoader.exit();
|
|
144
|
+
const doExit = async (uuid: string) => {
|
|
145
|
+
const shouldInterrupt = await RuntimeLoader.exit();
|
|
142
146
|
_joliboxJSCore.doExit(JSON.stringify({ uuid, shouldInterrupt }));
|
|
143
147
|
};
|
|
144
148
|
|
|
@@ -154,14 +158,14 @@ if (window.JoliAndroidSDKBridge) {
|
|
|
154
158
|
);
|
|
155
159
|
},
|
|
156
160
|
doExit,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
publish(event, params, webviewIds) {
|
|
162
|
+
const data = normalizeParams(params as Record<string, unknown>, 'joliboxJSCore');
|
|
163
|
+
_joliboxJSCore.publish({
|
|
164
|
+
event,
|
|
165
|
+
paramsString: JSON.stringify(data),
|
|
166
|
+
webviewIds
|
|
167
|
+
});
|
|
168
|
+
},
|
|
165
169
|
call(method: string, params: Record<string, unknown>, callbackId) {
|
|
166
170
|
const res = window.prompt(
|
|
167
171
|
'invoke',
|
|
@@ -79,12 +79,19 @@ export function dirtyURL(url: string, method: HTTPMethod, data?: unknown) {
|
|
|
79
79
|
|
|
80
80
|
if (!isObject(data)) return url;
|
|
81
81
|
|
|
82
|
-
const [
|
|
82
|
+
const [baseUrl, search = ''] = url.split('?');
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
...data
|
|
87
|
-
});
|
|
84
|
+
// Parse existing query parameters
|
|
85
|
+
const dataParams = new URLSearchParams(search);
|
|
88
86
|
|
|
89
|
-
|
|
87
|
+
// Add new parameters from data object
|
|
88
|
+
for (const [key, value] of Object.entries(data)) {
|
|
89
|
+
if (value !== undefined && value !== null) {
|
|
90
|
+
dataParams.append(key, String(value));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const queryString = dataParams.toString();
|
|
95
|
+
|
|
96
|
+
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
|
90
97
|
}
|
|
@@ -15,6 +15,7 @@ declare global {
|
|
|
15
15
|
onDocumentReady: (path: string) => void;
|
|
16
16
|
doExit: (uuid: string) => void;
|
|
17
17
|
bind?: (id: number, handler: (...args: unknown[]) => unknown) => void;
|
|
18
|
+
publish: (event: string, data: unknown, webviewIds: number[] | '*') => void;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
type ApplyNativeReturn<T> = T extends Promise<infer U>
|
|
@@ -255,6 +255,30 @@ declare global {
|
|
|
255
255
|
/** 错误码 */
|
|
256
256
|
errNo?: number;
|
|
257
257
|
};
|
|
258
|
+
|
|
259
|
+
openSchemaSync: (params: { schema: string }) => {
|
|
260
|
+
/** 错误消息 */
|
|
261
|
+
errMsg: string;
|
|
262
|
+
/** 错误码 */
|
|
263
|
+
errNo?: number;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
openPageSync: (params: { url: string }) => {
|
|
267
|
+
/** 错误消息 */
|
|
268
|
+
errMsg: string;
|
|
269
|
+
/** 错误码 */
|
|
270
|
+
errNo?: number;
|
|
271
|
+
data?: {
|
|
272
|
+
webviewId: number;
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
closePageSync: (params: { webviewId: number }) => {
|
|
277
|
+
/** 错误消息 */
|
|
278
|
+
errMsg: string;
|
|
279
|
+
/** 错误码 */
|
|
280
|
+
errNo?: number;
|
|
281
|
+
};
|
|
258
282
|
}
|
|
259
283
|
|
|
260
284
|
interface NativeEventMap {
|
|
@@ -293,6 +317,11 @@ declare global {
|
|
|
293
317
|
token?: string;
|
|
294
318
|
}
|
|
295
319
|
];
|
|
320
|
+
onInfoTapped: [
|
|
321
|
+
{
|
|
322
|
+
uuid: string;
|
|
323
|
+
}
|
|
324
|
+
];
|
|
296
325
|
}
|
|
297
326
|
}
|
|
298
327
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { context } from '@/common/context';
|
|
2
|
+
import { invokeNative, subscribe } from '../bootstrap/bridge';
|
|
3
|
+
import { Deferred } from '@jolibox/common';
|
|
4
|
+
import { createRecommendModal, IGame, IRecommendationButton, RecommendModalOnCloseParams } from '@jolibox/ui';
|
|
5
|
+
import { innerFetch as fetch } from '../network';
|
|
6
|
+
|
|
7
|
+
let exitRecommendationsCache: {
|
|
8
|
+
code: string;
|
|
9
|
+
data: {
|
|
10
|
+
gameListInfo: {
|
|
11
|
+
games: IGame[];
|
|
12
|
+
};
|
|
13
|
+
title: string;
|
|
14
|
+
buttons: IRecommendationButton[];
|
|
15
|
+
};
|
|
16
|
+
} | null = null;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fetches exit recommendations data from the API
|
|
20
|
+
*/
|
|
21
|
+
async function fetchExitRecommendations() {
|
|
22
|
+
if (exitRecommendationsCache) {
|
|
23
|
+
return exitRecommendationsCache;
|
|
24
|
+
}
|
|
25
|
+
const host = context.testMode ? `https://stg-game.jolibox.com` : `https://game.jolibox.com`;
|
|
26
|
+
const url = `${host}/api/user-retention/exit-recommendations?objectId=${context.mpId}&from=GAME_DETAIL`;
|
|
27
|
+
const {
|
|
28
|
+
response: { data }
|
|
29
|
+
} = await fetch<{
|
|
30
|
+
code: string;
|
|
31
|
+
data: {
|
|
32
|
+
gameListInfo: {
|
|
33
|
+
games: IGame[];
|
|
34
|
+
};
|
|
35
|
+
title: string;
|
|
36
|
+
buttons: IRecommendationButton[];
|
|
37
|
+
};
|
|
38
|
+
}>(url);
|
|
39
|
+
|
|
40
|
+
exitRecommendationsCache = data;
|
|
41
|
+
return data;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//prefetch
|
|
45
|
+
fetchExitRecommendations();
|
|
46
|
+
|
|
47
|
+
const openGameSchema = (game: IGame) => {
|
|
48
|
+
const { data } = invokeNative('envSync');
|
|
49
|
+
|
|
50
|
+
// Parse the original URL to preserve its structure
|
|
51
|
+
|
|
52
|
+
const url = new URL(data.schema);
|
|
53
|
+
const originalSearch = new URLSearchParams(url.search);
|
|
54
|
+
|
|
55
|
+
// Set or replace gameId and joliSource parameters
|
|
56
|
+
originalSearch.set('gameId', game.gameId);
|
|
57
|
+
originalSearch.set(
|
|
58
|
+
'joliSource',
|
|
59
|
+
context.encodeJoliSourceQuery({
|
|
60
|
+
__mpType: 'game',
|
|
61
|
+
__orientation: game.orientation ?? 'VERTICAL'
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const host = `https://${game.gameId}.content.jolibox.com/`;
|
|
66
|
+
// Construct the final schema URL
|
|
67
|
+
const schema = `${host}index.html?${originalSearch.toString()}`;
|
|
68
|
+
|
|
69
|
+
invokeNative('openSchemaSync', {
|
|
70
|
+
schema
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export async function openRetentionSchema() {
|
|
75
|
+
// const { data } = invokeNative('envSync');
|
|
76
|
+
// const { orientation, webviewId } = data;
|
|
77
|
+
// let joliPayload: Record<string, unknown> = {
|
|
78
|
+
// __mpType: 'miniApp',
|
|
79
|
+
// __transparent: true,
|
|
80
|
+
// // set entryPath
|
|
81
|
+
// __orientation: orientation ?? 'VERTICAL', // 默认竖屏
|
|
82
|
+
// __showStatusBar: false,
|
|
83
|
+
// __shouldInterupt: false,
|
|
84
|
+
// __showCapsuleButton: false
|
|
85
|
+
// };
|
|
86
|
+
// if (webviewId) {
|
|
87
|
+
// joliPayload = {
|
|
88
|
+
// ...joliPayload,
|
|
89
|
+
// __from: webviewId
|
|
90
|
+
// };
|
|
91
|
+
// }
|
|
92
|
+
|
|
93
|
+
// const joliSource = context.encodeJoliSourceQuery(joliPayload);
|
|
94
|
+
|
|
95
|
+
// const host = context.testMode
|
|
96
|
+
// ? `https://G32115508989327465281365749294.app.jolibox.com`
|
|
97
|
+
// : `https://G32115508989327465281365749294.app.jolibox.com`;
|
|
98
|
+
// const retentionSchema = `${host}/recommended-guide/${context.mpId}?appId=${context.mpId}&joliSource=${joliSource}&navigationStyle=present`;
|
|
99
|
+
|
|
100
|
+
const quitResultDeffer = new Deferred<boolean>();
|
|
101
|
+
// 小程序不走挽留逻辑
|
|
102
|
+
if (context.mpType !== 'game') {
|
|
103
|
+
quitResultDeffer.resolve(false);
|
|
104
|
+
return quitResultDeffer.promise;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const data = await fetchExitRecommendations();
|
|
108
|
+
|
|
109
|
+
if (data.code !== 'SUCCESS') {
|
|
110
|
+
quitResultDeffer.resolve(false);
|
|
111
|
+
return quitResultDeffer.promise;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { gameListInfo, title, buttons } = data.data;
|
|
115
|
+
|
|
116
|
+
const modal = createRecommendModal({
|
|
117
|
+
games: gameListInfo.games,
|
|
118
|
+
title,
|
|
119
|
+
buttons,
|
|
120
|
+
onClose: (params: RecommendModalOnCloseParams) => {
|
|
121
|
+
switch (params.type) {
|
|
122
|
+
case 'quit':
|
|
123
|
+
quitResultDeffer.resolve(false);
|
|
124
|
+
break;
|
|
125
|
+
case 'dismiss':
|
|
126
|
+
quitResultDeffer.resolve(true);
|
|
127
|
+
modal.hide();
|
|
128
|
+
break;
|
|
129
|
+
case 'navigate':
|
|
130
|
+
// TODO: 跳转游戏
|
|
131
|
+
if (params.data?.game) {
|
|
132
|
+
openGameSchema(params.data.game);
|
|
133
|
+
} else {
|
|
134
|
+
quitResultDeffer.resolve(true);
|
|
135
|
+
}
|
|
136
|
+
modal.hide();
|
|
137
|
+
break;
|
|
138
|
+
default:
|
|
139
|
+
// 关闭弹框,留在当前游戏
|
|
140
|
+
quitResultDeffer.resolve(true);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
// // 异步
|
|
146
|
+
// setTimeout(() => {
|
|
147
|
+
// invokeNative('openSchemaSync', {
|
|
148
|
+
// schema: retentionSchema
|
|
149
|
+
// });
|
|
150
|
+
// }, 0);
|
|
151
|
+
return quitResultDeffer.promise;
|
|
152
|
+
}
|