-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(*): add server logic for detect webview
- Loading branch information
Andrei Takarski
committed
Oct 31, 2024
1 parent
00a10e9
commit f24b65d
Showing
26 changed files
with
930 additions
and
20 deletions.
There are no files selected for viewing
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
export { BridgeToNative } from './bridge-to-native'; | ||
export { NativeParams, Theme, Environment, NativeFeatureKey, PdfType } from './types'; | ||
export { BridgeToNative } from './client/bridge-to-native'; | ||
export { NativeParams, Theme, Environment, NativeFeatureKey, PdfType } from './client/types'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { | ||
extractAppVersion, | ||
extractUserAgent, | ||
isAkeyWebview, | ||
extractNativeParamsFromCookies | ||
} from './utils'; | ||
import { RequestHeaderType } from './types'; | ||
import { versionPattern, webviewUaIOSPattern } from './reg-exp-patterns'; | ||
|
||
export const isWebviewByUserAgent = ( | ||
userAgent: string, | ||
appVersion: string | undefined, | ||
) => { | ||
if (userAgent && isAkeyWebview(userAgent)) { | ||
return false; | ||
} | ||
|
||
return ( | ||
(appVersion && versionPattern.test(appVersion)) || !!userAgent?.match(webviewUaIOSPattern) | ||
); | ||
}; | ||
|
||
export const isWebviewByCookies = (nativeParamsFromCookies: Record<string, any> | null) => { | ||
return !!(nativeParamsFromCookies && nativeParamsFromCookies.isWebview) | ||
} | ||
export const checkIsWebview = ( | ||
request: RequestHeaderType, | ||
): boolean => { | ||
const userAgent = extractUserAgent(request); | ||
|
||
// `app-version` в заголовках – индикатор вебвью. В iOS есть только в первом запросе от webview | ||
const appVersion = extractAppVersion(request); | ||
const nativeParamsFromCookies = extractNativeParamsFromCookies(request); | ||
|
||
return isWebviewByCookies(nativeParamsFromCookies) || isWebviewByUserAgent(userAgent, appVersion); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export const THEME_QUERY = 'theme'; | ||
export const TITLE = 'title'; | ||
|
||
export const WEBVIEW_IOS_APP_ID_QUERY = 'applicationId'; | ||
export const WEBVIEW_IOS_APP_VERSION_QUERY = 'device_app_version'; | ||
export const WEBVIEW_WITHOUT_LAYOUT_QUERY = 'without_layout'; | ||
export const WEBVIEW_NEXT_PAGE_ID_QUERY = 'nextPageId'; | ||
|
||
export const CAPSULE_UA_SUBSTR = 'Capsule'; | ||
export const AKEY_UA_SUBSTR = 'AKEY'; | ||
export const VOSKHOD_UA_SUBSTR = 'VOSKHOD'; | ||
|
||
export const NATIVE_PARAMS_COOKIE_NAME = 'app_native_params'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { | ||
THEME_QUERY, | ||
TITLE, | ||
WEBVIEW_IOS_APP_ID_QUERY, | ||
WEBVIEW_IOS_APP_VERSION_QUERY, | ||
WEBVIEW_NEXT_PAGE_ID_QUERY, | ||
WEBVIEW_WITHOUT_LAYOUT_QUERY, | ||
NATIVE_PARAMS_COOKIE_NAME | ||
} from './constants'; | ||
|
||
import { extractAppVersion } from './utils'; | ||
|
||
import { extractAndJoinOriginalWebviewParams } from './extract-and-join-original-webview-params'; | ||
import { checkIsWebview } from './check-is-webview'; | ||
import { iosAppIdPattern, versionPattern } from './reg-exp-patterns'; | ||
import { EmptyNativeParams, NativeParams, RequestHeaderType } from "./types"; | ||
|
||
/** | ||
* Вытаскивает из query и headers все детали для вебвью. | ||
* | ||
* @returns Примечание по `appVersion`: В вебвью окружении версия всегда имеет формат `x.x.x`. | ||
*/ | ||
|
||
export const detectAndExtractNativeParams = ( | ||
request: RequestHeaderType, | ||
addCookie?: (cookieKey: string, cookieValue: string) => void | ||
): EmptyNativeParams | NativeParams => { | ||
const isWebview = checkIsWebview(request); | ||
|
||
if (!isWebview) { | ||
return { isWebview } as EmptyNativeParams; | ||
} | ||
|
||
const { | ||
[THEME_QUERY]: themeQuery, | ||
// При желании через диплинк на вебвью можно передать желаемый заголовок, | ||
// который АО установит в верхней АМ панели при загрузке АО. | ||
// По умолчанию нужна именно пустая строка. | ||
[TITLE]: title = '', | ||
// Говорят, этого может и не быть в урле. Формат `com.xxxxxxxxx.app`. | ||
[WEBVIEW_IOS_APP_ID_QUERY]: iosAppIdQuery, | ||
[WEBVIEW_IOS_APP_VERSION_QUERY]: iosAppVersionQuery, | ||
[WEBVIEW_WITHOUT_LAYOUT_QUERY]: withoutLayoutQuery, | ||
[WEBVIEW_NEXT_PAGE_ID_QUERY]: nextPageId, | ||
} = request.query as Record<string, string>; | ||
|
||
const originalWebviewParams = extractAndJoinOriginalWebviewParams( | ||
request.query as Record<string, string>, | ||
); | ||
|
||
// Пробуем вытащить схему iOS приложения из query, если есть. | ||
let iosAppId; | ||
|
||
if (iosAppIdPattern.test(iosAppIdQuery)) { | ||
const [, appIdSubsting] = iosAppIdQuery.match(iosAppIdPattern) as string[]; | ||
|
||
iosAppId = appIdSubsting; | ||
} | ||
|
||
// Определяем версию АМ из query или заголовка. | ||
let appVersion = '0.0.0'; | ||
|
||
const appVersionFromHeaders = extractAppVersion(request); | ||
|
||
if (typeof iosAppVersionQuery === 'string' && versionPattern.test(iosAppVersionQuery)) { | ||
appVersion = iosAppVersionQuery; | ||
} else if ( | ||
typeof appVersionFromHeaders === 'string' && | ||
versionPattern.test(appVersionFromHeaders) | ||
) { | ||
const [, versionSubstring] = appVersionFromHeaders.match(versionPattern) || []; | ||
|
||
appVersion = versionSubstring; | ||
} | ||
|
||
const nativeParams = { | ||
appVersion, | ||
iosAppId, | ||
isWebview, | ||
theme: themeQuery === 'dark' ? 'dark' : 'light', | ||
title, | ||
withoutLayout: withoutLayoutQuery === 'true', | ||
originalWebviewParams, | ||
nextPageId: nextPageId ? Number(nextPageId) : null, | ||
} as NativeParams; | ||
|
||
if(addCookie) { | ||
addCookie(NATIVE_PARAMS_COOKIE_NAME, encodeURIComponent(JSON.stringify(nativeParams))); | ||
} | ||
|
||
return nativeParams; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
// Словарь всех известных на данный момент сервисных query параметров в webview | ||
const webviewInitParamsDictionary = [ | ||
'device_app_version', | ||
'device_os_version', | ||
'device_boot_time', | ||
'device_timezone', | ||
'applicationId', | ||
'device_app_id', | ||
'device_locale', | ||
'paySupported', | ||
'device_model', | ||
'device_uuid', | ||
'device_name', | ||
'device_id', | ||
'client_id', | ||
'theme', | ||
'scope', | ||
]; | ||
|
||
/** | ||
* Данная утилита извлекает из Fastify.Query все известные | ||
* сервисные query параметры которые добавляются к url внутри | ||
* webview при первой инициализации и собирает их в query строку. | ||
* | ||
* @param query - Fastify.Query в формате объекта | ||
* @return строка query параметров в формате: "title=Title&theme=dark..." | ||
* */ | ||
export const extractAndJoinOriginalWebviewParams = ( | ||
query: Record<string, string>, | ||
): string => { | ||
const params = new URLSearchParams(); | ||
|
||
webviewInitParamsDictionary.forEach((key) => { | ||
const value = query[key]; | ||
|
||
if (value) { | ||
params.set(key, value); | ||
} | ||
}); | ||
|
||
return params.toString(); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export * from './detect-and-extract-native-params'; | ||
export * from './check-is-webview'; | ||
export * from './types'; | ||
export * from './reg-exp-patterns'; | ||
export * from './extract-and-join-original-webview-params'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export const iosAppIdPattern = /^com\.([a-z]+)\.app$/; | ||
|
||
// Android приписывает после версии тип билда, например `feature`. Нам эта инфа не нужна. | ||
export const versionPattern = /^(\d+\.\d+\.\d+)(\s.+)?$/; | ||
|
||
export const webviewUaIOSPattern = new RegExp( | ||
['WebView', '(iPhone|iPod|iPad)(?!.*Safari)'].join('|'), | ||
'ig', | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
export type RequestHeaderType = Record<string, any>; | ||
|
||
export type EmptyNativeParams = { | ||
isWebview: false; | ||
}; | ||
|
||
export type NativeParams = { | ||
appVersion: string; | ||
iosAppId?: string; | ||
isWebview: true; | ||
theme: 'dark' | 'light'; | ||
title: string; | ||
withoutLayout: boolean; | ||
originalWebviewParams: string; | ||
nextPageId: number | null; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { | ||
CAPSULE_UA_SUBSTR, | ||
AKEY_UA_SUBSTR, | ||
VOSKHOD_UA_SUBSTR, | ||
NATIVE_PARAMS_COOKIE_NAME | ||
} from './constants'; | ||
import { RequestHeaderType } from './types'; | ||
|
||
/** | ||
* Заголовок с версией приложения, который посылает вебвью из AM Android | ||
*/ | ||
const AppVersion = 'app-version'; | ||
|
||
/** | ||
* Возвращает `app-version` из заголовков запроса | ||
*/ | ||
export function extractAppVersion(request: RequestHeaderType): string | undefined { | ||
return request.headers[AppVersion]; | ||
} | ||
|
||
/** | ||
* Возвращает `User-agent` из заголовков запроса | ||
*/ | ||
export function extractUserAgent(request: RequestHeaderType): string { | ||
return request.headers['user-agent']; | ||
} | ||
|
||
/** | ||
* Возвращает объект с `webview-параметрами` из cookies | ||
*/ | ||
export function extractNativeParamsFromCookies(request: RequestHeaderType): Record<string, string> | null { | ||
const cookieHeader = request.headers['cookie']; | ||
|
||
if (!cookieHeader) { | ||
return {}; | ||
} | ||
|
||
const cookiesArray = cookieHeader.split('; '); | ||
const cookieString = cookiesArray.find((cookie: string) => cookie.startsWith(`${NATIVE_PARAMS_COOKIE_NAME}=`)); | ||
|
||
if (!cookieString) return null; | ||
|
||
const [, value] = cookieString.split('='); | ||
|
||
try { | ||
return JSON.parse(decodeURIComponent(value)); | ||
} catch { | ||
return null; | ||
} | ||
} | ||
|
||
|
||
/** | ||
* Проверка по юзерагенту на сервере | ||
*/ | ||
export const isAkeyWebview = (userAgent: string) => | ||
userAgent.includes(CAPSULE_UA_SUBSTR) || | ||
userAgent.includes(AKEY_UA_SUBSTR) || | ||
userAgent.includes(VOSKHOD_UA_SUBSTR); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.