@alfalab/bridge-to-native 1.3.1 → 1.3.2-beta.0828f8f
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/client/bridge-to-native.d.ts +5 -11
- package/client/bridge-to-native.js +5 -11
- package/client/services-and-utils/native-navigation-and-title-service.d.ts +18 -15
- package/client/services-and-utils/native-navigation-and-title-service.js +135 -61
- package/package.json +114 -114
- package/query-and-headers-keys.js +4 -2
- package/server/prepare-native-app-details-for-client.d.ts +2 -1
- package/server/prepare-native-app-details-for-client.js +14 -10
- package/server/utils.d.ts +7 -2
- package/server/utils.js +20 -4
|
@@ -147,15 +147,8 @@ export declare class BridgeToNative {
|
|
|
147
147
|
* экзепляру B2N следующей страницы текущего WA (в случае multi-page application).
|
|
148
148
|
*
|
|
149
149
|
* ВАЖНО!
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* 1. Микс client-side навигации и server-side навигации в рамках одного WA.
|
|
154
|
-
* т.е. одно WA должно использовать либо только `navigateClientSide`, либо только `navigateServerSide`.
|
|
155
|
-
* 2. Старт в WA 1 → переход к WA 2 → переход к WA 1,
|
|
156
|
-
* т.е. при использовании server-side навигации, история переходов разных WA не должна смешиваться.
|
|
157
|
-
*
|
|
158
|
-
* Снять эти ограничения возможно, но нужны доработки.
|
|
150
|
+
* `goBackAFewStepsClientSide` можно использовать только в рамках истории по SPA WA! Движение назад
|
|
151
|
+
* server-side переходом на несколько шагов не поддерживается.
|
|
159
152
|
*
|
|
160
153
|
* @param url URL для перехода внутри WA server-side навигацией.
|
|
161
154
|
* @param nativeTitle Текст заголовка, для «нативной» части WV, пустая строка — отсутствие заголовка.
|
|
@@ -206,8 +199,9 @@ export declare class BridgeToNative {
|
|
|
206
199
|
/**
|
|
207
200
|
* Для перезагрузки страницы необходимо использовать этот метод.
|
|
208
201
|
* Иначе синхронизация состояния с NA будет потеряна.
|
|
209
|
-
*
|
|
210
|
-
*
|
|
202
|
+
*
|
|
203
|
+
* @param skipReload По умолчанию метод сам делает `location.reload`,
|
|
204
|
+
* но с помощью аргумента можно отключить этот вызов, если нужно.
|
|
211
205
|
*/
|
|
212
206
|
reload(skipReload?: boolean): void;
|
|
213
207
|
/**
|
|
@@ -181,15 +181,8 @@ class BridgeToNative {
|
|
|
181
181
|
* экзепляру B2N следующей страницы текущего WA (в случае multi-page application).
|
|
182
182
|
*
|
|
183
183
|
* ВАЖНО!
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* 1. Микс client-side навигации и server-side навигации в рамках одного WA.
|
|
188
|
-
* т.е. одно WA должно использовать либо только `navigateClientSide`, либо только `navigateServerSide`.
|
|
189
|
-
* 2. Старт в WA 1 → переход к WA 2 → переход к WA 1,
|
|
190
|
-
* т.е. при использовании server-side навигации, история переходов разных WA не должна смешиваться.
|
|
191
|
-
*
|
|
192
|
-
* Снять эти ограничения возможно, но нужны доработки.
|
|
184
|
+
* `goBackAFewStepsClientSide` можно использовать только в рамках истории по SPA WA! Движение назад
|
|
185
|
+
* server-side переходом на несколько шагов не поддерживается.
|
|
193
186
|
*
|
|
194
187
|
* @param url URL для перехода внутри WA server-side навигацией.
|
|
195
188
|
* @param nativeTitle Текст заголовка, для «нативной» части WV, пустая строка — отсутствие заголовка.
|
|
@@ -248,8 +241,9 @@ class BridgeToNative {
|
|
|
248
241
|
/**
|
|
249
242
|
* Для перезагрузки страницы необходимо использовать этот метод.
|
|
250
243
|
* Иначе синхронизация состояния с NA будет потеряна.
|
|
251
|
-
*
|
|
252
|
-
*
|
|
244
|
+
*
|
|
245
|
+
* @param skipReload По умолчанию метод сам делает `location.reload`,
|
|
246
|
+
* но с помощью аргумента можно отключить этот вызов, если нужно.
|
|
253
247
|
*/
|
|
254
248
|
reload(skipReload) {
|
|
255
249
|
this.nativeNavigationAndTitleService.reload(skipReload);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
|
|
2
1
|
import { type BrowserHistoryApiWrappers, type HistoryPushStateParams, type LocationAssignParam, type LogError } from '../types';
|
|
3
2
|
import { type NativeParamsService } from './native-params-service';
|
|
4
3
|
/**
|
|
5
4
|
* Сервис, отвечающий за взаимодействие WA с WV компонентами NA —
|
|
6
5
|
* «заголовком» и кнопкой «назад».
|
|
6
|
+
*
|
|
7
|
+
* Подробное описание сценариев навигации и логики восстановления состояния при hard навигации
|
|
8
|
+
* см. в документе {@link ./NAVIGATION_SCENARIOS.md}.
|
|
7
9
|
*/
|
|
8
10
|
export declare class NativeNavigationAndTitleService {
|
|
9
11
|
private nativeParamsService;
|
|
@@ -34,14 +36,23 @@ export declare class NativeNavigationAndTitleService {
|
|
|
34
36
|
* после нажатия на кнопку «Назад» в NA, вызова `history.back()` и `history.go(-x)`.
|
|
35
37
|
*/
|
|
36
38
|
private handleClientSideNavigationBack;
|
|
37
|
-
private
|
|
39
|
+
private hasSavedHistoryStack;
|
|
38
40
|
/**
|
|
39
|
-
* Инициализирует `nativeHistoryStack
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* - Инициализация при server-side переходе «назад» по истории (Сценарий 3).
|
|
41
|
+
* Инициализирует `nativeHistoryStack`.
|
|
42
|
+
*
|
|
43
|
+
* Подробное описание каждого сценария см. в {@link ./NAVIGATION_SCENARIOS.md}.
|
|
43
44
|
*/
|
|
44
45
|
private initializeNativeHistoryStack;
|
|
46
|
+
private initializeForNewOrigin;
|
|
47
|
+
private initializeForForward;
|
|
48
|
+
private initializeFromBackwardOrReload;
|
|
49
|
+
private stackContainsCrossOriginMarker;
|
|
50
|
+
private truncateToLastCrossOriginMarker;
|
|
51
|
+
/**
|
|
52
|
+
* Читает и парсит `nativeHistoryStack` из SessionStorage.
|
|
53
|
+
* При ошибке чтения или парсинга логирует через `logError` и пробрасывает исключение.
|
|
54
|
+
*/
|
|
55
|
+
private readSavedHistoryStack;
|
|
45
56
|
/**
|
|
46
57
|
* Подготавливает ссылку для корректного перехода server-side навигацией.
|
|
47
58
|
*
|
|
@@ -51,15 +62,7 @@ export declare class NativeNavigationAndTitleService {
|
|
|
51
62
|
*/
|
|
52
63
|
private prepareExternalLinkBeforeOpen;
|
|
53
64
|
/**
|
|
54
|
-
*
|
|
55
|
-
* снова сохраняет его в sessionStorage, уменьшая список на одну запись,
|
|
56
|
-
* на случай, если будет дальнейший переход назад server-side навигацией.
|
|
57
|
-
*
|
|
58
|
-
* @returns Актуальное состояние `nativeHistoryStack` из sessionStorage.
|
|
59
|
-
*/
|
|
60
|
-
private readAndUpdateNativeHistoryStackSessionStorage;
|
|
61
|
-
/**
|
|
62
|
-
* Сохранение состояния связи текущего WA с NA при server-side навигации в sessionStorage.
|
|
65
|
+
* Сохраняет `nativeHistoryStack` в SessionStorage.
|
|
63
66
|
*/
|
|
64
67
|
private saveNativeHistoryStack;
|
|
65
68
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/* eslint max-lines: ["error", {"skipComments": true}] */
|
|
2
|
+
/* eslint max-lines: ["error", {"max": 360, "skipComments": true}] */
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.NativeNavigationAndTitleService = void 0;
|
|
5
5
|
const query_and_headers_keys_1 = require("../../query-and-headers-keys");
|
|
@@ -7,6 +7,9 @@ const close_webview_util_1 = require("./close-webview-util");
|
|
|
7
7
|
/**
|
|
8
8
|
* Сервис, отвечающий за взаимодействие WA с WV компонентами NA —
|
|
9
9
|
* «заголовком» и кнопкой «назад».
|
|
10
|
+
*
|
|
11
|
+
* Подробное описание сценариев навигации и логики восстановления состояния при hard навигации
|
|
12
|
+
* см. в документе {@link ./NAVIGATION_SCENARIOS.md}.
|
|
10
13
|
*/
|
|
11
14
|
class NativeNavigationAndTitleService {
|
|
12
15
|
constructor(nativeParamsService, browserHistoryApiWrappers, logError) {
|
|
@@ -64,20 +67,26 @@ class NativeNavigationAndTitleService {
|
|
|
64
67
|
window.history.pushState(state, '', url);
|
|
65
68
|
}
|
|
66
69
|
this.nativeHistoryStack.push(nativeTitle);
|
|
70
|
+
this.saveNativeHistoryStack();
|
|
67
71
|
this.syncHistoryWithNative();
|
|
68
72
|
}
|
|
69
73
|
navigateServerSide(link, nativeTitle = '') {
|
|
70
74
|
const url = link instanceof URL ? link : new URL(link);
|
|
75
|
+
const isCrossOrigin = url.origin !== window.location.origin;
|
|
76
|
+
if (isCrossOrigin) {
|
|
77
|
+
this.nativeHistoryStack.push(2 /* NativeHistoryStackSpecialValues.ServerSideNavigationNextOrigin */);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
this.nativeHistoryStack.push(nativeTitle);
|
|
81
|
+
}
|
|
71
82
|
if (nativeTitle) {
|
|
72
83
|
url.searchParams.set(query_and_headers_keys_1.QUERY_B2N_TITLE, nativeTitle);
|
|
73
84
|
}
|
|
74
|
-
// TODO: Предыдущая реализация на iOS открывала новое WV. Возможно, что-то плохо работало,
|
|
75
|
-
// обязательно протестировать.
|
|
76
85
|
this.saveNativeHistoryStack();
|
|
77
86
|
window.location.assign(this.prepareExternalLinkBeforeOpen(url));
|
|
78
87
|
}
|
|
79
88
|
reload(skipReload = false) {
|
|
80
|
-
this.nativeHistoryStack.push(1 /* NativeHistoryStackSpecialValues.TemporaryReloadStub */);
|
|
89
|
+
this.nativeHistoryStack.push(1 /* NativeHistoryStackSpecialValues.TemporaryReloadStub */);
|
|
81
90
|
this.saveNativeHistoryStack();
|
|
82
91
|
// информация для серверной стороны B2N, что происходит `reload` и парсить запрос на предмет NA параметров не нужно (в нем их скорее всего не будет)
|
|
83
92
|
document.cookie = `${query_and_headers_keys_1.COOKIE_KEY_BRIDGE_TO_NATIVE_RELOAD}=true; Path=/`;
|
|
@@ -87,10 +96,12 @@ class NativeNavigationAndTitleService {
|
|
|
87
96
|
}
|
|
88
97
|
setInitialView(nativeTitle = '') {
|
|
89
98
|
this.nativeHistoryStack = [nativeTitle];
|
|
99
|
+
this.saveNativeHistoryStack();
|
|
90
100
|
this.syncHistoryWithNative();
|
|
91
101
|
}
|
|
92
102
|
setTitle(nativeTitle) {
|
|
93
103
|
this.nativeHistoryStack[this.nativeHistoryStack.length - 1] = nativeTitle;
|
|
104
|
+
this.saveNativeHistoryStack();
|
|
94
105
|
this.syncHistoryWithNative();
|
|
95
106
|
}
|
|
96
107
|
/**
|
|
@@ -122,89 +133,152 @@ class NativeNavigationAndTitleService {
|
|
|
122
133
|
(0, close_webview_util_1.closeWebviewUtil)();
|
|
123
134
|
return;
|
|
124
135
|
}
|
|
136
|
+
this.saveNativeHistoryStack();
|
|
125
137
|
this.syncHistoryWithNative();
|
|
126
138
|
}
|
|
127
|
-
|
|
139
|
+
// eslint-disable-next-line class-methods-use-this -- удобней использовать метод в контексте экземпляра.
|
|
140
|
+
hasSavedHistoryStack() {
|
|
128
141
|
return sessionStorage.getItem(query_and_headers_keys_1.SS_KEY_BRIDGE_TO_NATIVE_HISTORY_STACK) !== null;
|
|
129
142
|
}
|
|
130
143
|
/**
|
|
131
|
-
* Инициализирует `nativeHistoryStack
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* - Инициализация при server-side переходе «назад» по истории (Сценарий 3).
|
|
144
|
+
* Инициализирует `nativeHistoryStack`.
|
|
145
|
+
*
|
|
146
|
+
* Подробное описание каждого сценария см. в {@link ./NAVIGATION_SCENARIOS.md}.
|
|
135
147
|
*/
|
|
136
148
|
initializeNativeHistoryStack() {
|
|
137
149
|
const { nextPageId, title } = this.nativeParamsService;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
//
|
|
141
|
-
this.nativeHistoryStack =
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
catch (_a) {
|
|
152
|
-
this.nativeHistoryStack = [''];
|
|
153
|
-
}
|
|
150
|
+
const hasSS = this.hasSavedHistoryStack();
|
|
151
|
+
if (nextPageId && !hasSS) {
|
|
152
|
+
// есть nextPageId + нет SS → на этом origin ранее не были.
|
|
153
|
+
this.nativeHistoryStack = this.initializeForNewOrigin(nextPageId, title);
|
|
154
|
+
}
|
|
155
|
+
else if (nextPageId && hasSS) {
|
|
156
|
+
// есть nextPageId + есть SS → forward навигация.
|
|
157
|
+
this.nativeHistoryStack = this.initializeForForward(nextPageId, title);
|
|
158
|
+
}
|
|
159
|
+
else if (!nextPageId && hasSS) {
|
|
160
|
+
// нет nextPageId + есть SS → back-навигация или reload.
|
|
161
|
+
this.nativeHistoryStack = this.initializeFromBackwardOrReload(title);
|
|
154
162
|
}
|
|
155
163
|
else {
|
|
156
|
-
//
|
|
157
|
-
// её нет, значит это инициализация сразу после открытия нового WV.
|
|
164
|
+
// нет nextPageId + нет SS → старт нового WV, назад идти некуда.
|
|
158
165
|
this.nativeHistoryStack = [title];
|
|
159
166
|
}
|
|
167
|
+
this.saveNativeHistoryStack();
|
|
160
168
|
this.syncHistoryWithNative();
|
|
161
169
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
170
|
+
// eslint-disable-next-line class-methods-use-this -- удобней использовать метод в контексте экземпляра.
|
|
171
|
+
initializeForNewOrigin(nextPageId, title) {
|
|
172
|
+
const stack = new Array(nextPageId).fill(0 /* NativeHistoryStackSpecialValues.ServerSideNavigationStub */);
|
|
173
|
+
stack[stack.length - 1] = title;
|
|
174
|
+
return stack;
|
|
175
|
+
}
|
|
176
|
+
initializeForForward(nextPageId, title) {
|
|
177
|
+
try {
|
|
178
|
+
const savedStack = this.readSavedHistoryStack();
|
|
179
|
+
const lastSaved = savedStack[savedStack.length - 1];
|
|
180
|
+
if (lastSaved === 2 /* NativeHistoryStackSpecialValues.ServerSideNavigationNextOrigin */ &&
|
|
181
|
+
savedStack.length < nextPageId) {
|
|
182
|
+
const stack = new Array(nextPageId).fill(0 /* NativeHistoryStackSpecialValues.ServerSideNavigationStub */);
|
|
183
|
+
for (let i = 0; i < savedStack.length; i++) {
|
|
184
|
+
stack[i] = savedStack[i];
|
|
185
|
+
}
|
|
186
|
+
stack[stack.length - 1] = title;
|
|
187
|
+
return stack;
|
|
188
|
+
}
|
|
189
|
+
if (savedStack.length === nextPageId) {
|
|
190
|
+
savedStack[savedStack.length - 1] = title;
|
|
191
|
+
return savedStack;
|
|
192
|
+
}
|
|
193
|
+
return this.initializeForNewOrigin(nextPageId, title);
|
|
194
|
+
}
|
|
195
|
+
catch (_a) {
|
|
196
|
+
return this.initializeForNewOrigin(nextPageId, title);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
initializeFromBackwardOrReload(title) {
|
|
200
|
+
try {
|
|
201
|
+
const savedStack = this.readSavedHistoryStack();
|
|
202
|
+
const lastEntry = savedStack[savedStack.length - 1];
|
|
203
|
+
if (lastEntry === 1 /* NativeHistoryStackSpecialValues.TemporaryReloadStub */) {
|
|
204
|
+
savedStack.pop();
|
|
205
|
+
if (savedStack.length === 0) {
|
|
206
|
+
return [title];
|
|
207
|
+
}
|
|
208
|
+
return savedStack;
|
|
209
|
+
}
|
|
210
|
+
if (this.stackContainsCrossOriginMarker(savedStack)) {
|
|
211
|
+
return this.truncateToLastCrossOriginMarker(savedStack);
|
|
212
|
+
}
|
|
213
|
+
savedStack.pop();
|
|
214
|
+
if (savedStack.length === 0) {
|
|
215
|
+
return [title];
|
|
216
|
+
}
|
|
217
|
+
return savedStack;
|
|
218
|
+
}
|
|
219
|
+
catch (_a) {
|
|
220
|
+
return [title];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// eslint-disable-next-line class-methods-use-this -- удобней использовать метод в контексте экземпляра.
|
|
224
|
+
stackContainsCrossOriginMarker(stack) {
|
|
225
|
+
return stack.find((el) => el === 2 /* NativeHistoryStackSpecialValues.ServerSideNavigationNextOrigin */);
|
|
226
|
+
}
|
|
227
|
+
// eslint-disable-next-line class-methods-use-this -- удобней использовать метод в контексте экземпляра.
|
|
228
|
+
truncateToLastCrossOriginMarker(stack) {
|
|
229
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
230
|
+
if (stack[i] === 2 /* NativeHistoryStackSpecialValues.ServerSideNavigationNextOrigin */) {
|
|
231
|
+
return stack.slice(0, i);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return stack;
|
|
175
235
|
}
|
|
176
236
|
/**
|
|
177
|
-
* Читает
|
|
178
|
-
*
|
|
179
|
-
* на случай, если будет дальнейший переход назад server-side навигацией.
|
|
180
|
-
*
|
|
181
|
-
* @returns Актуальное состояние `nativeHistoryStack` из sessionStorage.
|
|
237
|
+
* Читает и парсит `nativeHistoryStack` из SessionStorage.
|
|
238
|
+
* При ошибке чтения или парсинга логирует через `logError` и пробрасывает исключение.
|
|
182
239
|
*/
|
|
183
|
-
|
|
240
|
+
readSavedHistoryStack() {
|
|
241
|
+
const serialized = sessionStorage.getItem(query_and_headers_keys_1.SS_KEY_BRIDGE_TO_NATIVE_HISTORY_STACK);
|
|
184
242
|
try {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
throw new Error();
|
|
188
|
-
}
|
|
189
|
-
const nativeHistoryStack = JSON.parse(serializedNativeHistoryStack); // происходит внутри оператора `catch`, поэтому кастинг типа приемлем
|
|
190
|
-
const nativeHistoryStackToSerialize = nativeHistoryStack.slice(0, -1);
|
|
191
|
-
sessionStorage.setItem(query_and_headers_keys_1.SS_KEY_BRIDGE_TO_NATIVE_HISTORY_STACK, JSON.stringify(nativeHistoryStackToSerialize));
|
|
192
|
-
if (nativeHistoryStack[nativeHistoryStack.length - 1] ===
|
|
193
|
-
1 /* NativeHistoryStackSpecialValues.TemporaryReloadStub */) {
|
|
194
|
-
return nativeHistoryStack.slice(0, -1);
|
|
243
|
+
if (!serialized) {
|
|
244
|
+
throw new Error(`${query_and_headers_keys_1.SS_KEY_BRIDGE_TO_NATIVE_HISTORY_STACK} sessionStorage expected not to be null`);
|
|
195
245
|
}
|
|
196
|
-
return
|
|
246
|
+
return JSON.parse(serialized);
|
|
197
247
|
}
|
|
198
248
|
catch (e) {
|
|
199
249
|
if (this.logError) {
|
|
200
|
-
this.logError(
|
|
201
|
-
|
|
250
|
+
this.logError(`Клиентский код B2N не смог получить ${query_and_headers_keys_1.SS_KEY_BRIDGE_TO_NATIVE_HISTORY_STACK} из sessionStorage
|
|
251
|
+
Могут возникнуть проблемы с кнопкой «Назад» в NA.`, e);
|
|
202
252
|
}
|
|
203
|
-
throw
|
|
253
|
+
throw e;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Подготавливает ссылку для корректного перехода server-side навигацией.
|
|
258
|
+
*
|
|
259
|
+
* @param url URL для перехода внутри WA server-side навигацией.
|
|
260
|
+
* @return Подготовленная ссылка для экземпляра B2N следующего WA или
|
|
261
|
+
* экзепляра B2N следующей страницы текущего WA
|
|
262
|
+
*/
|
|
263
|
+
prepareExternalLinkBeforeOpen(url) {
|
|
264
|
+
const currentPageId = this.nativeHistoryStack.length;
|
|
265
|
+
const modifiedUrl = new URL(url);
|
|
266
|
+
const { originalWebviewParams, appVersion } = this.nativeParamsService;
|
|
267
|
+
if (originalWebviewParams) {
|
|
268
|
+
const originalWebviewSearchParams = new URLSearchParams(originalWebviewParams);
|
|
269
|
+
originalWebviewSearchParams.forEach((value, key) => {
|
|
270
|
+
modifiedUrl.searchParams.set(key, value);
|
|
271
|
+
});
|
|
204
272
|
}
|
|
273
|
+
// Явно добавляем query-параметр `device_app_version` используемый NA на iOS, чтобы он был и в Android окружении.
|
|
274
|
+
// Таким образом гарантируется, что версию приложения будет видеть следующее WA
|
|
275
|
+
// (заголовок `app-version` может отсутствовать при server-side переходах).
|
|
276
|
+
modifiedUrl.searchParams.set(query_and_headers_keys_1.QUERY_NATIVE_IOS_APPVERSION, appVersion);
|
|
277
|
+
modifiedUrl.searchParams.set(query_and_headers_keys_1.QUERY_B2N_NEXT_PAGEID, currentPageId.toString());
|
|
278
|
+
return modifiedUrl;
|
|
205
279
|
}
|
|
206
280
|
/**
|
|
207
|
-
*
|
|
281
|
+
* Сохраняет `nativeHistoryStack` в SessionStorage.
|
|
208
282
|
*/
|
|
209
283
|
saveNativeHistoryStack() {
|
|
210
284
|
const serializedNativeHistoryStack = JSON.stringify(this.nativeHistoryStack);
|
package/package.json
CHANGED
|
@@ -1,121 +1,121 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
"name": "@alfalab/bridge-to-native",
|
|
3
|
+
"version": "1.3.2-beta.0828f8f",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Утилита для удобной работы веб приложения внутри нативного приложения и коммуникации с ним.",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20.19.2",
|
|
8
|
+
"npm": "please-use-yarn"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/core-ds/bridge-to-native.git"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
"./client": {
|
|
16
|
+
"import": "./client/index.js",
|
|
17
|
+
"require": "./client/index.js",
|
|
18
|
+
"types": "./client/index.d.ts"
|
|
9
19
|
},
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
"./server": {
|
|
21
|
+
"import": "./server/index.js",
|
|
22
|
+
"require": "./server/index.js",
|
|
23
|
+
"types": "./server/index.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/core-ds/bridge-to-native/issues"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "yarn build:clean && yarn build:ts",
|
|
32
|
+
"build:clean": "shx rm -rf .publish",
|
|
33
|
+
"build:copy-package-json": "shx cp package.json .publish/package.json",
|
|
34
|
+
"build:ts": "tsc --build",
|
|
35
|
+
"format": "arui-presets-lint format",
|
|
36
|
+
"format:check": "arui-presets-lint format:check",
|
|
37
|
+
"lint": "yarn lint:scripts && yarn format:check",
|
|
38
|
+
"lint:fix": "yarn lint:scripts --fix && yarn format",
|
|
39
|
+
"lint:scripts": "arui-presets-lint scripts",
|
|
40
|
+
"pub": "npm publish .publish --userconfig \"../.npmrc\" --tag \"$TAG\"",
|
|
41
|
+
"release": "yarn build && yarn build:copy-package-json && yarn pub",
|
|
42
|
+
"test": "arui-scripts test --silent --collect-coverage"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@happy-dom/jest-environment": "18.0.1",
|
|
46
|
+
"@types/jest": "29.5.14",
|
|
47
|
+
"@types/node": "20.19.1",
|
|
48
|
+
"arui-presets-lint": "8.7.0",
|
|
49
|
+
"arui-scripts": "19.0.7",
|
|
50
|
+
"copyfiles": "2.4.1",
|
|
51
|
+
"jest-junit": "10.0.0",
|
|
52
|
+
"lint-staged": "^12.5.0",
|
|
53
|
+
"promisify-child-process": "4.1.1",
|
|
54
|
+
"shx": "0.3.4",
|
|
55
|
+
"typescript": "5.5.4"
|
|
56
|
+
},
|
|
57
|
+
"commitlint": {
|
|
58
|
+
"extends": "./node_modules/arui-presets-lint/commitlint"
|
|
59
|
+
},
|
|
60
|
+
"eslintConfig": {
|
|
61
|
+
"extends": "./node_modules/arui-presets-lint/eslint",
|
|
62
|
+
"parserOptions": {
|
|
63
|
+
"project": [
|
|
64
|
+
"./__tests__/tsconfig.json",
|
|
65
|
+
"./src/client/tsconfig.json",
|
|
66
|
+
"./src/server/tsconfig.json"
|
|
67
|
+
]
|
|
13
68
|
},
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"types": "./client/index.d.ts"
|
|
19
|
-
},
|
|
20
|
-
"./server": {
|
|
21
|
-
"import": "./server/index.js",
|
|
22
|
-
"require": "./server/index.js",
|
|
23
|
-
"types": "./server/index.d.ts"
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
"sideEffects": false,
|
|
27
|
-
"bugs": {
|
|
28
|
-
"url": "https://github.com/core-ds/bridge-to-native/issues"
|
|
29
|
-
},
|
|
30
|
-
"scripts": {
|
|
31
|
-
"build": "yarn build:clean && yarn build:ts",
|
|
32
|
-
"build:clean": "shx rm -rf .publish",
|
|
33
|
-
"build:copy-package-json": "shx cp package.json .publish/package.json",
|
|
34
|
-
"build:ts": "tsc --build",
|
|
35
|
-
"format": "arui-presets-lint format",
|
|
36
|
-
"format:check": "arui-presets-lint format:check",
|
|
37
|
-
"lint": "yarn lint:scripts && yarn format:check",
|
|
38
|
-
"lint:fix": "yarn lint:scripts --fix && yarn format",
|
|
39
|
-
"lint:scripts": "arui-presets-lint scripts",
|
|
40
|
-
"pub": "npm publish .publish --userconfig \"../.npmrc\" --tag \"$TAG\"",
|
|
41
|
-
"release": "yarn build && yarn build:copy-package-json && yarn pub",
|
|
42
|
-
"test": "arui-scripts test --silent --collect-coverage"
|
|
43
|
-
},
|
|
44
|
-
"devDependencies": {
|
|
45
|
-
"@happy-dom/jest-environment": "18.0.1",
|
|
46
|
-
"@types/jest": "29.5.14",
|
|
47
|
-
"@types/node": "20.19.1",
|
|
48
|
-
"arui-presets-lint": "8.7.0",
|
|
49
|
-
"arui-scripts": "19.0.7",
|
|
50
|
-
"copyfiles": "2.4.1",
|
|
51
|
-
"jest-junit": "10.0.0",
|
|
52
|
-
"lint-staged": "^12.5.0",
|
|
53
|
-
"promisify-child-process": "4.1.1",
|
|
54
|
-
"shx": "0.3.4",
|
|
55
|
-
"typescript": "5.5.4"
|
|
56
|
-
},
|
|
57
|
-
"commitlint": {
|
|
58
|
-
"extends": "./node_modules/arui-presets-lint/commitlint"
|
|
59
|
-
},
|
|
60
|
-
"eslintConfig": {
|
|
61
|
-
"extends": "./node_modules/arui-presets-lint/eslint",
|
|
62
|
-
"parserOptions": {
|
|
63
|
-
"project": [
|
|
64
|
-
"./__tests__/tsconfig.json",
|
|
65
|
-
"./src/client/tsconfig.json",
|
|
66
|
-
"./src/server/tsconfig.json"
|
|
67
|
-
]
|
|
68
|
-
},
|
|
69
|
-
"overrides": [
|
|
70
|
-
{
|
|
71
|
-
"files": [
|
|
72
|
-
"__tests__/**/*"
|
|
73
|
-
],
|
|
74
|
-
"rules": {
|
|
75
|
-
"max-lines": "off"
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
]
|
|
79
|
-
},
|
|
80
|
-
"jest": {
|
|
81
|
-
"projects": [
|
|
82
|
-
{
|
|
83
|
-
"displayName": "client-tests",
|
|
84
|
-
"testEnvironment": "@happy-dom/jest-environment",
|
|
85
|
-
"testPathIgnorePatterns": [
|
|
86
|
-
"/__tests__/server/"
|
|
87
|
-
]
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
"displayName": "server-tests",
|
|
91
|
-
"testEnvironment": "node",
|
|
92
|
-
"testPathIgnorePatterns": [
|
|
93
|
-
"/__tests__/client/"
|
|
94
|
-
]
|
|
95
|
-
}
|
|
96
|
-
],
|
|
97
|
-
"testRegex": [
|
|
98
|
-
"__tests__/.*\\.test\\.ts"
|
|
69
|
+
"overrides": [
|
|
70
|
+
{
|
|
71
|
+
"files": [
|
|
72
|
+
"__tests__/**/*"
|
|
99
73
|
],
|
|
100
|
-
"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
74
|
+
"rules": {
|
|
75
|
+
"max-lines": "off"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
"jest": {
|
|
81
|
+
"projects": [
|
|
82
|
+
{
|
|
83
|
+
"displayName": "client-tests",
|
|
84
|
+
"testEnvironment": "@happy-dom/jest-environment",
|
|
85
|
+
"testPathIgnorePatterns": [
|
|
86
|
+
"/__tests__/server/"
|
|
107
87
|
]
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
|
|
114
|
-
".yarn/releases/*"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"displayName": "server-tests",
|
|
91
|
+
"testEnvironment": "node",
|
|
92
|
+
"testPathIgnorePatterns": [
|
|
93
|
+
"/__tests__/client/"
|
|
115
94
|
]
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
"testRegex": [
|
|
98
|
+
"__tests__/.*\\.test\\.ts"
|
|
99
|
+
],
|
|
100
|
+
"coveragePathIgnorePatterns": [
|
|
101
|
+
"/node_modules/",
|
|
102
|
+
"/src/(client|server)/index.ts"
|
|
103
|
+
],
|
|
104
|
+
"reporters": [
|
|
105
|
+
"default",
|
|
106
|
+
"jest-junit"
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
"prettier": "arui-presets-lint/prettier",
|
|
110
|
+
"stylelint": {
|
|
111
|
+
"extends": "arui-presets-lint/stylelint",
|
|
112
|
+
"ignoreFiles": [
|
|
113
|
+
"coverage/**/*.js",
|
|
114
|
+
".yarn/releases/*"
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
"publishConfig": {
|
|
118
|
+
"registry": "https://registry.npmjs.org"
|
|
119
|
+
},
|
|
120
|
+
"packageManager": "yarn@4.12.0"
|
|
121
121
|
}
|
|
@@ -34,7 +34,7 @@ exports.QUERY_B2N_TITLE = 'b2n-title';
|
|
|
34
34
|
// (т.к. `title` часто может использоваться самим веб-приложением).
|
|
35
35
|
// Игнорируется, если указан 'b2n-title'.
|
|
36
36
|
exports.QUERY_B2N_TITLE_DEPRECATED = 'title';
|
|
37
|
-
// NA на iOS
|
|
37
|
+
// NA на iOS передаёт в этом параметре схему,
|
|
38
38
|
// под которым оно зарегистрировано в OS.
|
|
39
39
|
// Известные проблемы:
|
|
40
40
|
// * В старых версиях отсутствует, клиентский код использует хардкод (см. `src/client/constants.ts`);
|
|
@@ -42,7 +42,9 @@ exports.QUERY_B2N_TITLE_DEPRECATED = 'title';
|
|
|
42
42
|
// и схемой, которая приходит в этом параметре.
|
|
43
43
|
// Нужно просить iOS разработчика собрать сборку нормально.
|
|
44
44
|
exports.QUERY_NATIVE_IOS_APPID = 'applicationId';
|
|
45
|
-
// NA на iOS
|
|
45
|
+
// Исторически NA на iOS передаёт в этом параметре свою версию.
|
|
46
|
+
// B2N сознательно переиспользует этот query и для server-side переходов в Android,
|
|
47
|
+
// чтобы не вводить отдельный служебный параметр только для переноса версии между WA.
|
|
46
48
|
exports.QUERY_NATIVE_IOS_APPVERSION = 'device_app_version';
|
|
47
49
|
// NA на обеих платформах в этом параметре передаёт активную тему (светлая/тёмная).
|
|
48
50
|
exports.QUERY_NATIVE_THEME = 'theme';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type NativeParams } from '../types';
|
|
1
2
|
import { type UniversalRequest } from './types';
|
|
2
3
|
/**
|
|
3
4
|
* Парсит запрос, доставая из него данные о нативном приложении,
|
|
@@ -10,4 +11,4 @@ import { type UniversalRequest } from './types';
|
|
|
10
11
|
* Нужно передать функцию, которая средствами используемого веб-сервера добавит заголовок в ответ.
|
|
11
12
|
* b2native с её помощью добавит `Set-Cookie` заголовок с некоторыми данными для своего клиентского кода.
|
|
12
13
|
*/
|
|
13
|
-
export declare function prepareNativeAppDetailsForClient(request: UniversalRequest, setResponseHeader: (headerKey: string, headerValue: string) => void):
|
|
14
|
+
export declare function prepareNativeAppDetailsForClient(request: UniversalRequest, setResponseHeader: (headerKey: string, headerValue: string) => void): Partial<NativeParams>;
|
|
@@ -25,25 +25,21 @@ function prepareNativeAppDetailsForClient(request, setResponseHeader) {
|
|
|
25
25
|
// 1) Данных NA в запросе с большой вероятностью не будет;
|
|
26
26
|
// 2) клиентская сторона сохранит всё, что нужно в SessionStorage
|
|
27
27
|
const cookieHeader = (0, utils_1.getHeaderValue)(request, query_and_headers_keys_1.HEADER_KEY_COOKIE);
|
|
28
|
+
const nativeParamsFromCookie = (0, utils_1.readNativeParamsFromCookie)(cookieHeader);
|
|
28
29
|
const hasReloadFlag = cookieHeader?.includes(`${query_and_headers_keys_1.COOKIE_KEY_BRIDGE_TO_NATIVE_RELOAD}=true`);
|
|
29
30
|
if (hasReloadFlag) {
|
|
30
31
|
setResponseHeader('Set-Cookie', `${query_and_headers_keys_1.COOKIE_KEY_BRIDGE_TO_NATIVE_RELOAD}=false; Max-Age=0; Path=/`);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
return JSON.parse(decodeURIComponent(cookieData));
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return parseRequest(request);
|
|
38
|
-
}
|
|
32
|
+
if (nativeParamsFromCookie) {
|
|
33
|
+
return nativeParamsFromCookie;
|
|
39
34
|
}
|
|
35
|
+
return buildNativeParams(request);
|
|
40
36
|
}
|
|
41
|
-
const nativeParams =
|
|
37
|
+
const nativeParams = buildNativeParams(request, nativeParamsFromCookie);
|
|
42
38
|
const serializedNativeParams = encodeURIComponent(JSON.stringify(nativeParams));
|
|
43
39
|
setResponseHeader('Set-Cookie', `${query_and_headers_keys_1.COOKIE_KEY_BRIDGE_TO_NATIVE_DATA}=${serializedNativeParams}; Path=/`);
|
|
44
40
|
return nativeParams;
|
|
45
41
|
}
|
|
46
|
-
function
|
|
42
|
+
function buildNativeParams(request, nativeParamsFromCookie) {
|
|
47
43
|
// Прихраним «сервисные» query-параметры от нативного приложения,
|
|
48
44
|
// чтобы подмешать их к URL при переходе в другое веб-приложение
|
|
49
45
|
// в рамках одной вебвью-сессии.
|
|
@@ -70,6 +66,9 @@ function parseRequest(request) {
|
|
|
70
66
|
const [, appIdSubsting] = iosAppIdQuery.match(regexp_patterns_1.iosAppIdPattern); // кастинг ок — в условии блока регулярка проверена
|
|
71
67
|
nativeParams.iosAppId = appIdSubsting;
|
|
72
68
|
}
|
|
69
|
+
// Версия приложения может приехать прямо в текущем request (в query-параметре `device_app_version`
|
|
70
|
+
// или заголовке `app-version`), а при server-side переходах / возврате назад может уже не приехать.
|
|
71
|
+
// В таких сценариях берём `appVersion` фолбэком из куки `bridgeToNativeData`.
|
|
73
72
|
if (iosAppVersionQuery && regexp_patterns_1.versionPattern.test(iosAppVersionQuery)) {
|
|
74
73
|
nativeParams.appVersion = iosAppVersionQuery;
|
|
75
74
|
}
|
|
@@ -77,6 +76,11 @@ function parseRequest(request) {
|
|
|
77
76
|
const [, versionSubstring] = appVersionFromHeaders.match(regexp_patterns_1.versionPattern); // кастинг ок — в условии блока регулярка проверена
|
|
78
77
|
nativeParams.appVersion = versionSubstring;
|
|
79
78
|
}
|
|
79
|
+
else if (nativeParamsFromCookie?.appVersion &&
|
|
80
|
+
regexp_patterns_1.versionPattern.test(nativeParamsFromCookie.appVersion)) {
|
|
81
|
+
const [, versionSubstring] = nativeParamsFromCookie.appVersion.match(regexp_patterns_1.versionPattern);
|
|
82
|
+
nativeParams.appVersion = versionSubstring;
|
|
83
|
+
}
|
|
80
84
|
else {
|
|
81
85
|
nativeParams.appVersion = '0.0.0';
|
|
82
86
|
}
|
package/server/utils.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type NativeParams } from '../types';
|
|
1
2
|
import { type UniversalRequest } from './types';
|
|
2
3
|
/**
|
|
3
4
|
* На основе объекта запроса любого типа возвращает
|
|
@@ -31,6 +32,10 @@ export declare function parseHeaderTimestamp(headerValue: string | number | null
|
|
|
31
32
|
/**
|
|
32
33
|
* Возвращает значение для нужной куки
|
|
33
34
|
*
|
|
34
|
-
* @param
|
|
35
|
+
* @param cookieHeader Имя куки
|
|
35
36
|
*/
|
|
36
|
-
export declare const getBridgeToNativeDataCookie: (
|
|
37
|
+
export declare const getBridgeToNativeDataCookie: (cookieHeader: string | null) => string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Возвращает десериализованные данные из `bridgeToNativeData` cookie.
|
|
40
|
+
*/
|
|
41
|
+
export declare function readNativeParamsFromCookie(cookieHeader: string | null): Partial<NativeParams> | null;
|
package/server/utils.js
CHANGED
|
@@ -5,6 +5,7 @@ exports.getHeaderValue = getHeaderValue;
|
|
|
5
5
|
exports.getQueryValues = getQueryValues;
|
|
6
6
|
exports.hasBridgeToNativeDataCookie = hasBridgeToNativeDataCookie;
|
|
7
7
|
exports.parseHeaderTimestamp = parseHeaderTimestamp;
|
|
8
|
+
exports.readNativeParamsFromCookie = readNativeParamsFromCookie;
|
|
8
9
|
const query_and_headers_keys_1 = require("../query-and-headers-keys");
|
|
9
10
|
const regexp_patterns_1 = require("./regexp-patterns");
|
|
10
11
|
/**
|
|
@@ -57,13 +58,13 @@ function parseHeaderTimestamp(headerValue) {
|
|
|
57
58
|
/**
|
|
58
59
|
* Возвращает значение для нужной куки
|
|
59
60
|
*
|
|
60
|
-
* @param
|
|
61
|
+
* @param cookieHeader Имя куки
|
|
61
62
|
*/
|
|
62
|
-
const getBridgeToNativeDataCookie = (
|
|
63
|
-
if (!
|
|
63
|
+
const getBridgeToNativeDataCookie = (cookieHeader) => {
|
|
64
|
+
if (!cookieHeader) {
|
|
64
65
|
return undefined;
|
|
65
66
|
}
|
|
66
|
-
const cookies =
|
|
67
|
+
const cookies = cookieHeader.split(';');
|
|
67
68
|
for (const cookie of cookies) {
|
|
68
69
|
const [key, value] = cookie.trim().split('=');
|
|
69
70
|
if (key === query_and_headers_keys_1.COOKIE_KEY_BRIDGE_TO_NATIVE_DATA) {
|
|
@@ -73,3 +74,18 @@ const getBridgeToNativeDataCookie = (cookieName) => {
|
|
|
73
74
|
return undefined;
|
|
74
75
|
};
|
|
75
76
|
exports.getBridgeToNativeDataCookie = getBridgeToNativeDataCookie;
|
|
77
|
+
/**
|
|
78
|
+
* Возвращает десериализованные данные из `bridgeToNativeData` cookie.
|
|
79
|
+
*/
|
|
80
|
+
function readNativeParamsFromCookie(cookieHeader) {
|
|
81
|
+
const cookieData = (0, exports.getBridgeToNativeDataCookie)(cookieHeader);
|
|
82
|
+
if (!cookieData) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(decodeURIComponent(cookieData));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|