From b5c9d0208bdd44b8ce646d3f5641f98ce70f714e Mon Sep 17 00:00:00 2001 From: Aliaksandr Pashkevich Date: Sun, 18 Feb 2024 17:14:53 +0300 Subject: [PATCH] =?UTF-8?q?feat(deploy=5Fbeta):=20=D0=B2=D1=8B=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=BD=D1=8F=D0=BB=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82?= =?UTF-8?q?=D1=8B=20=D0=BD=D0=B5=D0=BE=D0=B1=D1=85=D0=BE=D0=B4=D0=B8=D0=BC?= =?UTF-8?q?=D1=8B=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 8 +- src/bridge-to-native.ts | 228 ++++++++++++++++++++ src/index.ts | 231 +-------------------- src/native-fallbacks.ts | 2 +- src/native-navigation-and-title.ts | 2 +- src/types.ts | 2 + test/index.test.ts | 4 +- test/integration.test.ts | 2 +- {src => test}/mock/mock-session-storage.ts | 0 test/native-fallbacks.test.ts | 4 +- test/native-navigation-and-title.test.ts | 6 +- tsconfig.build.json | 2 +- 12 files changed, 248 insertions(+), 243 deletions(-) create mode 100644 src/bridge-to-native.ts rename {src => test}/mock/mock-session-storage.ts (100%) diff --git a/package.json b/package.json index ef5576c..f4703e3 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "changelog": "bash bin/fill-changelog-file-and-notify-github.sh", "compile": "yarn compile:clean && yarn compile:ts && yarn compile:copy-resources", "compile:copy-package-json": "shx cp package.json .publish/package.json", - "compile:copy-resources": "yarn copyfiles -e \"**/*.{[jt]s*(x),snap}\" -e \"**/*.json\" -e \"src/mock/**/*\" -u 1 \"src/**/*\" .publish", + "compile:copy-resources": "yarn copyfiles -e \"**/*.{[jt]s*(x),snap}\" -e \"**/*.json\" -u 1 \"src/**/*\" .publish", "compile:clean": "shx rm -rf .publish", "compile:ts": "tsc --project tsconfig.build.json", "lint:scripts": "eslint \"**/*.{js,jsx,ts,tsx}\" --ext .js,.jsx,.ts,.tsx", @@ -67,13 +67,15 @@ ], "coveragePathIgnorePatterns": [ "/node_modules/", - "/test/" + "/test/", + "/src/index.ts" ], "transformIgnorePatterns": [ "node_modules/(?!(uuid)/)" ], "testPathIgnorePatterns": [ - "/node_modules/" + "/node_modules/", + "/test/mock" ], "reporters": [ "jest-junit", diff --git a/src/bridge-to-native.ts b/src/bridge-to-native.ts new file mode 100644 index 0000000..37b4ad4 --- /dev/null +++ b/src/bridge-to-native.ts @@ -0,0 +1,228 @@ +/* eslint-disable no-underscore-dangle */ + +import { + CLOSE_WEBVIEW_SEARCH_KEY, + CLOSE_WEBVIEW_SEARCH_VALUE, + nativeFeaturesFromVersion, + PREVIOUS_B2N_STATE_STORAGE_KEY, + versionToIosAppId, +} from './constants'; +import { NativeFallbacks } from './native-fallbacks'; +import { NativeNavigationAndTitle } from './native-navigation-and-title'; +import type { + Environment, + HandleRedirect, + NativeFeatureKey, + NativeParams, + Theme, + WebViewWindow, +} from './types'; +import { PreviousBridgeToNativeState } from './types'; +import { isValidVersionFormat } from './utils'; + +/** + * Этот класс — абстракция для связи веб приложения с нативом и предназначен ТОЛЬКО + * для использования в вебвью окружении. + */ +export class BridgeToNative { + // Webview, запущенное в Android окружении имеет объект `Android` в window. + public readonly AndroidBridge = (window as WebViewWindow).Android; + + public readonly environment: Environment = this.AndroidBridge ? 'android' : 'ios'; + + public readonly nativeFallbacks: NativeFallbacks; + + private nextPageId: number | null; + + private _nativeNavigationAndTitle: NativeNavigationAndTitle; + + private _originalWebviewParams: string; + + // В формате `x.x.x`. + private _appVersion: string; + + // Необходимо для формирования диплинка. + private _iosAppId?: string; + + private _theme: Theme; + + private readonly _blankPagePath: string; + + private readonly _handleRedirect: HandleRedirect; + + constructor( + handleRedirect: HandleRedirect, + blankPagePath: string, + nativeParams?: NativeParams, + ) { + const previousState = !!sessionStorage.getItem(PREVIOUS_B2N_STATE_STORAGE_KEY); + + if (previousState) { + this.restorePreviousState(); + this.nativeFallbacks = new NativeFallbacks(this); + this._blankPagePath = blankPagePath; + + return; + } + + this._appVersion = + nativeParams && isValidVersionFormat(nativeParams?.appVersion) + ? nativeParams.appVersion + : '0.0.0'; + this._iosAppId = this.getIosAppId(nativeParams?.iosAppId); + this._theme = nativeParams?.theme === 'dark' ? 'dark' : 'light'; + this._originalWebviewParams = nativeParams?.originalWebviewParams || ''; + this._nativeNavigationAndTitle = new NativeNavigationAndTitle( + this, + nativeParams ? nativeParams.nextPageId : null, + nativeParams?.title, + handleRedirect, + ); + this._handleRedirect = handleRedirect; + + this.nextPageId = nativeParams ? nativeParams.nextPageId : null; + this.nativeFallbacks = new NativeFallbacks(this); + this._blankPagePath = blankPagePath; + } + + get theme() { + return this._theme; + } + + get appVersion() { + return this._appVersion; + } + + get iosAppId() { + return this._iosAppId; + } + + get nativeNavigationAndTitle() { + return this._nativeNavigationAndTitle; + } + + get originalWebviewParams() { + return this._originalWebviewParams; + } + + /** + * Метод, проверяющий, можно ли использовать нативную функциональность в текущей версии приложения. + * + * @param feature Название функциональности, которую нужно проверить. + */ + public canUseNativeFeature(feature: NativeFeatureKey) { + const { fromVersion } = nativeFeaturesFromVersion[this.environment][feature]; + + return this.isCurrentVersionHigherOrEqual(fromVersion); + } + + /** + * Метод, отправляющий сигнал нативу, что нужно закрыть текущее вебвью. + */ + // eslint-disable-next-line class-methods-use-this + public closeWebview() { + const originalPageUrl = new URL(window.location.href); + + originalPageUrl.searchParams.set(CLOSE_WEBVIEW_SEARCH_KEY, CLOSE_WEBVIEW_SEARCH_VALUE); + window.location.href = originalPageUrl.toString(); + } + + /** + * Сравнивает текущую версию приложения с переданной. + * + * @param versionToCompare Версия, с которой нужно сравнить текущую. + * @returns `true` – текущая версия больше или равняется переданной, + * `false` – текущая версия ниже. + */ + public isCurrentVersionHigherOrEqual(versionToCompare: string) { + if (!isValidVersionFormat(versionToCompare)) { + return false; + } + + const matchPattern = /(\d+)\.(\d+)\.(\d+)/; + + type ExpectedTupple = [string, string, string, string]; + + const [, ...appVersionComponents] = this._appVersion.match(matchPattern) as ExpectedTupple; // Формат версии проверен в конструкторе, можно смело убирать `null` из типа. + + const [, ...versionToCompareComponents] = versionToCompare.match( + matchPattern, + ) as ExpectedTupple; + + for (let i = 0; i < appVersionComponents.length; i++) { + if (appVersionComponents[i] !== versionToCompareComponents[i]) { + return appVersionComponents[i] >= versionToCompareComponents[i]; + } + } + + return true; + } + + /** + * Сохраняет текущее состояние BridgeToNative в sessionStorage. + * Так же сохраняет текущее состояние nativeNavigationAndTitle. + */ + private saveCurrentState() { + // В nativeNavigationAndTitle этот метод отмечен модификатором доступа private дабы не торчал наружу, но тут его нужно вызвать + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this._nativeNavigationAndTitle.saveCurrentState(); + + const currentState: PreviousBridgeToNativeState = { + appVersion: this._appVersion, + theme: this._theme, + nextPageId: this.nextPageId, + originalWebviewParams: this._originalWebviewParams || '', + iosAppId: this._iosAppId, + }; + + sessionStorage.setItem(PREVIOUS_B2N_STATE_STORAGE_KEY, JSON.stringify(currentState)); + } + + /** + * Возвращает схему приложения в iOS окружении, на основе версии. + * + * @param knownIosAppId Тип iOS приложения, если он известен. + * @returns Тип приложения, `undefined` для Android окружения. + */ + private getIosAppId(knownIosAppId?: string) { + if (this.environment !== 'ios') { + return undefined; + } + + if (knownIosAppId) { + return knownIosAppId; + } + + const keys = Object.keys(versionToIosAppId); + + const rightKey = + [...keys].reverse().find((version) => this.isCurrentVersionHigherOrEqual(version)) || + keys[0]; + + return atob(versionToIosAppId[rightKey as keyof typeof versionToIosAppId]); + } + + /** + * Восстанавливает свое предыдущее состояние из sessionStorage + */ + private restorePreviousState() { + const previousState: PreviousBridgeToNativeState = JSON.parse( + sessionStorage.getItem(PREVIOUS_B2N_STATE_STORAGE_KEY) || '', + ); + + this._appVersion = previousState.appVersion; + this._iosAppId = previousState.iosAppId; + this._theme = previousState.theme; + this._originalWebviewParams = previousState.originalWebviewParams; + this.nextPageId = previousState.nextPageId; + this._nativeNavigationAndTitle = new NativeNavigationAndTitle( + this, + previousState.nextPageId, + '', + this._handleRedirect, + ); + + sessionStorage.removeItem(PREVIOUS_B2N_STATE_STORAGE_KEY); + } +} diff --git a/src/index.ts b/src/index.ts index b4fa615..683d027 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,229 +1,2 @@ -/* eslint-disable no-underscore-dangle */ - -import { - CLOSE_WEBVIEW_SEARCH_KEY, - CLOSE_WEBVIEW_SEARCH_VALUE, - nativeFeaturesFromVersion, - PREVIOUS_B2N_STATE_STORAGE_KEY, - versionToIosAppId, -} from './constants'; -import { NativeFallbacks } from './native-fallbacks'; -import { NativeNavigationAndTitle } from './native-navigation-and-title'; -import type { - Environment, - HandleRedirect, - NativeFeatureKey, - NativeParams, - WebViewWindow, -} from './types'; -import { PreviousBridgeToNativeState } from './types'; -import { isValidVersionFormat } from './utils'; - -type Theme = 'light' | 'dark'; - -/** - * Этот класс — абстракция для связи веб приложения с нативом и предназначен ТОЛЬКО - * для использования в вебвью окружении. - */ -export class BridgeToNative { - // Webview, запущенное в Android окружении имеет объект `Android` в window. - public readonly AndroidBridge = (window as WebViewWindow).Android; - - public readonly environment: Environment = this.AndroidBridge ? 'android' : 'ios'; - - public readonly nativeFallbacks: NativeFallbacks; - - private nextPageId: number | null; - - private _nativeNavigationAndTitle: NativeNavigationAndTitle; - - private _originalWebviewParams: string; - - // В формате `x.x.x`. - private _appVersion: string; - - // Необходимо для формирования диплинка. - private _iosAppId?: string; - - private _theme: Theme; - - private readonly _blankPagePath: string; - - private readonly _handleRedirect: HandleRedirect; - - constructor( - handleRedirect: HandleRedirect, - blankPagePath: string, - nativeParams?: NativeParams, - ) { - const previousState = !!sessionStorage.getItem(PREVIOUS_B2N_STATE_STORAGE_KEY); - - if (previousState) { - this.restorePreviousState(); - this.nativeFallbacks = new NativeFallbacks(this); - this._blankPagePath = blankPagePath; - - return; - } - - this._appVersion = - nativeParams && isValidVersionFormat(nativeParams?.appVersion) - ? nativeParams.appVersion - : '0.0.0'; - this._iosAppId = this.getIosAppId(nativeParams?.iosAppId); - this._theme = nativeParams?.theme === 'dark' ? 'dark' : 'light'; - this._originalWebviewParams = nativeParams?.originalWebviewParams || ''; - this._nativeNavigationAndTitle = new NativeNavigationAndTitle( - this, - nativeParams ? nativeParams.nextPageId : null, - nativeParams?.title, - handleRedirect, - ); - this._handleRedirect = handleRedirect; - - this.nextPageId = nativeParams ? nativeParams.nextPageId : null; - this.nativeFallbacks = new NativeFallbacks(this); - this._blankPagePath = blankPagePath; - } - - get theme() { - return this._theme; - } - - get appVersion() { - return this._appVersion; - } - - get iosAppId() { - return this._iosAppId; - } - - get nativeNavigationAndTitle() { - return this._nativeNavigationAndTitle; - } - - get originalWebviewParams() { - return this._originalWebviewParams; - } - - /** - * Метод, проверяющий, можно ли использовать нативную функциональность в текущей версии приложения. - * - * @param feature Название функциональности, которую нужно проверить. - */ - public canUseNativeFeature(feature: NativeFeatureKey) { - const { fromVersion } = nativeFeaturesFromVersion[this.environment][feature]; - - return this.isCurrentVersionHigherOrEqual(fromVersion); - } - - /** - * Метод, отправляющий сигнал нативу, что нужно закрыть текущее вебвью. - */ - // eslint-disable-next-line class-methods-use-this - public closeWebview() { - const originalPageUrl = new URL(window.location.href); - - originalPageUrl.searchParams.set(CLOSE_WEBVIEW_SEARCH_KEY, CLOSE_WEBVIEW_SEARCH_VALUE); - window.location.href = originalPageUrl.toString(); - } - - /** - * Сравнивает текущую версию приложения с переданной. - * - * @param versionToCompare Версия, с которой нужно сравнить текущую. - * @returns `true` – текущая версия больше или равняется переданной, - * `false` – текущая версия ниже. - */ - public isCurrentVersionHigherOrEqual(versionToCompare: string) { - if (!isValidVersionFormat(versionToCompare)) { - return false; - } - - const matchPattern = /(\d+)\.(\d+)\.(\d+)/; - - type ExpectedTupple = [string, string, string, string]; - - const [, ...appVersionComponents] = this._appVersion.match(matchPattern) as ExpectedTupple; // Формат версии проверен в конструкторе, можно смело убирать `null` из типа. - - const [, ...versionToCompareComponents] = versionToCompare.match( - matchPattern, - ) as ExpectedTupple; - - for (let i = 0; i < appVersionComponents.length; i++) { - if (appVersionComponents[i] !== versionToCompareComponents[i]) { - return appVersionComponents[i] >= versionToCompareComponents[i]; - } - } - - return true; - } - - /** - * Сохраняет текущее состояние BridgeToNative в sessionStorage. - * Так же сохраняет текущее состояние nativeNavigationAndTitle. - */ - private saveCurrentState() { - // В nativeNavigationAndTitle этот метод отмечен модификатором доступа private дабы не торчал наружу, но тут его нужно вызвать - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this._nativeNavigationAndTitle.saveCurrentState(); - - const currentState: PreviousBridgeToNativeState = { - appVersion: this._appVersion, - theme: this._theme, - nextPageId: this.nextPageId, - originalWebviewParams: this._originalWebviewParams || '', - iosAppId: this._iosAppId, - }; - - sessionStorage.setItem(PREVIOUS_B2N_STATE_STORAGE_KEY, JSON.stringify(currentState)); - } - - /** - * Возвращает схему приложения в iOS окружении, на основе версии. - * - * @param knownIosAppId Тип iOS приложения, если он известен. - * @returns Тип приложения, `undefined` для Android окружения. - */ - private getIosAppId(knownIosAppId?: string) { - if (this.environment !== 'ios') { - return undefined; - } - - if (knownIosAppId) { - return knownIosAppId; - } - - const keys = Object.keys(versionToIosAppId); - - const rightKey = - [...keys].reverse().find((version) => this.isCurrentVersionHigherOrEqual(version)) || - keys[0]; - - return atob(versionToIosAppId[rightKey as keyof typeof versionToIosAppId]); - } - - /** - * Восстанавливает свое предыдущее состояние из sessionStorage - */ - private restorePreviousState() { - const previousState: PreviousBridgeToNativeState = JSON.parse( - sessionStorage.getItem(PREVIOUS_B2N_STATE_STORAGE_KEY) || '', - ); - - this._appVersion = previousState.appVersion; - this._iosAppId = previousState.iosAppId; - this._theme = previousState.theme; - this._originalWebviewParams = previousState.originalWebviewParams; - this.nextPageId = previousState.nextPageId; - this._nativeNavigationAndTitle = new NativeNavigationAndTitle( - this, - previousState.nextPageId, - '', - this._handleRedirect, - ); - - sessionStorage.removeItem(PREVIOUS_B2N_STATE_STORAGE_KEY); - } -} +export { BridgeToNative } from './bridge-to-native'; +export { NativeParams, Theme, Environment, NativeFeatureKey, PdfType } from './types'; diff --git a/src/native-fallbacks.ts b/src/native-fallbacks.ts index 004d902..48e2461 100644 --- a/src/native-fallbacks.ts +++ b/src/native-fallbacks.ts @@ -1,6 +1,6 @@ -import type { BridgeToNative } from '.'; import { PdfType } from './types'; import { getUrlInstance } from './utils'; +import type { BridgeToNative } from './bridge-to-native'; /** * Класс содержит реализацию обходных путей для веб-фич, которые не работают в нативном-вебвью. diff --git a/src/native-navigation-and-title.ts b/src/native-navigation-and-title.ts index f4793c5..5bdd8b7 100644 --- a/src/native-navigation-and-title.ts +++ b/src/native-navigation-and-title.ts @@ -4,7 +4,7 @@ import { } from './constants'; import { HandleRedirect, PreviousNativeNavigationAndTitleState, SyncPurpose } from './types'; import { extractAppNameRouteAndQuery } from './utils'; -import { BridgeToNative } from '.'; +import { BridgeToNative } from './bridge-to-native'; /** * Класс, отвечающий за взаимодействие с нативными элементами в приложении – заголовком и нативной кнопкой назад. diff --git a/src/types.ts b/src/types.ts index cf34fb2..717a39e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -52,3 +52,5 @@ export type HandleRedirect = ( path?: string, params?: Record, ) => void; + +export type Theme = 'light' | 'dark'; diff --git a/test/index.test.ts b/test/index.test.ts index bb912bb..0541d3f 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,12 +1,12 @@ /* eslint-disable @typescript-eslint/dot-notation -- отключено, чтобы можно было обращаться к приватным полям для их тестирования */ -import { BridgeToNative } from '../src'; +import { BridgeToNative } from '../src/bridge-to-native'; import { CLOSE_WEBVIEW_SEARCH_KEY, CLOSE_WEBVIEW_SEARCH_VALUE, PREVIOUS_B2N_STATE_STORAGE_KEY, } from '../src/constants'; -import { mockSessionStorage } from '../src/mock/mock-session-storage'; +import { mockSessionStorage } from './mock/mock-session-storage'; import { WebViewWindow } from '../src/types'; const mockedNativeFallbacksInstance = {}; diff --git a/test/integration.test.ts b/test/integration.test.ts index fe150b1..7546823 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/dot-notation -- отключено, чтобы можно было обращаться к приватным полям для их тестирования */ -import { BridgeToNative } from '../src'; +import { BridgeToNative } from '../src/bridge-to-native'; describe('BridgeToNative integration testing', () => { const defaultAmParams = { diff --git a/src/mock/mock-session-storage.ts b/test/mock/mock-session-storage.ts similarity index 100% rename from src/mock/mock-session-storage.ts rename to test/mock/mock-session-storage.ts diff --git a/test/native-fallbacks.test.ts b/test/native-fallbacks.test.ts index 12908f5..88b3b0e 100644 --- a/test/native-fallbacks.test.ts +++ b/test/native-fallbacks.test.ts @@ -1,4 +1,4 @@ -import type { BridgeToNative } from '../src'; +import type { BridgeToNative } from '../src/bridge-to-native'; import { nativeFeaturesFromVersion } from '../src/constants'; import { NativeFallbacks } from '../src/native-fallbacks'; import { PdfType } from '../src/types'; @@ -24,7 +24,7 @@ const mockedBridgeToAmInstance = { }, } as unknown as BridgeToNative; -jest.mock('../src', () => ({ +jest.mock('../src/bridge-to-native', () => ({ __esModule: true, BridgeToNative: function MockedBridgeToAmConstructor() { return mockedBridgeToAmInstance; diff --git a/test/native-navigation-and-title.test.ts b/test/native-navigation-and-title.test.ts index 5b6b152..9a549d2 100644 --- a/test/native-navigation-and-title.test.ts +++ b/test/native-navigation-and-title.test.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/dot-notation -- отключено, чтобы можно было обращаться к приватным полям для их тестирования */ -import type { BridgeToNative } from '../src'; +import type { BridgeToNative } from '../src/bridge-to-native'; import { PREVIOUS_NATIVE_NAVIGATION_AND_TITLE_STATE_STORAGE_KEY } from '../src/constants'; -import { mockSessionStorage } from '../src/mock/mock-session-storage'; +import { mockSessionStorage } from './mock/mock-session-storage'; import { NativeNavigationAndTitle } from '../src/native-navigation-and-title'; let androidEnvFlag = false; @@ -36,7 +36,7 @@ Object.defineProperty(global, 'handleRedirect', { configurable: true, }); -jest.mock('../src', () => ({ +jest.mock('../src/bridge-to-native', () => ({ __esModule: true, BridgeToNative: function MockedBridgeToAmConstructor() { return mockedBridgeToNativeInstance; diff --git a/tsconfig.build.json b/tsconfig.build.json index 533eb6c..be81d7e 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -7,5 +7,5 @@ "composite": false, "removeComments": false }, - "exclude": ["test/**/*.*", "src/mock/*.*"] + "exclude": ["test/**/*.*"] }