@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.
@@ -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 divider = new URL(url).searchParams.toString() ? '&' : '?';
172
- const modifiedUrl = new URL(`${url}${divider}${this.nativeParamsService.originalWebviewParams}`);
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.1",
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 && npm version \"$VERSION\" --no-git-tag-version && yarn build:copy-package-json && yarn pub",
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): any;
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
- const cookieData = (0, utils_1.getBridgeToNativeDataCookie)(cookieHeader);
32
- if (cookieData) {
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 = parseRequest(request);
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 parseRequest(request) {
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 cookieName Имя куки
35
+ * @param cookieHeader Имя куки
35
36
  */
36
- export declare const getBridgeToNativeDataCookie: (cookieName: string | null) => string | undefined;
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 cookieName Имя куки
61
+ * @param cookieHeader Имя куки
61
62
  */
62
- const getBridgeToNativeDataCookie = (cookieName) => {
63
- if (!cookieName) {
63
+ const getBridgeToNativeDataCookie = (cookieHeader) => {
64
+ if (!cookieHeader) {
64
65
  return undefined;
65
66
  }
66
- const cookies = cookieName.split(';');
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
+ }