@alfalab/bridge-to-native 1.1.0 → 1.2.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.
@@ -46,6 +46,10 @@ export declare class BridgeToNative {
46
46
  * Тема (светлая/тёмная), в котором работает NA.
47
47
  */
48
48
  get theme(): import("./types").Theme;
49
+ /**
50
+ * Время старта открытия WV экрана в формате timestamp. Значение никак не меняется в процессе всей жизни WV экрана.
51
+ */
52
+ get webviewLaunchTime(): number | null;
49
53
  /**
50
54
  * Метод, проверяющий, можно ли использовать ли указанную функциональность
51
55
  * в текущей версии NA.
@@ -62,6 +62,12 @@ class BridgeToNative {
62
62
  get theme() {
63
63
  return this.nativeParamsService.theme;
64
64
  }
65
+ /**
66
+ * Время старта открытия WV экрана в формате timestamp. Значение никак не меняется в процессе всей жизни WV экрана.
67
+ */
68
+ get webviewLaunchTime() {
69
+ return this.nativeParamsService.webviewLaunchTime;
70
+ }
65
71
  /**
66
72
  * Метод, проверяющий, можно ли использовать ли указанную функциональность
67
73
  * в текущей версии NA.
@@ -15,6 +15,7 @@ export declare class NativeParamsService {
15
15
  originalWebviewParams: string | null;
16
16
  theme: Theme;
17
17
  title: string;
18
+ webviewLaunchTime: number | null;
18
19
  constructor(logError?: LogError | undefined);
19
20
  canUseNativeFeature(feature: NativeFeatureKey): boolean;
20
21
  isCurrentVersionHigherOrEqual(versionToCompare: string): boolean;
@@ -21,6 +21,7 @@ class NativeParamsService {
21
21
  this.originalWebviewParams = (nativeParams === null || nativeParams === void 0 ? void 0 : nativeParams.originalWebviewParams) || null;
22
22
  this.theme = (nativeParams === null || nativeParams === void 0 ? void 0 : nativeParams.theme) === 'dark' ? 'dark' : 'light';
23
23
  this.title = (nativeParams === null || nativeParams === void 0 ? void 0 : nativeParams.title) || '';
24
+ this.webviewLaunchTime = (nativeParams === null || nativeParams === void 0 ? void 0 : nativeParams.webviewLaunchTime) || null;
24
25
  }
25
26
  canUseNativeFeature(feature) {
26
27
  const { fromVersion } = constants_1.NATIVE_FEATURES_FROM_VERSION[this.environment][feature];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfalab/bridge-to-native",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "license": "MIT",
5
5
  "description": "Утилита для удобной работы веб приложения внутри нативного приложения и коммуникации с ним.",
6
6
  "engines": {
@@ -3,6 +3,7 @@ export declare const HEADER_KEY_USER_AGENT = "user-agent";
3
3
  export declare const COOKIE_KEY_BRIDGE_TO_NATIVE_DATA = "bridgeToNativeData";
4
4
  export declare const COOKIE_KEY_BRIDGE_TO_NATIVE_RELOAD = "bridgeToNativeReload";
5
5
  export declare const HEADER_KEY_NATIVE_APPVERSION = "app-version";
6
+ export declare const HEADER_KEY_WV_LAUNCH_TIME = "webview-launch-time";
6
7
  export declare const QUERY_B2N_NEXT_PAGEID = "b2n-next-page-id";
7
8
  export declare const QUERY_B2N_TITLE = "b2n-title";
8
9
  export declare const QUERY_B2N_TITLE_DEPRECATED = "title";
@@ -3,7 +3,7 @@
3
3
  * Ключи стандартных заголовков.
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.SS_KEY_BRIDGE_TO_NATIVE_HISTORY_STACK = exports.QUERY_NATIVE_THEME = exports.QUERY_NATIVE_IOS_APPVERSION = exports.QUERY_NATIVE_IOS_APPID = exports.QUERY_B2N_TITLE_DEPRECATED = exports.QUERY_B2N_TITLE = exports.QUERY_B2N_NEXT_PAGEID = exports.HEADER_KEY_NATIVE_APPVERSION = exports.COOKIE_KEY_BRIDGE_TO_NATIVE_RELOAD = exports.COOKIE_KEY_BRIDGE_TO_NATIVE_DATA = exports.HEADER_KEY_USER_AGENT = exports.HEADER_KEY_COOKIE = void 0;
6
+ exports.SS_KEY_BRIDGE_TO_NATIVE_HISTORY_STACK = exports.QUERY_NATIVE_THEME = exports.QUERY_NATIVE_IOS_APPVERSION = exports.QUERY_NATIVE_IOS_APPID = exports.QUERY_B2N_TITLE_DEPRECATED = exports.QUERY_B2N_TITLE = exports.QUERY_B2N_NEXT_PAGEID = exports.HEADER_KEY_WV_LAUNCH_TIME = exports.HEADER_KEY_NATIVE_APPVERSION = exports.COOKIE_KEY_BRIDGE_TO_NATIVE_RELOAD = exports.COOKIE_KEY_BRIDGE_TO_NATIVE_DATA = exports.HEADER_KEY_USER_AGENT = exports.HEADER_KEY_COOKIE = void 0;
7
7
  exports.HEADER_KEY_COOKIE = 'cookie';
8
8
  exports.HEADER_KEY_USER_AGENT = 'user-agent';
9
9
  /*
@@ -21,6 +21,8 @@ exports.COOKIE_KEY_BRIDGE_TO_NATIVE_RELOAD = 'bridgeToNativeReload';
21
21
  // * Есть подозрение на некоторую нестабильность — возможно делает это
22
22
  // только для первого документа в вебвью-сессии. Исследовать.
23
23
  exports.HEADER_KEY_NATIVE_APPVERSION = 'app-version';
24
+ // Ключ заголовка, в котором передается время старта открытия WV экрана в формате timestamp
25
+ exports.HEADER_KEY_WV_LAUNCH_TIME = 'webview-launch-time';
24
26
  // Флаг, предписывающий клиентскому коду B2N начать связь
25
27
  // с нативным приложением с указанного в этом параметре id, а не с 1.
26
28
  // Необходимо для server-side навигации.
@@ -1,4 +1,3 @@
1
- import { type NativeParams } from '../types';
2
1
  import { type UniversalRequest } from './types';
3
2
  /**
4
3
  * Парсит запрос, доставая из него данные о нативном приложении,
@@ -11,4 +10,4 @@ import { type UniversalRequest } from './types';
11
10
  * Нужно передать функцию, которая средствами используемого веб-сервера добавит заголовок в ответ.
12
11
  * b2native с её помощью добавит `Set-Cookie` заголовок с некоторыми данными для своего клиентского кода.
13
12
  */
14
- export declare function prepareNativeAppDetailsForClient(request: UniversalRequest, setResponseHeader: (headerKey: string, headerValue: string) => void): Partial<NativeParams>;
13
+ export declare function prepareNativeAppDetailsForClient(request: UniversalRequest, setResponseHeader: (headerKey: string, headerValue: string) => void): any;
@@ -17,26 +17,30 @@ const utils_1 = require("./utils");
17
17
  * b2native с её помощью добавит `Set-Cookie` заголовок с некоторыми данными для своего клиентского кода.
18
18
  */
19
19
  function prepareNativeAppDetailsForClient(request, setResponseHeader) {
20
- // Проверяем наличие куки bridgeToNativeData в запросе. Если куки нет — устанавливаем её.
21
- // Также сверяем тему из query-параметров с темой, сохранённой в куке, потому что
22
- // при повторном открытии WebView сессионная кука может сохраниться, и если тема приложения
23
- // изменилась между сессиями, WebView и нативное приложение будут не синхронизированы.
24
- // В таком случае обновляем куку актуальными данными из query-параметров
25
- // Для случая когда передан флаг `reload` куку перезаписывать не нужно, потому что:
20
+ // Поскольку вебвью модули имеют особенность сохранять сессионную куку подолгу,
21
+ // даже после перезагрузки устройства или обновления приложения/ОС, ее значение
22
+ // актуализируется при каждом запросе на сервер. Исключением является вызов `reload()`.
23
+ // В этом случае функция возвращает объект с параметрами, полученными из
24
+ // ранее сохраненной куки, но перезаписываться кука не будет, потому что:
26
25
  // 1) Данных NA в запросе с большой вероятностью не будет;
27
26
  // 2) клиентская сторона сохранит всё, что нужно в SessionStorage
28
27
  const cookieHeader = (0, utils_1.getHeaderValue)(request, query_and_headers_keys_1.HEADER_KEY_COOKIE);
29
- const nativeParams = parseRequest(request);
30
28
  const hasReloadFlag = cookieHeader?.includes(`${query_and_headers_keys_1.COOKIE_KEY_BRIDGE_TO_NATIVE_RELOAD}=true`);
31
- const shouldUpdateCookie = !(0, utils_1.hasBridgeToNativeDataCookie)(cookieHeader) || !isSameTheme(request, cookieHeader);
32
- if (!hasReloadFlag && shouldUpdateCookie) {
33
- const serializedNativeParams = encodeURIComponent(JSON.stringify(nativeParams));
34
- setResponseHeader('Set-Cookie', `${query_and_headers_keys_1.COOKIE_KEY_BRIDGE_TO_NATIVE_DATA}=${serializedNativeParams}; Path=/`);
35
- return nativeParams;
36
- }
37
29
  if (hasReloadFlag) {
38
30
  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
+ }
39
+ }
39
40
  }
41
+ const nativeParams = parseRequest(request);
42
+ const serializedNativeParams = encodeURIComponent(JSON.stringify(nativeParams));
43
+ setResponseHeader('Set-Cookie', `${query_and_headers_keys_1.COOKIE_KEY_BRIDGE_TO_NATIVE_DATA}=${serializedNativeParams}; Path=/`);
40
44
  return nativeParams;
41
45
  }
42
46
  function parseRequest(request) {
@@ -54,6 +58,7 @@ function parseRequest(request) {
54
58
  query_and_headers_keys_1.QUERY_B2N_TITLE_DEPRECATED,
55
59
  ]);
56
60
  const appVersionFromHeaders = (0, utils_1.getHeaderValue)(request, query_and_headers_keys_1.HEADER_KEY_NATIVE_APPVERSION);
61
+ const webviewLaunchTime = (0, utils_1.parseHeaderTimestamp)((0, utils_1.getHeaderValue)(request, query_and_headers_keys_1.HEADER_KEY_WV_LAUNCH_TIME));
57
62
  const nativeParams = {
58
63
  originalWebviewParams,
59
64
  theme: theme || 'light',
@@ -84,20 +89,8 @@ function parseRequest(request) {
84
89
  else if (typeof deprecatedTitle === 'string') {
85
90
  nativeParams.title = deprecatedTitle;
86
91
  }
87
- return nativeParams;
88
- }
89
- // Возвращает результат сравнения темы полученной, из query-параметров
90
- // с темой, полученной из cookie
91
- function isSameTheme(request, cookies) {
92
- if (!cookies)
93
- return false;
94
- const [themeFromQuery] = (0, utils_1.getQueryValues)(request, [query_and_headers_keys_1.QUERY_NATIVE_THEME]);
95
- const parsedCookies = (0, utils_1.parseCookies)(cookies);
96
- const bridgeCookie = parsedCookies[query_and_headers_keys_1.COOKIE_KEY_BRIDGE_TO_NATIVE_DATA];
97
- try {
98
- return JSON.parse(bridgeCookie || '{}').theme === themeFromQuery;
99
- }
100
- catch (_) {
101
- return false;
92
+ if (webviewLaunchTime) {
93
+ nativeParams.webviewLaunchTime = webviewLaunchTime;
102
94
  }
95
+ return nativeParams;
103
96
  }
package/server/utils.d.ts CHANGED
@@ -22,4 +22,15 @@ export declare function getQueryValues(request: UniversalRequest, queryKeys: str
22
22
  * @param request Объект запроса (Request или IncomingMessage).
23
23
  */
24
24
  export declare function hasBridgeToNativeDataCookie(cookieHeader: string | null): boolean;
25
- export declare function parseCookies(cookieHeader: string): Record<string, string>;
25
+ /**
26
+ * Преобразует значение из заголовка в timestamp
27
+ *
28
+ * @param headerValue Значение заголовка
29
+ */
30
+ export declare function parseHeaderTimestamp(headerValue: string | number | null): number | null;
31
+ /**
32
+ * Возвращает значение для нужной куки
33
+ *
34
+ * @param cookieName Имя куки
35
+ */
36
+ export declare const getBridgeToNativeDataCookie: (cookieName: string | null) => string | undefined;
package/server/utils.js CHANGED
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getBridgeToNativeDataCookie = void 0;
3
4
  exports.getHeaderValue = getHeaderValue;
4
5
  exports.getQueryValues = getQueryValues;
5
6
  exports.hasBridgeToNativeDataCookie = hasBridgeToNativeDataCookie;
6
- exports.parseCookies = parseCookies;
7
+ exports.parseHeaderTimestamp = parseHeaderTimestamp;
8
+ const query_and_headers_keys_1 = require("../query-and-headers-keys");
7
9
  const regexp_patterns_1 = require("./regexp-patterns");
8
10
  /**
9
11
  * На основе объекта запроса любого типа возвращает
@@ -41,13 +43,33 @@ function getQueryValues(request, queryKeys) {
41
43
  function hasBridgeToNativeDataCookie(cookieHeader) {
42
44
  return Boolean(cookieHeader && regexp_patterns_1.bridgeToNativeDataCookieExistencePattern.test(cookieHeader));
43
45
  }
44
- function parseCookies(cookieHeader) {
45
- return cookieHeader.split(';').reduce((acc, part) => {
46
- const [key, ...rest] = part.split('=');
47
- const trimmedKey = key.trim();
48
- if (!trimmedKey)
49
- return acc;
50
- acc[trimmedKey] = decodeURIComponent(rest.join('=').trim());
51
- return acc;
52
- }, {});
46
+ /**
47
+ * Преобразует значение из заголовка в timestamp
48
+ *
49
+ * @param headerValue Значение заголовка
50
+ */
51
+ function parseHeaderTimestamp(headerValue) {
52
+ if (!headerValue || (typeof headerValue === 'string' && !headerValue?.trim()))
53
+ return null;
54
+ const timestamp = Number(headerValue);
55
+ return Number.isFinite(timestamp) ? timestamp : null;
53
56
  }
57
+ /**
58
+ * Возвращает значение для нужной куки
59
+ *
60
+ * @param cookieName Имя куки
61
+ */
62
+ const getBridgeToNativeDataCookie = (cookieName) => {
63
+ if (!cookieName) {
64
+ return undefined;
65
+ }
66
+ const cookies = cookieName.split(';');
67
+ for (const cookie of cookies) {
68
+ const [key, value] = cookie.trim().split('=');
69
+ if (key === query_and_headers_keys_1.COOKIE_KEY_BRIDGE_TO_NATIVE_DATA) {
70
+ return value;
71
+ }
72
+ }
73
+ return undefined;
74
+ };
75
+ exports.getBridgeToNativeDataCookie = getBridgeToNativeDataCookie;
package/types.d.ts CHANGED
@@ -5,4 +5,5 @@ export type NativeParams = {
5
5
  theme: string;
6
6
  nextPageId: number | null;
7
7
  originalWebviewParams: string;
8
+ webviewLaunchTime?: number;
8
9
  };