import cookies from 'js-cookie' import semver from 'semver' import { COOKIE_ROOT_DOMAIN, COOKIE_APP_KEY, COOKIE_APP_EXTRA_KEY, COOKIE_ACCESS_TOKEN_KEY, MSG_REQUIRE_LOGIN, APP_INJECT_COOKIE_KEYS, DEF_SYNCED_COOKIE_EXP } from './constant' import { Notifier, SyncCookiesOptions } from '../types/global' const setupWebViewJavascriptBridge = (callback) => { if (window.WebViewJavascriptBridge) { return callback(window.WebViewJavascriptBridge) } if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback) } window.WVJBCallbacks = [callback] var WVJBIframe = document.createElement('iframe') WVJBIframe.style.display = 'none' WVJBIframe.src = 'https://__bridge_loaded__' document.documentElement.appendChild(WVJBIframe) setTimeout(function () { document.documentElement.removeChild(WVJBIframe) }, 0) } const parseJWT = (token: string): any => { const payloadStr = token.split('.')[1] if (!payloadStr) { return null } try { return JSON.parse(atob(payloadStr)) } catch (e) { // tslint:disable-next-line console.error('[parseJWT]', e) } return null } const getAppInfo = () => { const val = cookies.get(COOKIE_APP_KEY) const matched = val && /^(ios|android)\ ((?:\d\.?)+)$/.exec(val) if (!matched) { return null } return { os: matched[1], version: matched[2] } } const getAppExtra = (): any => { const val = cookies.get(COOKIE_APP_EXTRA_KEY) const segments = val && val.split(/\s+/) if (!segments || !segments.length) { return {} } return segments.reduce((map, item) => { const matched = /^([0-9a-zA-Z_-]+)\/(.+)$/.exec(item) if (matched) { map[matched[1]] = matched[2] } return map }, {}) } let cachedAppInfo: any let cachedappExtra: any class ProginnBridge { root = window.app_event || window.appBridge private notifier?: Notifier constructor(opts?: { notifier?: Notifier }) { const { notifier } = opts || {} this.notifier = notifier } get appInfo() { cachedAppInfo = cachedAppInfo || getAppInfo() return cachedAppInfo } get appExtra() { cachedappExtra = cachedappExtra || getAppExtra() return cachedappExtra } get appVersion() { return this.appInfo?.version } get os() { return this.appInfo?.os } get isInApp() { return !!(this.appInfo || this.root) } get isAndroid() { return /Android/.test(window.navigator.userAgent) || this.os === 'android' || false } get isIos() { return /iP(hone|ad|od)/.test(window.navigator.userAgent) || this.os === 'ios' || false } get cookie() { return cookies.get() } get isLogined() { return !!cookies.get(COOKIE_ACCESS_TOKEN_KEY) } get uid(): string | null { const token = cookies.get(COOKIE_ACCESS_TOKEN_KEY) const payload = token && parseJWT(token) return payload?.uid } inject(name: string, cb: (...args: any) => void, root = 'Proginn') { window[root] = window[root] || {} window[root][name] = cb } invoke(fn: string, data: any = null, cb: Function = () => {}) { if (this.isIos && this.compareAppVersion('gte', '4.22.0')) { setupWebViewJavascriptBridge((bridge) => { bridge.callHandler(fn, data, cb) }) return } if (!this.root) { // tslint:disable-next-line console.warn(`Bridge invoke ${fn} skipped.`) return null } if (this.isAndroid) { if (typeof this.root[fn] === 'function') { return data ? this.root[fn](data) : this.root[fn]() } else { return null } } else { return this.root(fn, data) } } back() { if (!this.isInApp) { window.history.back() } else { if (this.isIos && this.compareAppVersion('gte', '4.22.0')) { this.invoke('back') } else { this.invoke('back_page') } } } close() { if (this.isAndroid || this.compareAppVersion('lt', '4.22.0')) { this.invoke('finishActivity') } else { this.invoke('close') } } load(url: string) { window.location.href = url } open(url: string, title?: string) { if (this.isInApp && (!this.isAndroid || this.compareAppVersion('gte', '4.20.0'))) { url = `proginn://webview?url=${encodeURIComponent(url)}${title ? '&title=' + encodeURIComponent(title) : ''}` } this.load(url) } login() { if (!this.isInApp) { return } this.load('proginn://login?backToPage=true') } checkLogin(force = false) { if (force || !this.isLogined) { this.notifier && this.notifier(MSG_REQUIRE_LOGIN, -99) this.login() return false } return true } compareAppVersion(operator: 'gt' | 'lt' | 'gte' | 'lte' | 'eq' | 'neq', version: string) { return this.appVersion ? semver[operator](this.appVersion, version) : false } syncCookies(opts?: SyncCookiesOptions) { opts = opts || {} opts.domain = opts.domain || COOKIE_ROOT_DOMAIN opts.expires = opts.expires || DEF_SYNCED_COOKIE_EXP for (const key of APP_INJECT_COOKIE_KEYS) { const val = cookies.get(key) if (val) { cookies.set(key, val, opts) } } } // generally to fix android webview cookies non-inject bug cacheCookiesInStorage() { for (const key of APP_INJECT_COOKIE_KEYS) { const val = cookies.get(key) if (val) { window.localStorage.setItem(key, val) } else { window.localStorage.removeItem(key) } } } loadCookiesInStorage(opts?: SyncCookiesOptions) { opts = opts || {} opts.domain = opts.domain || COOKIE_ROOT_DOMAIN opts.expires = opts.expires || DEF_SYNCED_COOKIE_EXP for (const key of APP_INJECT_COOKIE_KEYS) { // pass if already exists if (cookies.get(key)) { continue } const val = window.localStorage.getItem(key) if (val) { cookies.set(key, val, opts) } } } // load data loadUserData(data: any) { if (this.isAndroid) { this.invoke('user_load', data) } else if (this.compareAppVersion('lt', '4.22.0')) { this.invoke('user_load', { userInfo: data, }) } else { this.invoke('loadUserData', data) } } loadShareData(data: any) { if (this.isAndroid) { this.invoke('load_share_data', JSON.stringify(data)) } else if (this.compareAppVersion('lt', '4.22.0')) { this.invoke('load_share_data', data) } else { this.invoke('loadShareData', data) } } loadTopicData(data: { topic_id: string user_id: string share_content: any topics: any[] }) { if (this.isAndroid) { this.invoke('topic_load', data.topic_id) } else if (this.compareAppVersion('lt', '4.22.0')) { this.invoke('topic_load', data) } else { this.invoke('loadTopicData', data) } } // ui setNavigationBarColor(hex: string) { if (this.isAndroid || this.compareAppVersion('lt', '4.22.0')) { this.invoke('setTitleBarColor', hex) } else { this.invoke('setNavigationBarColor', hex) } } setNavigationBarTitle(text: string) { this.invoke('setNavigationBarTitle', text) } } export default ProginnBridge