@alfalab/bridge-to-native 1.2.1 → 1.3.2
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/services-and-utils/native-navigation-and-title-service.d.ts +1 -0
- package/client/services-and-utils/native-navigation-and-title-service.js +33 -3
- package/package.json +2 -3
- 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
|
@@ -50,6 +50,7 @@ export declare class NativeNavigationAndTitleService {
|
|
|
50
50
|
* экзепляра B2N следующей страницы текущего WA
|
|
51
51
|
*/
|
|
52
52
|
private prepareExternalLinkBeforeOpen;
|
|
53
|
+
private static shouldInitializeFromNextPageId;
|
|
53
54
|
/**
|
|
54
55
|
* Читает сохраннённый в sessionStorage `nativeHistoryStack`,
|
|
55
56
|
* снова сохраняет его в sessionStorage, уменьшая список на одну запись,
|
|
@@ -135,9 +135,12 @@ class NativeNavigationAndTitleService {
|
|
|
135
135
|
*/
|
|
136
136
|
initializeNativeHistoryStack() {
|
|
137
137
|
const { nextPageId, title } = this.nativeParamsService;
|
|
138
|
-
if (nextPageId
|
|
138
|
+
if (nextPageId &&
|
|
139
|
+
NativeNavigationAndTitleService.shouldInitializeFromNextPageId(nextPageId)) {
|
|
139
140
|
// Сценарий 2 – `nextPageId` ставит метод `this.navigateServerSide`,
|
|
140
141
|
// т.е. это инициализация сразу после перехода server-side навигацией.
|
|
142
|
+
// Если в sessionStorage уже есть стек, используем `nextPageId` только для прямого
|
|
143
|
+
// перехода "вперёд", когда он на 1 больше сохранённой глубины истории.
|
|
141
144
|
this.nativeHistoryStack = new Array(nextPageId).fill(0 /* NativeHistoryStackSpecialValues.ServerSideNavigationStub */);
|
|
142
145
|
this.nativeHistoryStack[this.nativeHistoryStack.length - 1] = title;
|
|
143
146
|
}
|
|
@@ -168,11 +171,38 @@ class NativeNavigationAndTitleService {
|
|
|
168
171
|
*/
|
|
169
172
|
prepareExternalLinkBeforeOpen(url) {
|
|
170
173
|
const currentPageId = this.nativeHistoryStack.length;
|
|
171
|
-
const
|
|
172
|
-
const
|
|
174
|
+
const modifiedUrl = new URL(url);
|
|
175
|
+
const { originalWebviewParams, appVersion } = this.nativeParamsService;
|
|
176
|
+
if (originalWebviewParams) {
|
|
177
|
+
const originalWebviewSearchParams = new URLSearchParams(originalWebviewParams);
|
|
178
|
+
originalWebviewSearchParams.forEach((value, key) => {
|
|
179
|
+
modifiedUrl.searchParams.set(key, value);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
// Явно добавляем query-параметр `device_app_version` используемый NA на iOS, чтобы он был и в Android окружении.
|
|
183
|
+
// Таким образом гарантируется, что версию приложения будет видеть следующее WA
|
|
184
|
+
// (заголовок `app-version` может отсутствовать при server-side переходах).
|
|
185
|
+
modifiedUrl.searchParams.set(query_and_headers_keys_1.QUERY_NATIVE_IOS_APPVERSION, appVersion);
|
|
173
186
|
modifiedUrl.searchParams.set(query_and_headers_keys_1.QUERY_B2N_NEXT_PAGEID, (currentPageId + 1).toString());
|
|
174
187
|
return modifiedUrl;
|
|
175
188
|
}
|
|
189
|
+
static shouldInitializeFromNextPageId(nextPageId) {
|
|
190
|
+
if (!NativeNavigationAndTitleService.hasSavedHistoryStack()) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const serializedNativeHistoryStack = sessionStorage.getItem(query_and_headers_keys_1.SS_KEY_BRIDGE_TO_NATIVE_HISTORY_STACK);
|
|
195
|
+
if (!serializedNativeHistoryStack) {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
return (nextPageId ===
|
|
199
|
+
JSON.parse(serializedNativeHistoryStack).length +
|
|
200
|
+
1);
|
|
201
|
+
}
|
|
202
|
+
catch (_a) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
176
206
|
/**
|
|
177
207
|
* Читает сохраннённый в sessionStorage `nativeHistoryStack`,
|
|
178
208
|
* снова сохраняет его в sessionStorage, уменьшая список на одну запись,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfalab/bridge-to-native",
|
|
3
|
-
"version": "1.2
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Утилита для удобной работы веб приложения внутри нативного приложения и коммуникации с ним.",
|
|
6
6
|
"engines": {
|
|
@@ -32,14 +32,13 @@
|
|
|
32
32
|
"build:clean": "shx rm -rf .publish",
|
|
33
33
|
"build:copy-package-json": "shx cp package.json .publish/package.json",
|
|
34
34
|
"build:ts": "tsc --build",
|
|
35
|
-
"changelog": "bash bin/fill-changelog-file-and-notify-github.sh",
|
|
36
35
|
"format": "arui-presets-lint format",
|
|
37
36
|
"format:check": "arui-presets-lint format:check",
|
|
38
37
|
"lint": "yarn lint:scripts && yarn format:check",
|
|
39
38
|
"lint:fix": "yarn lint:scripts --fix && yarn format",
|
|
40
39
|
"lint:scripts": "arui-presets-lint scripts",
|
|
41
40
|
"pub": "npm publish .publish --userconfig \"../.npmrc\" --tag \"$TAG\"",
|
|
42
|
-
"release": "yarn build &&
|
|
41
|
+
"release": "yarn build && yarn build:copy-package-json && yarn pub",
|
|
43
42
|
"test": "arui-scripts test --silent --collect-coverage"
|
|
44
43
|
},
|
|
45
44
|
"devDependencies": {
|
|
@@ -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
|
+
}
|