@alfalab/bridge-to-native 0.0.13 → 0.1.0

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.
@@ -0,0 +1,63 @@
1
+ import { NativeFallbacks } from './native-fallbacks';
2
+ import { NativeNavigationAndTitle } from './native-navigation-and-title';
3
+ import type { Environment, HandleRedirect, NativeFeatureKey, NativeParams, Theme } from './types';
4
+ /**
5
+ * Этот класс — абстракция для связи веб приложения с нативом и предназначен ТОЛЬКО
6
+ * для использования в вебвью окружении.
7
+ */
8
+ export declare class BridgeToNative {
9
+ readonly AndroidBridge: {
10
+ setPageSettings: (params: string) => void;
11
+ } | undefined;
12
+ readonly environment: Environment;
13
+ readonly nativeFallbacks: NativeFallbacks;
14
+ private nextPageId;
15
+ private readonly _blankPagePath;
16
+ private readonly _handleRedirect;
17
+ constructor(handleRedirect: HandleRedirect, blankPagePath: string, nativeParams?: NativeParams);
18
+ private _nativeNavigationAndTitle;
19
+ get nativeNavigationAndTitle(): NativeNavigationAndTitle;
20
+ private _originalWebviewParams;
21
+ get originalWebviewParams(): string;
22
+ private _appVersion;
23
+ get appVersion(): string;
24
+ private _iosAppId?;
25
+ get iosAppId(): string | undefined;
26
+ private _theme;
27
+ get theme(): Theme;
28
+ /**
29
+ * Метод, проверяющий, можно ли использовать нативную функциональность в текущей версии приложения.
30
+ *
31
+ * @param feature Название функциональности, которую нужно проверить.
32
+ */
33
+ canUseNativeFeature(feature: NativeFeatureKey): boolean;
34
+ /**
35
+ * Метод, отправляющий сигнал нативу, что нужно закрыть текущее вебвью.
36
+ */
37
+ closeWebview(): void;
38
+ /**
39
+ * Сравнивает текущую версию приложения с переданной.
40
+ *
41
+ * @param versionToCompare Версия, с которой нужно сравнить текущую.
42
+ * @returns `true` – текущая версия больше или равняется переданной,
43
+ * `false` – текущая версия ниже.
44
+ */
45
+ isCurrentVersionHigherOrEqual(versionToCompare: string): boolean;
46
+ checkAndroidAllowOpenInNewWebview(): boolean;
47
+ /**
48
+ * Сохраняет текущее состояние BridgeToNative в sessionStorage.
49
+ * Так же сохраняет текущее состояние nativeNavigationAndTitle.
50
+ */
51
+ private saveCurrentState;
52
+ /**
53
+ * Возвращает схему приложения в iOS окружении, на основе версии.
54
+ *
55
+ * @param knownIosAppId Тип iOS приложения, если он известен.
56
+ * @returns Тип приложения, `undefined` для Android окружения.
57
+ */
58
+ private getIosAppId;
59
+ /**
60
+ * Восстанавливает свое предыдущее состояние из sessionStorage
61
+ */
62
+ private restorePreviousState;
63
+ }
@@ -0,0 +1,154 @@
1
+ /* eslint-disable no-underscore-dangle */
2
+ import { CLOSE_WEBVIEW_SEARCH_KEY, CLOSE_WEBVIEW_SEARCH_VALUE, nativeFeaturesFromVersion, PREVIOUS_B2N_STATE_STORAGE_KEY, START_VERSION_ANDROID_ALLOW_OPEN_NEW_WEBVIEW, versionToIosAppId, } from './constants';
3
+ import { NativeFallbacks } from './native-fallbacks';
4
+ import { NativeNavigationAndTitle } from './native-navigation-and-title';
5
+ import { isValidVersionFormat } from './utils';
6
+ /**
7
+ * Этот класс — абстракция для связи веб приложения с нативом и предназначен ТОЛЬКО
8
+ * для использования в вебвью окружении.
9
+ */
10
+ export class BridgeToNative {
11
+ // Webview, запущенное в Android окружении имеет объект `Android` в window.
12
+ AndroidBridge = window.Android;
13
+ environment = this.AndroidBridge ? 'android' : 'ios';
14
+ nativeFallbacks;
15
+ nextPageId;
16
+ _blankPagePath;
17
+ _handleRedirect;
18
+ constructor(handleRedirect, blankPagePath, nativeParams) {
19
+ const previousState = !!sessionStorage.getItem(PREVIOUS_B2N_STATE_STORAGE_KEY);
20
+ if (previousState) {
21
+ this._handleRedirect = handleRedirect;
22
+ this.restorePreviousState();
23
+ this.nativeFallbacks = new NativeFallbacks(this);
24
+ this._blankPagePath = blankPagePath;
25
+ return;
26
+ }
27
+ this._appVersion =
28
+ nativeParams && isValidVersionFormat(nativeParams?.appVersion)
29
+ ? nativeParams.appVersion
30
+ : '0.0.0';
31
+ this._iosAppId = this.getIosAppId(nativeParams?.iosAppId);
32
+ this._theme = nativeParams?.theme === 'dark' ? 'dark' : 'light';
33
+ this._originalWebviewParams = nativeParams?.originalWebviewParams || '';
34
+ this._nativeNavigationAndTitle = new NativeNavigationAndTitle(this, nativeParams ? nativeParams.nextPageId : null, nativeParams?.title, handleRedirect);
35
+ this._handleRedirect = handleRedirect;
36
+ this.nextPageId = nativeParams ? nativeParams.nextPageId : null;
37
+ this.nativeFallbacks = new NativeFallbacks(this);
38
+ this._blankPagePath = blankPagePath;
39
+ }
40
+ _nativeNavigationAndTitle;
41
+ get nativeNavigationAndTitle() {
42
+ return this._nativeNavigationAndTitle;
43
+ }
44
+ _originalWebviewParams;
45
+ get originalWebviewParams() {
46
+ return this._originalWebviewParams;
47
+ }
48
+ // В формате `x.x.x`.
49
+ _appVersion;
50
+ get appVersion() {
51
+ return this._appVersion;
52
+ }
53
+ // Необходимо для формирования диплинка.
54
+ _iosAppId;
55
+ get iosAppId() {
56
+ return this._iosAppId;
57
+ }
58
+ _theme;
59
+ get theme() {
60
+ return this._theme;
61
+ }
62
+ /**
63
+ * Метод, проверяющий, можно ли использовать нативную функциональность в текущей версии приложения.
64
+ *
65
+ * @param feature Название функциональности, которую нужно проверить.
66
+ */
67
+ canUseNativeFeature(feature) {
68
+ const { fromVersion } = nativeFeaturesFromVersion[this.environment][feature];
69
+ return this.isCurrentVersionHigherOrEqual(fromVersion);
70
+ }
71
+ /**
72
+ * Метод, отправляющий сигнал нативу, что нужно закрыть текущее вебвью.
73
+ */
74
+ // eslint-disable-next-line class-methods-use-this
75
+ closeWebview() {
76
+ const originalPageUrl = new URL(window.location.href);
77
+ originalPageUrl.searchParams.set(CLOSE_WEBVIEW_SEARCH_KEY, CLOSE_WEBVIEW_SEARCH_VALUE);
78
+ window.location.href = originalPageUrl.toString();
79
+ }
80
+ /**
81
+ * Сравнивает текущую версию приложения с переданной.
82
+ *
83
+ * @param versionToCompare Версия, с которой нужно сравнить текущую.
84
+ * @returns `true` – текущая версия больше или равняется переданной,
85
+ * `false` – текущая версия ниже.
86
+ */
87
+ isCurrentVersionHigherOrEqual(versionToCompare) {
88
+ if (!isValidVersionFormat(versionToCompare)) {
89
+ return false;
90
+ }
91
+ const matchPattern = /(\d+)\.(\d+)\.(\d+)/;
92
+ const [, ...appVersionComponents] = this._appVersion.match(matchPattern); // Формат версии проверен в конструкторе, можно смело убирать `null` из типа.
93
+ const [, ...versionToCompareComponents] = versionToCompare.match(matchPattern);
94
+ for (let i = 0; i < appVersionComponents.length; i++) {
95
+ if (appVersionComponents[i] !== versionToCompareComponents[i]) {
96
+ return appVersionComponents[i] >= versionToCompareComponents[i];
97
+ }
98
+ }
99
+ return true;
100
+ }
101
+ checkAndroidAllowOpenInNewWebview() {
102
+ const comparisonResult = this.isCurrentVersionHigherOrEqual(START_VERSION_ANDROID_ALLOW_OPEN_NEW_WEBVIEW);
103
+ return this.environment === 'android' && comparisonResult;
104
+ }
105
+ /**
106
+ * Сохраняет текущее состояние BridgeToNative в sessionStorage.
107
+ * Так же сохраняет текущее состояние nativeNavigationAndTitle.
108
+ */
109
+ saveCurrentState() {
110
+ // В nativeNavigationAndTitle этот метод отмечен модификатором доступа private дабы не торчал наружу, но тут его нужно вызвать
111
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
112
+ // @ts-ignore
113
+ this._nativeNavigationAndTitle.saveCurrentState();
114
+ const currentState = {
115
+ appVersion: this._appVersion,
116
+ theme: this._theme,
117
+ nextPageId: this.nextPageId,
118
+ originalWebviewParams: this._originalWebviewParams || '',
119
+ iosAppId: this._iosAppId,
120
+ };
121
+ sessionStorage.setItem(PREVIOUS_B2N_STATE_STORAGE_KEY, JSON.stringify(currentState));
122
+ }
123
+ /**
124
+ * Возвращает схему приложения в iOS окружении, на основе версии.
125
+ *
126
+ * @param knownIosAppId Тип iOS приложения, если он известен.
127
+ * @returns Тип приложения, `undefined` для Android окружения.
128
+ */
129
+ getIosAppId(knownIosAppId) {
130
+ if (this.environment !== 'ios') {
131
+ return undefined;
132
+ }
133
+ if (knownIosAppId) {
134
+ return knownIosAppId;
135
+ }
136
+ const keys = Object.keys(versionToIosAppId);
137
+ const rightKey = [...keys].reverse().find((version) => this.isCurrentVersionHigherOrEqual(version)) ||
138
+ keys[0];
139
+ return atob(versionToIosAppId[rightKey]);
140
+ }
141
+ /**
142
+ * Восстанавливает свое предыдущее состояние из sessionStorage
143
+ */
144
+ restorePreviousState() {
145
+ const previousState = JSON.parse(sessionStorage.getItem(PREVIOUS_B2N_STATE_STORAGE_KEY) || '');
146
+ this._appVersion = previousState.appVersion;
147
+ this._iosAppId = previousState.iosAppId;
148
+ this._theme = previousState.theme;
149
+ this._originalWebviewParams = previousState.originalWebviewParams;
150
+ this.nextPageId = previousState.nextPageId;
151
+ this._nativeNavigationAndTitle = new NativeNavigationAndTitle(this, previousState.nextPageId, '', this._handleRedirect);
152
+ sessionStorage.removeItem(PREVIOUS_B2N_STATE_STORAGE_KEY);
153
+ }
154
+ }
@@ -0,0 +1,15 @@
1
+ import { NativeFeaturesFromVersion } from './types';
2
+ export declare const START_VERSION_ANDROID_ALLOW_OPEN_NEW_WEBVIEW = "10.35.0";
3
+ export declare const ANDROID_APP_ID = "YWxmYWJhbms=";
4
+ export declare const CLOSE_WEBVIEW_SEARCH_KEY = "closeWebView";
5
+ export declare const CLOSE_WEBVIEW_SEARCH_VALUE = "true";
6
+ export declare const PREVIOUS_B2N_STATE_STORAGE_KEY = "previousBridgeToNativeState";
7
+ export declare const PREVIOUS_NATIVE_NAVIGATION_AND_TITLE_STATE_STORAGE_KEY = "previousNativeNavigationAndTitleState";
8
+ export declare const versionToIosAppId: {
9
+ readonly '0.0.0': "YWxmYWJhbms=";
10
+ readonly '12.22.0': "YWNvbmNpZXJnZQ==";
11
+ readonly '12.26.0': "a2l0dHljYXNo";
12
+ readonly '12.31.0': "YXdlYXNzaXN0";
13
+ };
14
+ export declare const nativeFeaturesFromVersion: NativeFeaturesFromVersion;
15
+ export declare const DEEP_LINK_PATTERN: RegExp;
@@ -0,0 +1,27 @@
1
+ export const START_VERSION_ANDROID_ALLOW_OPEN_NEW_WEBVIEW = '10.35.0';
2
+ export const ANDROID_APP_ID = 'YWxmYWJhbms=';
3
+ export const CLOSE_WEBVIEW_SEARCH_KEY = 'closeWebView';
4
+ export const CLOSE_WEBVIEW_SEARCH_VALUE = 'true';
5
+ export const PREVIOUS_B2N_STATE_STORAGE_KEY = 'previousBridgeToNativeState';
6
+ export const PREVIOUS_NATIVE_NAVIGATION_AND_TITLE_STATE_STORAGE_KEY = 'previousNativeNavigationAndTitleState';
7
+ export const versionToIosAppId = {
8
+ '0.0.0': 'YWxmYWJhbms=',
9
+ '12.22.0': 'YWNvbmNpZXJnZQ==',
10
+ '12.26.0': 'a2l0dHljYXNo',
11
+ '12.31.0': 'YXdlYXNzaXN0',
12
+ };
13
+ export const nativeFeaturesFromVersion = {
14
+ android: {
15
+ linksInBrowser: {
16
+ fromVersion: '11.71.0',
17
+ },
18
+ geolocation: { fromVersion: '11.71.0' },
19
+ },
20
+ ios: {
21
+ linksInBrowser: {
22
+ fromVersion: '13.3.0',
23
+ },
24
+ geolocation: { fromVersion: '0.0.0' },
25
+ },
26
+ };
27
+ export const DEEP_LINK_PATTERN = /^(\/|\x61\x6c\x66\x61\x62\x61\x6e\x6b:\/{3}dashboard\/|\x61\x6c\x66\x61\x62\x61\x6e\x6b:\/{3}|\x61\x6c\x66\x61\x62\x61\x6e\x6b:\/{2}|https:\/{2}\x6f\x6e\x6c\x69\x6e\x65\x2e\x61\x6c\x66\x61\x62\x61\x6e\x6b\x2e\x72\x75\/)/;
package/esm/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { BridgeToNative } from './bridge-to-native';
2
+ export { NativeParams, Theme, Environment, NativeFeatureKey, PdfType } from './types';
package/esm/index.js ADDED
@@ -0,0 +1 @@
1
+ export { BridgeToNative } from './bridge-to-native';
@@ -0,0 +1,64 @@
1
+ import { ExternalNavigationOptions, PdfType } from './types';
2
+ import type { BridgeToNative } from './bridge-to-native';
3
+ /**
4
+ * Класс содержит реализацию обходных путей для веб-фич, которые не работают в нативном-вебвью.
5
+ */
6
+ export declare class NativeFallbacks {
7
+ private b2n;
8
+ constructor(b2n: BridgeToNative);
9
+ /**
10
+ * Метод, возвращающий пропсы для ссылок, ведущих на ВНЕШНИЙ ресурс. Которые просто
11
+ * нужно «подмешать» к ссылке в JSX:
12
+ *
13
+ * ```
14
+ * <a {...bridgeToNative.nativeFallbacks.getExternalLinkProps('https://ya.ru')}>Link to external feature</a>
15
+ * ```
16
+ * Либо просто достать интересующие поля - onClick или href
17
+ * ```
18
+ * const {onClick, href} = bridgeToNative.nativeFallbacks.getExternalLinkProps(url, clickHandler)
19
+ * document.querySelector('.myLink').onclick = onClick;
20
+ * <a {...bridgeToNative.nativeFallbacks.getExternalLinkProps('https://ya.ru')}>Link to external feature</a>
21
+ * ```
22
+ * В разных OS и разных версиях приложения, открытие ресурса будет работать по-разному:
23
+ *
24
+ * - Если текущая версия приложения может открыть ссылку в браузере и не задан параметр `forceOpenInWebview`,
25
+ * обогащаем URL специальным query-параметром (`target=_blank` в приложении не работает).
26
+ * - Если это iOS, меняем URL на диплинк, который откроет ссылку в новом вебвью, поверх текущего.
27
+ * К первому-вебвью, пользователь вернётся, когда закроет второе вебвью с внешним ресурсом.
28
+ * - В старых приложениях на Андроид – URL не меняем, но добавляем `onClick` для сбрасывания синхронизации
29
+ * навигации с приложением. Это «фолбэк-сценарий» с плохим UX (сайт полностью выпадает из истории), но другого способа нет.
30
+ *
31
+ * @param link Строка - валидный урл.
32
+ * @param options - опции
33
+ * @param options.forceOpenInWebview Boolean - по умолчанию = false, если передать true,
34
+ * все ссылки будут открываться в рамках webview, иначе открытие по возможности будет происходить в браузере.
35
+ * @param options.onClick Дополнительный обработчик на клик, например, для отправки метрики.
36
+ * Внимание! Не факт, что в «фолбэк-сценарии» асинхронная операция будет выполнена (метрика отправлена)!
37
+ * @returns Пропсы для ссылки в вебвью окружении.
38
+ */
39
+ getExternalLinkProps(link: string, options?: ExternalNavigationOptions): {
40
+ href: string;
41
+ onClick: (() => void) | undefined;
42
+ };
43
+ /**
44
+ * Метод для открытия PDF в нативном вьювере.
45
+ *
46
+ * Есть нюансы с версиями приложения, OS устройства.
47
+ * Надо тестировать по моде статистики.
48
+ *
49
+ * @param url ссылка на pdf
50
+ * @param type тип pdf ссылки
51
+ * @param title название pdf файла
52
+ */
53
+ openPdf(url: string, type?: PdfType, title?: string): void;
54
+ /**
55
+ * Метод, для перехода на ВНЕШНИЙ ресурс.
56
+ *
57
+ * См. описание в `getExternalLinkProps`, чтобы узнать, как выбирается способ для перехода.
58
+ *
59
+ * @param link Строка - валидный урл.
60
+ * @param forceOpenInWebview Boolean - по умолчанию = false, если передать true,
61
+ * все ссылки будут открываться в рамках webview, иначе открытие по возможности будет происходить в браузере.
62
+ */
63
+ visitExternalResource(link: string, forceOpenInWebview?: boolean): void;
64
+ }
@@ -0,0 +1,119 @@
1
+ import { getAppId, getUrlInstance } from './utils';
2
+ /**
3
+ * Класс содержит реализацию обходных путей для веб-фич, которые не работают в нативном-вебвью.
4
+ */
5
+ export class NativeFallbacks {
6
+ b2n;
7
+ constructor(b2n) {
8
+ this.b2n = b2n;
9
+ }
10
+ /**
11
+ * Метод, возвращающий пропсы для ссылок, ведущих на ВНЕШНИЙ ресурс. Которые просто
12
+ * нужно «подмешать» к ссылке в JSX:
13
+ *
14
+ * ```
15
+ * <a {...bridgeToNative.nativeFallbacks.getExternalLinkProps('https://ya.ru')}>Link to external feature</a>
16
+ * ```
17
+ * Либо просто достать интересующие поля - onClick или href
18
+ * ```
19
+ * const {onClick, href} = bridgeToNative.nativeFallbacks.getExternalLinkProps(url, clickHandler)
20
+ * document.querySelector('.myLink').onclick = onClick;
21
+ * <a {...bridgeToNative.nativeFallbacks.getExternalLinkProps('https://ya.ru')}>Link to external feature</a>
22
+ * ```
23
+ * В разных OS и разных версиях приложения, открытие ресурса будет работать по-разному:
24
+ *
25
+ * - Если текущая версия приложения может открыть ссылку в браузере и не задан параметр `forceOpenInWebview`,
26
+ * обогащаем URL специальным query-параметром (`target=_blank` в приложении не работает).
27
+ * - Если это iOS, меняем URL на диплинк, который откроет ссылку в новом вебвью, поверх текущего.
28
+ * К первому-вебвью, пользователь вернётся, когда закроет второе вебвью с внешним ресурсом.
29
+ * - В старых приложениях на Андроид – URL не меняем, но добавляем `onClick` для сбрасывания синхронизации
30
+ * навигации с приложением. Это «фолбэк-сценарий» с плохим UX (сайт полностью выпадает из истории), но другого способа нет.
31
+ *
32
+ * @param link Строка - валидный урл.
33
+ * @param options - опции
34
+ * @param options.forceOpenInWebview Boolean - по умолчанию = false, если передать true,
35
+ * все ссылки будут открываться в рамках webview, иначе открытие по возможности будет происходить в браузере.
36
+ * @param options.onClick Дополнительный обработчик на клик, например, для отправки метрики.
37
+ * Внимание! Не факт, что в «фолбэк-сценарии» асинхронная операция будет выполнена (метрика отправлена)!
38
+ * @returns Пропсы для ссылки в вебвью окружении.
39
+ */
40
+ getExternalLinkProps(link, options = {}) {
41
+ const { onClick, forceOpenInWebview } = options;
42
+ const url = getUrlInstance(link);
43
+ const appId = getAppId(this.b2n.environment, this.b2n.iosAppId);
44
+ if (!forceOpenInWebview && this.b2n.canUseNativeFeature('linksInBrowser')) {
45
+ url.searchParams.append('openInBrowser', 'true');
46
+ return { href: url.href, onClick };
47
+ }
48
+ if (this.b2n.iosAppId || this.b2n.checkAndroidAllowOpenInNewWebview()) {
49
+ return {
50
+ href: `${appId}://webFeature?type=recommendation&url=${encodeURIComponent(url.href)}`,
51
+ onClick: options?.onClick,
52
+ };
53
+ }
54
+ return {
55
+ href: url.href,
56
+ onClick: () => {
57
+ onClick?.();
58
+ this.b2n.nativeNavigationAndTitle?.setInitialView('');
59
+ },
60
+ };
61
+ }
62
+ /**
63
+ * Метод для открытия PDF в нативном вьювере.
64
+ *
65
+ * Есть нюансы с версиями приложения, OS устройства.
66
+ * Надо тестировать по моде статистики.
67
+ *
68
+ * @param url ссылка на pdf
69
+ * @param type тип pdf ссылки
70
+ * @param title название pdf файла
71
+ */
72
+ openPdf(url, type = 'pdfFile', title) {
73
+ const params = new URLSearchParams();
74
+ params.append('type', type);
75
+ params.append('url', decodeURIComponent(url));
76
+ if (title) {
77
+ params.append('title', title.replace(/\s/g, '_'));
78
+ }
79
+ let replaceUrl = url;
80
+ const paramsStr = params.toString();
81
+ if (this.b2n.environment === 'ios') {
82
+ replaceUrl = `${this.b2n.iosAppId}:///dashboard/pdf_viewer?${paramsStr}`;
83
+ }
84
+ // У андройда через диплинк открывается, но предыдущий экран затирается.
85
+ // Поэтому мы открываем base64 через конвертирование в бинарный pdf (через ручки сервиса)
86
+ // Это позволяет перейти назад к вебвью
87
+ if (this.b2n.environment === 'android' && type === 'base64') {
88
+ replaceUrl = `/services/base64-to-pdf?${paramsStr}`;
89
+ }
90
+ const windowObjectReference = window.open(replaceUrl);
91
+ if (windowObjectReference === null) {
92
+ window.location.replace(replaceUrl);
93
+ }
94
+ }
95
+ /**
96
+ * Метод, для перехода на ВНЕШНИЙ ресурс.
97
+ *
98
+ * См. описание в `getExternalLinkProps`, чтобы узнать, как выбирается способ для перехода.
99
+ *
100
+ * @param link Строка - валидный урл.
101
+ * @param forceOpenInWebview Boolean - по умолчанию = false, если передать true,
102
+ * все ссылки будут открываться в рамках webview, иначе открытие по возможности будет происходить в браузере.
103
+ */
104
+ visitExternalResource(link, forceOpenInWebview = false) {
105
+ const url = getUrlInstance(link);
106
+ const appId = getAppId(this.b2n.environment, this.b2n.iosAppId);
107
+ if (!forceOpenInWebview && this.b2n.canUseNativeFeature('linksInBrowser')) {
108
+ url.searchParams.append('openInBrowser', 'true');
109
+ window.location.replace(url.href);
110
+ }
111
+ else if (this.b2n.iosAppId || this.b2n.checkAndroidAllowOpenInNewWebview()) {
112
+ window.location.replace(`${appId}://webFeature?type=recommendation&url=${encodeURIComponent(url.href)}`);
113
+ }
114
+ else {
115
+ this.b2n.nativeNavigationAndTitle?.setInitialView('');
116
+ window.location.replace(url.href);
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,147 @@
1
+ import { HandleRedirect } from './types';
2
+ import { BridgeToNative } from './bridge-to-native';
3
+ /**
4
+ * Класс, отвечающий за взаимодействие с нативными элементами в приложении – заголовком и нативной кнопкой назад.
5
+ */
6
+ export declare class NativeNavigationAndTitle {
7
+ private b2n;
8
+ private nativeHistoryStack;
9
+ private numOfBackSteps;
10
+ private lastSetPageSettingsParams;
11
+ private readonly _handleWindowRedirect;
12
+ constructor(b2n: BridgeToNative, pageId: number | null, initialNativeTitle: string | undefined, handleWindowRedirect: HandleRedirect);
13
+ /**
14
+ * Метод, вызывающий `history.back()` или закрывающий вебвью, если нет записей
15
+ * в истории переходов.
16
+ */
17
+ goBack(): void;
18
+ /**
19
+ * Метод, вызывающий history.go(-колл. шагов назад) и модифицирует внутреннее
20
+ * состояние, чтобы в дальнейшем зарегистрировать этот переход в приложении.
21
+ *
22
+ * @param stepsNumber Количество шагов назад.
23
+ * Возможно передача как положительного, так и отрицательного числа.
24
+ * 0 будет проигнорирован.
25
+ * @param autoCloseWebview Флаг – закрывать ли вебвью автоматически,
26
+ * если переданное кол-во шагов будет больше чем записей в истории.
27
+ */
28
+ goBackAFewSteps(stepsNumber: number, autoCloseWebview?: boolean): void;
29
+ /**
30
+ * @param path Путь для перехода на функциональность внутри приложения.
31
+ * @param historyState (https://developer.mozilla.org/en-US/docs/Web/API/History/state) для новой записи в истории.
32
+ */
33
+ handleRedirect(path: string, historyState?: Record<string, unknown>): void;
34
+ /**
35
+ * В этом варианте аргументы 2,3,4 соответствуют аргументам 1,2,3 метода `src/shared/utils/handle-redirect`.
36
+ *
37
+ * @param pageTitle Заголовок, который нужно отрисовать в приложении.
38
+ * @param appName См. первый параметр `src/handle-redirect.ts`.
39
+ * @param path См. второй параметр `src/handle-redirect.ts`.
40
+ * @param params См. третий параметр `src/handle-redirect.ts`.
41
+ * @param historyState (https://developer.mozilla.org/en-US/docs/Web/API/History/state) для новой записи в истории.
42
+ */
43
+ handleRedirect(pageTitle: string, appName: string, path?: string, params?: Record<string, string>, historyState?: Record<string, unknown>): void;
44
+ /**
45
+ * Информирует натив, что веб находится на первом экране (сбрасывает историю переходов, не влияя на браузерную
46
+ * историю), а значит следующее нажатие на кнопку "Назад" в нативе закроет вебвью.
47
+ *
48
+ * @param pageTitle Заголовок, который нужно отрисовать в нативе.
49
+ */
50
+ setInitialView(pageTitle?: string): void;
51
+ /**
52
+ * Метод для смены заголовка в нативе без влияния на историю переходов.
53
+ *
54
+ * @param pageTitle Заголовок, который нужно отрисовать в нативе.
55
+ */
56
+ setTitle(pageTitle: string): void;
57
+ /**
58
+ * Метод для открытия второго web приложения в рамках одной вебвью сессии.
59
+ * Сохраняет все текущее состояние текущего экземпляра bridgeToAm и AmNavigationAndTitle в sessionStorage, а
60
+ * так же наполняет url необходимыми query параметрами. Работает только в Android окружении.
61
+ * В IOS окружении будет открыто новое webview поверх текущего.
62
+ *
63
+ * @param url адрес второго web приложения, к которому перед переходом на него будут добавлены
64
+ * все initial query параметры от натива и параметр nextPageId (Android)
65
+ */
66
+ navigateInsideASharedSession(url: string): void;
67
+ /**
68
+ * Безопасный способ для перезагрузки страницы.
69
+ */
70
+ pseudoReloadPage(): void;
71
+ /**
72
+ * Вызывает обработчик deeplinks в нативе (АМ) и передает туда переданный deeplink.
73
+ * На Android текущее webview будет закрыто из-за технических особенностей.
74
+ * На IOS нативная фича открывается в следующем по стеку экране и при выходе из нее пользователь вернется обратно в webview.
75
+ * На IOS есть возможность закрыть webview перед открытием нативной фичи, передав второй параметр closeIOSWebviewBeforeCallNativeDeeplinkHandler = true
76
+ * @param deeplink диплинк на нативную АМ фичу в AM
77
+ * @param [closeIOSWebviewBeforeCallNativeDeeplinkHandler = false] закрыть текущее webview после открытия нативной фичи (применимо только для IOS на Android по техническим причинам webview всегда будет закрываться)
78
+ */
79
+ handleNativeDeeplink(deeplink: string, closeIOSWebviewBeforeCallNativeDeeplinkHandler?: boolean): void;
80
+ /**
81
+ * Метод для сохранения текущего состояния NativeNavigationAndTitle в sessionStorage.
82
+ */
83
+ private saveCurrentState;
84
+ /**
85
+ * Метод, вычисляющий `pageId`, который нужно послать в приложение
86
+ * для правильной синхронизации с нативной-кнопкой "Назад".
87
+ *
88
+ * @param purpose Цель взаимодействия с приложением.
89
+ * @returns Правильный pageId.
90
+ */
91
+ private getNativePageId;
92
+ /**
93
+ * Вспомогательный метод для `getNativePageId` initialization кейса.
94
+ *
95
+ * @returns Правильный pageId.
96
+ */
97
+ private getNativePageIdForInitialization;
98
+ /**
99
+ * Вспомогательный метод для `getNativePageId` navigation кейса.
100
+ *
101
+ * @returns Правильный pageId.
102
+ */
103
+ private getNativePageIdForNavigation;
104
+ /**
105
+ * Вспомогательный метод для `getNativePageId` only-title кейса.
106
+ *
107
+ * @returns Правильный pageId.
108
+ */
109
+ private getNativePageIdForTitleReplacing;
110
+ /**
111
+ * Обработчик для `window.onpopstate` события. Который сработает
112
+ * после нажатия на кнопку "Назад" в нативе, вызова `history.back()` и `history.go(-x)`.
113
+ */
114
+ private handleBack;
115
+ /**
116
+ * Синхронизирует состояние истории переходов и заголовок с приложением.
117
+ *
118
+ * @param pageTitle Заголовок, который нужно отрисовать в приложении.
119
+ * @param purpose Цель взаимодействия с приложением.
120
+ */
121
+ private syncHistoryWithNative;
122
+ /**
123
+ * Метод для перехода в веб из другого веб приложения в рамках
124
+ * одной вебвью сессии
125
+ * @param pageId - Номер текущего page который нужно отправить в приложение
126
+ * @param title - Title текущего page который нужно отправить в приложение
127
+ */
128
+ private supportSharedSession;
129
+ /**
130
+ * Восстанавливает свое предыдущее состояние nativeHistoryStack и title из sessionStorage
131
+ */
132
+ private restorePreviousState;
133
+ /**
134
+ * Вспомогательный метод для setInitialView, supportSharedSession
135
+ * переназначает обработчик @handleBack для `window.onpopstate` события
136
+ */
137
+ private reassignPopstateListener;
138
+ /**
139
+ * Вспомогательный метод для navigateInsideASharedSession.
140
+ * Подготавливает внешнюю ссылку в рамках контракта для совместной работы веб-приложений в
141
+ * рамках одной вебвью сессии
142
+ * @param url - url иного веб приложения
143
+ * @return подготовленная согласно контракту ссылка на иное веб приложение с initial query
144
+ * параметрами от натива, а так же nextPageId.
145
+ */
146
+ private prepareExternalLinkBeforeOpen;
147
+ }
@@ -0,0 +1,326 @@
1
+ import { DEEP_LINK_PATTERN, PREVIOUS_NATIVE_NAVIGATION_AND_TITLE_STATE_STORAGE_KEY, } from './constants';
2
+ import { extractAppNameRouteAndQuery } from './utils';
3
+ /**
4
+ * Класс, отвечающий за взаимодействие с нативными элементами в приложении – заголовком и нативной кнопкой назад.
5
+ */
6
+ export class NativeNavigationAndTitle {
7
+ b2n;
8
+ nativeHistoryStack = [''];
9
+ numOfBackSteps = 1;
10
+ // Тут сохраняются параметры, которые в последний раз были отправлены в приложение.
11
+ // Просто, чтобы не слать одинаковые сигналы в приложение.
12
+ lastSetPageSettingsParams = '';
13
+ _handleWindowRedirect;
14
+ constructor(b2n, pageId, initialNativeTitle = '', handleWindowRedirect) {
15
+ this.b2n = b2n;
16
+ this.handleBack = this.handleBack.bind(this);
17
+ this._handleWindowRedirect = handleWindowRedirect;
18
+ const previousState = !!sessionStorage.getItem(PREVIOUS_NATIVE_NAVIGATION_AND_TITLE_STATE_STORAGE_KEY);
19
+ if (pageId) {
20
+ this.supportSharedSession(pageId, initialNativeTitle);
21
+ }
22
+ else if (previousState) {
23
+ this.restorePreviousState();
24
+ }
25
+ else {
26
+ this.setInitialView(initialNativeTitle);
27
+ }
28
+ }
29
+ /**
30
+ * Метод, вызывающий `history.back()` или закрывающий вебвью, если нет записей
31
+ * в истории переходов.
32
+ */
33
+ goBack() {
34
+ this.goBackAFewSteps(-1, true);
35
+ }
36
+ /**
37
+ * Метод, вызывающий history.go(-колл. шагов назад) и модифицирует внутреннее
38
+ * состояние, чтобы в дальнейшем зарегистрировать этот переход в приложении.
39
+ *
40
+ * @param stepsNumber Количество шагов назад.
41
+ * Возможно передача как положительного, так и отрицательного числа.
42
+ * 0 будет проигнорирован.
43
+ * @param autoCloseWebview Флаг – закрывать ли вебвью автоматически,
44
+ * если переданное кол-во шагов будет больше чем записей в истории.
45
+ */
46
+ goBackAFewSteps(stepsNumber, autoCloseWebview = false) {
47
+ if (!stepsNumber) {
48
+ return;
49
+ }
50
+ const stepsToBack = Math.abs(stepsNumber);
51
+ const maxStepsToBack = this.nativeHistoryStack.length - 1;
52
+ if (stepsToBack > maxStepsToBack) {
53
+ if (autoCloseWebview) {
54
+ this.b2n.closeWebview();
55
+ return;
56
+ }
57
+ this.numOfBackSteps = maxStepsToBack;
58
+ }
59
+ else {
60
+ this.numOfBackSteps = stepsToBack;
61
+ }
62
+ window.history.go(-this.numOfBackSteps);
63
+ }
64
+ /**
65
+ * Метод вызывает `src/shared/utils/handle-redirect` из `newclick-host-ui`
66
+ * и регистрирует этот переход в приложении, чтобы кнопка «Назад» в Нативе вызывала
67
+ * переход назад в вебе.
68
+ */
69
+ handleRedirect(pageTitleOrPath, appNameOrHistoryState, path, params, historyState) {
70
+ const checkAppNameArgument = (argument) => Boolean(appNameOrHistoryState && typeof appNameOrHistoryState === 'string');
71
+ const isAppNameArgument = checkAppNameArgument(appNameOrHistoryState);
72
+ if (isAppNameArgument) {
73
+ this._handleWindowRedirect(appNameOrHistoryState, path, params, historyState);
74
+ }
75
+ else {
76
+ const { appName: extractedAppName, path: extractedPath, query: extractedQuery, } = extractAppNameRouteAndQuery(pageTitleOrPath);
77
+ this._handleWindowRedirect(extractedAppName, extractedPath, extractedQuery, appNameOrHistoryState);
78
+ }
79
+ const title = isAppNameArgument ? pageTitleOrPath : '';
80
+ this.nativeHistoryStack.push(title);
81
+ this.syncHistoryWithNative(title, 'navigation');
82
+ }
83
+ /**
84
+ * Информирует натив, что веб находится на первом экране (сбрасывает историю переходов, не влияя на браузерную
85
+ * историю), а значит следующее нажатие на кнопку "Назад" в нативе закроет вебвью.
86
+ *
87
+ * @param pageTitle Заголовок, который нужно отрисовать в нативе.
88
+ */
89
+ setInitialView(pageTitle = '') {
90
+ this.nativeHistoryStack = [pageTitle];
91
+ this.syncHistoryWithNative(pageTitle, 'initialization');
92
+ this.reassignPopstateListener();
93
+ }
94
+ /**
95
+ * Метод для смены заголовка в нативе без влияния на историю переходов.
96
+ *
97
+ * @param pageTitle Заголовок, который нужно отрисовать в нативе.
98
+ */
99
+ setTitle(pageTitle) {
100
+ this.nativeHistoryStack[this.nativeHistoryStack.length - 1] = pageTitle;
101
+ this.syncHistoryWithNative(pageTitle, 'title-replacing');
102
+ }
103
+ /**
104
+ * Метод для открытия второго web приложения в рамках одной вебвью сессии.
105
+ * Сохраняет все текущее состояние текущего экземпляра bridgeToAm и AmNavigationAndTitle в sessionStorage, а
106
+ * так же наполняет url необходимыми query параметрами. Работает только в Android окружении.
107
+ * В IOS окружении будет открыто новое webview поверх текущего.
108
+ *
109
+ * @param url адрес второго web приложения, к которому перед переходом на него будут добавлены
110
+ * все initial query параметры от натива и параметр nextPageId (Android)
111
+ */
112
+ navigateInsideASharedSession(url) {
113
+ if (this.b2n.environment === 'ios') {
114
+ const nativeDeeplink = `/webFeature?type=recommendation&url=${encodeURIComponent(url)}`;
115
+ this.handleNativeDeeplink(nativeDeeplink);
116
+ return;
117
+ }
118
+ // В b2n этот метод отмечен модификатором доступа private, но тут его нужно вызвать
119
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
120
+ // @ts-ignore
121
+ this.b2n.saveCurrentState();
122
+ window.location.assign(this.prepareExternalLinkBeforeOpen(url));
123
+ }
124
+ /**
125
+ * Безопасный способ для перезагрузки страницы.
126
+ */
127
+ pseudoReloadPage() {
128
+ // В b2n этот метод отмечен модификатором доступа private, но тут его нужно вызвать
129
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
130
+ // @ts-ignore
131
+ this.handleRedirect(this.b2n._blankPagePath);
132
+ this.goBack();
133
+ }
134
+ /**
135
+ * Вызывает обработчик deeplinks в нативе (АМ) и передает туда переданный deeplink.
136
+ * На Android текущее webview будет закрыто из-за технических особенностей.
137
+ * На IOS нативная фича открывается в следующем по стеку экране и при выходе из нее пользователь вернется обратно в webview.
138
+ * На IOS есть возможность закрыть webview перед открытием нативной фичи, передав второй параметр closeIOSWebviewBeforeCallNativeDeeplinkHandler = true
139
+ * @param deeplink диплинк на нативную АМ фичу в AM
140
+ * @param [closeIOSWebviewBeforeCallNativeDeeplinkHandler = false] закрыть текущее webview после открытия нативной фичи (применимо только для IOS на Android по техническим причинам webview всегда будет закрываться)
141
+ */
142
+ handleNativeDeeplink(deeplink, closeIOSWebviewBeforeCallNativeDeeplinkHandler = false) {
143
+ const clearedDeeplinkPath = deeplink.replace(DEEP_LINK_PATTERN, '');
144
+ if (this.b2n.environment === 'ios') {
145
+ if (closeIOSWebviewBeforeCallNativeDeeplinkHandler) {
146
+ this.b2n.closeWebview();
147
+ setTimeout(() => window.location.replace(`${this.b2n.iosAppId}://${clearedDeeplinkPath}`), 0);
148
+ return;
149
+ }
150
+ window.location.replace(`${this.b2n.iosAppId}://${clearedDeeplinkPath}`);
151
+ }
152
+ else {
153
+ window.location.replace(`alfabank://${clearedDeeplinkPath}`);
154
+ }
155
+ }
156
+ /**
157
+ * Метод для сохранения текущего состояния NativeNavigationAndTitle в sessionStorage.
158
+ */
159
+ saveCurrentState() {
160
+ const currentState = {
161
+ title: this.nativeHistoryStack[this.nativeHistoryStack.length - 1],
162
+ nativeHistoryStack: this.nativeHistoryStack,
163
+ };
164
+ sessionStorage.setItem(PREVIOUS_NATIVE_NAVIGATION_AND_TITLE_STATE_STORAGE_KEY, JSON.stringify(currentState));
165
+ }
166
+ /**
167
+ * Метод, вычисляющий `pageId`, который нужно послать в приложение
168
+ * для правильной синхронизации с нативной-кнопкой "Назад".
169
+ *
170
+ * @param purpose Цель взаимодействия с приложением.
171
+ * @returns Правильный pageId.
172
+ */
173
+ getNativePageId(purpose) {
174
+ function assertUnreachable(val) {
175
+ throw new Error(`Unexpected value "${val}"`);
176
+ }
177
+ let pageId;
178
+ switch (purpose) {
179
+ case 'initialization':
180
+ pageId = this.getNativePageIdForInitialization();
181
+ break;
182
+ case 'navigation':
183
+ pageId = this.getNativePageIdForNavigation();
184
+ break;
185
+ case 'title-replacing':
186
+ pageId = this.getNativePageIdForTitleReplacing();
187
+ break;
188
+ default:
189
+ assertUnreachable(purpose);
190
+ }
191
+ return pageId;
192
+ }
193
+ /**
194
+ * Вспомогательный метод для `getNativePageId` initialization кейса.
195
+ *
196
+ * @returns Правильный pageId.
197
+ */
198
+ getNativePageIdForInitialization() {
199
+ // * В iOS для "первой" страницы не нужно слать `pageId`.
200
+ // * В Android важно, чтобы `pageId` "первой" страницы
201
+ // всегда был одинаковый.
202
+ return this.b2n.environment === 'ios' ? null : 1;
203
+ }
204
+ /**
205
+ * Вспомогательный метод для `getNativePageId` navigation кейса.
206
+ *
207
+ * @returns Правильный pageId.
208
+ */
209
+ getNativePageIdForNavigation() {
210
+ const stackSize = this.nativeHistoryStack.length;
211
+ // Нажимая на кнопку назад, можно дойти до "первой" страницы,
212
+ // в iOS для "первой" страницы не нужно слать `pageId`.
213
+ return this.b2n.environment === 'ios' && stackSize <= 1 ? null : stackSize;
214
+ }
215
+ /**
216
+ * Вспомогательный метод для `getNativePageId` only-title кейса.
217
+ *
218
+ * @returns Правильный pageId.
219
+ */
220
+ getNativePageIdForTitleReplacing() {
221
+ const stackSize = this.nativeHistoryStack.length;
222
+ if (this.b2n.environment === 'android') {
223
+ // Для смены заголовка в Андроид просто повторяем текущий `pageId`.
224
+ // В отличии от iOS, если не послать `pageId` первой страницы,
225
+ // Вебвью не будет закрываться по клику на нативный «Назад».
226
+ return stackSize <= 1 ? 1 : stackSize;
227
+ }
228
+ // Если в iOS не послать `pageId`, следующее нажатие на
229
+ // нативную кнопку назад закроет webview.
230
+ return stackSize <= 1 ? null : stackSize;
231
+ }
232
+ /**
233
+ * Обработчик для `window.onpopstate` события. Который сработает
234
+ * после нажатия на кнопку "Назад" в нативе, вызова `history.back()` и `history.go(-x)`.
235
+ */
236
+ handleBack() {
237
+ const previousState = !!sessionStorage.getItem(PREVIOUS_NATIVE_NAVIGATION_AND_TITLE_STATE_STORAGE_KEY);
238
+ if (previousState) {
239
+ // В b2n этот метод отмечен модификатором доступа private дабы не торчал наружу, но тут его нужно вызвать
240
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
241
+ // @ts-ignore
242
+ this.b2n.restorePreviousState();
243
+ }
244
+ this.nativeHistoryStack = this.nativeHistoryStack.slice(0, -this.numOfBackSteps);
245
+ this.numOfBackSteps = 1;
246
+ if (this.nativeHistoryStack.length < 1) {
247
+ this.b2n.closeWebview();
248
+ return;
249
+ }
250
+ const pageTitle = this.nativeHistoryStack[this.nativeHistoryStack.length - 1];
251
+ this.syncHistoryWithNative(pageTitle, 'navigation');
252
+ }
253
+ /**
254
+ * Синхронизирует состояние истории переходов и заголовок с приложением.
255
+ *
256
+ * @param pageTitle Заголовок, который нужно отрисовать в приложении.
257
+ * @param purpose Цель взаимодействия с приложением.
258
+ */
259
+ syncHistoryWithNative(pageTitle, purpose) {
260
+ const pageId = this.getNativePageId(purpose);
261
+ if (this.b2n.environment === 'android') {
262
+ const pageSettingsObj = { pageTitle };
263
+ if (pageId) {
264
+ pageSettingsObj.pageId = pageId;
265
+ }
266
+ const paramsToSend = JSON.stringify(pageSettingsObj);
267
+ if (this.lastSetPageSettingsParams !== paramsToSend) {
268
+ this.b2n.AndroidBridge?.setPageSettings(paramsToSend);
269
+ this.lastSetPageSettingsParams = paramsToSend;
270
+ }
271
+ }
272
+ else {
273
+ const pageTitleStr = `?pageTitle=${encodeURIComponent(pageTitle)}`;
274
+ const pageIdStr = pageId ? `&pageId=${pageId}` : '';
275
+ const paramsToSend = `ios:setPageSettings/${pageTitleStr + pageIdStr}`;
276
+ if (this.lastSetPageSettingsParams !== paramsToSend) {
277
+ window.location.replace(paramsToSend);
278
+ this.lastSetPageSettingsParams = paramsToSend;
279
+ }
280
+ }
281
+ }
282
+ /**
283
+ * Метод для перехода в веб из другого веб приложения в рамках
284
+ * одной вебвью сессии
285
+ * @param pageId - Номер текущего page который нужно отправить в приложение
286
+ * @param title - Title текущего page который нужно отправить в приложение
287
+ */
288
+ supportSharedSession(pageId, title) {
289
+ this.nativeHistoryStack = new Array(pageId).fill('');
290
+ this.syncHistoryWithNative(title, 'title-replacing');
291
+ this.reassignPopstateListener();
292
+ }
293
+ /**
294
+ * Восстанавливает свое предыдущее состояние nativeHistoryStack и title из sessionStorage
295
+ */
296
+ restorePreviousState() {
297
+ const previousState = JSON.parse(sessionStorage.getItem(PREVIOUS_NATIVE_NAVIGATION_AND_TITLE_STATE_STORAGE_KEY) || '');
298
+ this.nativeHistoryStack = previousState.nativeHistoryStack;
299
+ this.syncHistoryWithNative(previousState.title, 'title-replacing');
300
+ this.reassignPopstateListener();
301
+ sessionStorage.removeItem(PREVIOUS_NATIVE_NAVIGATION_AND_TITLE_STATE_STORAGE_KEY);
302
+ }
303
+ /**
304
+ * Вспомогательный метод для setInitialView, supportSharedSession
305
+ * переназначает обработчик @handleBack для `window.onpopstate` события
306
+ */
307
+ reassignPopstateListener() {
308
+ window.removeEventListener('popstate', this.handleBack);
309
+ window.addEventListener('popstate', this.handleBack);
310
+ }
311
+ /**
312
+ * Вспомогательный метод для navigateInsideASharedSession.
313
+ * Подготавливает внешнюю ссылку в рамках контракта для совместной работы веб-приложений в
314
+ * рамках одной вебвью сессии
315
+ * @param url - url иного веб приложения
316
+ * @return подготовленная согласно контракту ссылка на иное веб приложение с initial query
317
+ * параметрами от натива, а так же nextPageId.
318
+ */
319
+ prepareExternalLinkBeforeOpen(url) {
320
+ const currentPageId = this.nativeHistoryStack.length;
321
+ const divider = new URL(url).searchParams.toString() ? '&' : '?';
322
+ const link = new URL(`${url}${divider}${this.b2n.originalWebviewParams}`);
323
+ link.searchParams.set('nextPageId', (currentPageId + 1).toString());
324
+ return link.toString();
325
+ }
326
+ }
package/esm/types.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ export declare type NativeParams = {
2
+ appVersion: string;
3
+ title?: string;
4
+ iosAppId?: string;
5
+ theme: string;
6
+ nextPageId: number | null;
7
+ originalWebviewParams: string;
8
+ };
9
+ export declare type NativeFeatureKey = 'geolocation' | 'linksInBrowser';
10
+ declare type NativeFeaturesParams = Readonly<Record<NativeFeatureKey, {
11
+ fromVersion: string;
12
+ }>>;
13
+ export declare type NativeFeaturesFromVersion = Readonly<{
14
+ android: NativeFeaturesParams;
15
+ ios: NativeFeaturesParams;
16
+ }>;
17
+ export declare type Environment = 'android' | 'ios';
18
+ export declare type WebViewWindow = Window & {
19
+ Android?: {
20
+ setPageSettings: (params: string) => void;
21
+ };
22
+ handleRedirect?: (appName: string, path?: string, params?: Record<string, string>) => VoidFunction;
23
+ };
24
+ export declare type PdfType = 'pdfFile' | 'base64' | 'binary';
25
+ export declare type PreviousBridgeToNativeState = Omit<NativeParams, 'title' | 'theme'> & {
26
+ theme: 'dark' | 'light';
27
+ };
28
+ export declare type PreviousNativeNavigationAndTitleState = {
29
+ nativeHistoryStack: string[];
30
+ title: string;
31
+ };
32
+ export declare type SyncPurpose = 'initialization' | 'navigation' | 'title-replacing';
33
+ export declare type HandleRedirect = (appName: string, path?: string, params?: Record<string, string>, historyState?: Record<string, unknown>) => void;
34
+ export declare type Theme = 'light' | 'dark';
35
+ export declare type ExternalNavigationOptions = {
36
+ onClick?: () => void;
37
+ forceOpenInWebview?: boolean;
38
+ };
39
+ export {};
package/esm/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/esm/utils.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { Environment } from './types';
2
+ /**
3
+ * Разделяет веб ссылку на компоненты
4
+ * @param route внутренний путь для навигации
5
+ * @return объект с appName, route, query
6
+ */
7
+ export declare const extractAppNameRouteAndQuery: (route: string) => {
8
+ appName: string;
9
+ path: string;
10
+ query: Record<string, string> | undefined;
11
+ };
12
+ /**
13
+ * Возвращает экземпляр `URL` из ссылки, докидывая `https://` при отсутствии.
14
+ */
15
+ export declare const getUrlInstance: (link: string) => URL;
16
+ /**
17
+ * Проверяет, что переданная строка содержит версию приложения в правильном формате.
18
+ *
19
+ * @param version Строка с версией для проверки.
20
+ * @returns Правильный формат или нет.
21
+ */
22
+ export declare const isValidVersionFormat: (version?: string | undefined) => boolean;
23
+ export declare const getAppId: (environment: Environment, iosAppId?: string | undefined) => string | null;
package/esm/utils.js ADDED
@@ -0,0 +1,62 @@
1
+ import { ANDROID_APP_ID } from './constants';
2
+ /**
3
+ * Разделяет веб ссылку на компоненты
4
+ * @param route внутренний путь для навигации
5
+ * @return объект с appName, route, query
6
+ */
7
+ export const extractAppNameRouteAndQuery = (route) => {
8
+ let appName = '';
9
+ let path = '';
10
+ let query;
11
+ const clearedPath = route.replace(/(?:^\/)|(?:\/$)/g, '');
12
+ const segments = clearedPath.split('/');
13
+ const queryByPath = clearedPath.split('?')[1];
14
+ appName = segments.shift()?.split('?')[0] || '';
15
+ if (queryByPath) {
16
+ query = Array.from(new URLSearchParams(queryByPath).entries()).reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
17
+ }
18
+ path = segments.join('/').replace(`?${queryByPath}`, '');
19
+ return { appName, path, query };
20
+ };
21
+ /**
22
+ * Возвращает экземпляр `URL` из ссылки, докидывая `https://` при отсутствии.
23
+ */
24
+ export const getUrlInstance = (link) => {
25
+ const protocolRequiredPattern = /^https?:\/\//;
26
+ let url;
27
+ if (protocolRequiredPattern.test(link)) {
28
+ url = new URL(link);
29
+ }
30
+ else {
31
+ try {
32
+ // Пробуем докинуть `https://`, как правило, это помогает.
33
+ url = new URL(`https://${link}`);
34
+ }
35
+ catch (e) {
36
+ // Кажется, добавив протокол, сюда мы больше не сможем вывалиться, но на всякий случай...
37
+ url = new URL('about:blank');
38
+ }
39
+ }
40
+ return url;
41
+ };
42
+ /**
43
+ * Проверяет, что переданная строка содержит версию приложения в правильном формате.
44
+ *
45
+ * @param version Строка с версией для проверки.
46
+ * @returns Правильный формат или нет.
47
+ */
48
+ export const isValidVersionFormat = (version) => {
49
+ if (!version)
50
+ return false;
51
+ const versionPattern = /^\d+\.\d+\.\d+$/;
52
+ return versionPattern.test(version);
53
+ };
54
+ export const getAppId = (environment, iosAppId) => {
55
+ if (environment === 'android') {
56
+ return atob(ANDROID_APP_ID);
57
+ }
58
+ if (environment === 'ios' && iosAppId && typeof iosAppId === 'string') {
59
+ return iosAppId;
60
+ }
61
+ return null;
62
+ };
package/package.json CHANGED
@@ -1,11 +1,27 @@
1
1
  {
2
2
  "license": "UNLICENSED",
3
3
  "name": "@alfalab/bridge-to-native",
4
- "version": "0.0.13",
4
+ "version": "0.1.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/core-ds/bridge-to-native.git"
8
8
  },
9
+ "main": "./index.js",
10
+ "module": "./esm/index.js",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./esm/index.js",
14
+ "require": "./index.js"
15
+ },
16
+ "./*": {
17
+ "import": "./esm/*",
18
+ "require": "./*"
19
+ }
20
+ },
21
+ "sideEffects": false,
22
+ "files": [
23
+ "*"
24
+ ],
9
25
  "bugs": {
10
26
  "url": "https://github.com/core-ds/bridge-to-native/issues"
11
27
  },
@@ -13,18 +29,19 @@
13
29
  "scripts": {
14
30
  "build": "yarn compile",
15
31
  "changelog": "bash bin/fill-changelog-file-and-notify-github.sh",
16
- "compile": "yarn compile:clean && yarn compile:ts && yarn compile:copy-resources",
32
+ "compile": "yarn compile:clean && yarn compile:ts && yarn compile:ts:esm && yarn compile:copy-resources",
17
33
  "compile:copy-package-json": "shx cp package.json .publish/package.json",
18
34
  "compile:copy-resources": "yarn copyfiles -e \"**/*.{[jt]s*(x),snap}\" -e \"**/*.json\" -u 1 \"src/**/*\" .publish",
19
35
  "compile:clean": "shx rm -rf .publish",
20
36
  "compile:ts": "tsc --project tsconfig.build.json",
37
+ "compile:ts:esm": "tsc --project tsconfig.build.esm.json",
21
38
  "lint:scripts": "eslint \"**/*.{js,jsx,ts,tsx}\" --ext .js,.jsx,.ts,.tsx",
22
39
  "lint": "yarn lint:scripts && prettier --check \"./src/*.{ts,tsx,js,jsx,json}\"",
23
40
  "lint:fix": "yarn lint:scripts --fix",
24
41
  "pub": "npm publish .publish --userconfig \"../.npmrc\" --tag \"$TAG\"",
25
42
  "format": "prettier --write \"./**/*.{ts,tsx,js,jsx,css,json}\"",
26
43
  "test": "jest --silent --collect-coverage",
27
- "release": "yarn compile && npm version \"$VERSION\" --no-git-tag-version && yarn compile:copy-package-json && yarn pub"
44
+ "release": "yarn build && npm version \"$VERSION\" --no-git-tag-version && yarn compile:copy-package-json && yarn pub"
28
45
  },
29
46
  "dependencies": {},
30
47
  "devDependencies": {