Commit 156cad2d authored by Jim Lin's avatar Jim Lin
Browse files

[ANUE-2010] Add util webview

parent 22b344cc
......@@ -16,6 +16,8 @@
"tslint.configFile": "tslint.json",
"workbench.colorCustomizations": {
"activityBar.background": "#ea6242",
"activityBar.activeBackground": "#ea6242",
"activityBar.activeBorder": "#36e958",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#36e958",
......@@ -26,7 +28,9 @@
"titleBar.inactiveForeground": "#e7e7e799",
"statusBar.background": "#e03f19",
"statusBarItem.hoverBackground": "#ea6242",
"statusBar.foreground": "#e7e7e7"
"statusBar.foreground": "#e7e7e7",
"statusBar.border": "#e03f19",
"titleBar.border": "#e03f19"
},
"peacock.color": "#e03f19"
}
\ No newline at end of file
......@@ -31,4 +31,14 @@ $ yarn analyze
```
yarn publish-npm v${VERSION_CODE}
```
## Webview傳遞資訊
專案除了使用在網頁上之外也被使用在鉅亨網的Android/iOS app中,因此我們和app
團隊有著一套傳遞訊息的協定,在專案裡面也對這個協定的實踐寫成了一個模組。
主要的實作可以參考[utils/webview.ts]
// TODO: 將 portal 那邊的 native.send 統一到 SDK。
```
\ No newline at end of file
......@@ -10,6 +10,7 @@ require('@utils/rpc');
require('@utils/res');
require('@utils/obfuscator');
require('@utils/paramly');
require('@utils/webview');
require('@utils/sso');
require('@utils/urly');
require('@utils/urld');
......
{
"name": "anue-fe-sdk",
"version": "3.3.1",
"version": "3.3.2",
"description": "Anue SDK for developers",
"scripts": {
"start": "tsc -w -p ./",
......
......@@ -25,7 +25,10 @@ namespace Anue.Auth {
time: number;
} | null;
// Merge the memberships and the user profile from
// `member-always-right.api.cnyes.com/member-always-right/api/v1/user/profile?product=cnyes,driver,video`
interface UserProfile {
// response -> items.users
avatar: string;
name: string;
email: string;
......@@ -45,6 +48,41 @@ namespace Anue.Auth {
postalCode?: string;
country?: string;
address?: string;
// response -> items.cnyes + items.driver + items.video
memberships?: Memberships;
}
interface Memberships {
cnyes?: CnyesMembership;
driver?: DriverMembership;
video?: VideoMembership;
}
// interface CnyesMembership {
// // TODO: Complete the API format
// }
interface DriverMembership {
product_id: string;
is_trial: boolean;
// TODO: Complete the API format
}
enum OrderStatus {
CREATED = 'CREATED', // 已建立
AUTHORIZED = 'AUTHORIZED', // 已授權
CAPTURED = 'CAPTURED', // 已付款
REFUNDED = 'REFUNDED' // 已退款
}
interface VideoMembership {
id: string;
course_id: string;
status: OrderStatus;
payment_type: 'CREDIT' | 'COUPON';
amount: number;
coupon_code: string;
// TODO: Complete the API format
}
interface Config {
......@@ -58,6 +96,9 @@ namespace Anue.Auth {
singleProvider?: boolean;
rpcHost?: string;
alwaysSendToken?: boolean;
token?: string;
flag?: string;
isWebView?: boolean;
}
interface StateChangeEvent {
......@@ -100,6 +141,9 @@ declare class AuthService {
updateEmail(email: string): Promise<{ error: any }>;
isLogin(): boolean;
optimisticallyGetProfile(): Anue.Auth.UserProfile;
syncProfileFromRemote(shouldRenewProfile?: boolean): Anue.Auth.UserProfile;
setAuthToken(token: string, flag?: string): void;
setIsInWebView(isWebView: boolean): void;
}
type AnueAuth = new (config: Anue.Auth.Config = {}) => AuthService;
......
namespace Anue.Shared {
declare interface SharedContext {
set(k: string, v: any): void;
createStatic<T>(key, val): SharedStatic<T>;
......@@ -14,12 +13,13 @@ namespace Anue.Shared {
get(name: 'utils.isomorphy'): Anue.Utils.Isomorphy;
get(name: 'utils.rpc'): Anue.Utils.RPC;
get(name: 'utils.res'): Anue.Utils.Res;
get(name: 'utils.webview'): Anue.Utils.WebView;
get(name: 'utils.waitty'): Anue.Utils.Waitty;
get(name: 'utils.paramly'): Anue.Utils.Paramly;
get(name: 'utils.obfuscator'): Anue.Utils.Obfuscator;
get(name: 'library.net.abstract'): Anue.Network.DriverConstructor;
get(name: 'library.net'): Anue.Network.PackageDefine;
get(name: 'packages.imc'): typeof import ('anue-fe-sdk/IMC');
get(name: 'packages.imc'): Anue.IMC;
get(name: 'packages.auth'): AnueAuth;
get(name: 'packages.otac'): Anue.OTAC.OTACManager;
get(name: 'extern.trackjs'): Anue.Tracking.Compat;
......
......@@ -53,6 +53,29 @@ namespace Anue.Utils {
}
type Urly = (...args: string[]) => string;
// TODO: Move the implementation to here from fe-portal.
type NativeEventName =
| 'closeWebView'
| 'anueRequestRefreshToken'
| 'anueMemberLevelChanged'
| 'anueAlert';
interface NativeEventListener {
webViewStatus(): void;
webViewRefreshToken(): void;
}
interface NativeGet {
webViewStatus(): void;
webViewRefreshToken(): string;
}
interface WebView {
send(eventName: NativeEventName, payload?: Record<string, any>): void;
addListener: NativeEventListener;
get: NativeGet;
}
interface Urld {
init: () => void;
parse: () => Record<string, string>;
......
interface Window {
webViewToken: string;
webViewStatus: 'onResume' | 'onPause' | number;
appCallWebCurrentStatus: (status: number) => void;
appCallWebRefreshToken: (token: string) => void;
}
type ProviderResponse = Anue.Provider.NullableResponse;
type ChangeHandler = Anue.Provider.ChangeHandler;
type EventyHandler<T = any> = (e: T) => void;
......@@ -8,6 +14,11 @@ declare function mockAs<T>(target: any): T;
declare const DEBUG: boolean;
declare const native: {
event: (str: string) => void;
member_event: (str: string) => void;
};
declare const anue: {
shared: Anue.Shared.SharedContext;
urld?: Record<string, any>;
......@@ -23,6 +34,18 @@ declare module 'anue-fe-sdk/getty' {
export = getty;
}
declare module 'anue-fe-sdk/webview' {
const webview: {
send(
eventName: Anue.Utils.NativeEventName,
payload?: Record<string, any>
): void;
addListener: Anue.Utils.NativeEventListener;
get: Anue.Utils.NativeGet;
};
export = webview;
}
declare module 'anue-fe-sdk/eventy' {
class Eventy {
handlers = {};
......
......@@ -28,6 +28,7 @@ const waitty = Shared.get('utils.waitty');
const urly = Shared.get('utils.urly');
const getty = Shared.get('utils.getty');
const Eventy = Shared.get('utils.eventy');
const webview = Shared.get('utils.webview');
const isServer = typeof document === 'undefined';
const refreshTokenEndpoints = [
......@@ -104,6 +105,7 @@ class Auth {
private mSessionId = -1;
private tokenFormatter;
private alwaysSendToken = false;
private isInWebView = false;
private static getSSOData = async (): Promise<{
token: string;
flag: string;
......@@ -137,8 +139,10 @@ class Auth {
event = new Eventy();
constructor(config: Anue.Auth.Config = {}) {
const { host } = config;
const { host, token, isWebView } = config;
this.alwaysSendToken = !!config.alwaysSendToken;
this.mSSOToken = this.mSSOToken || token || '';
this.isInWebView = !!isWebView;
if (!rpcChannel && !isServer) {
const RPC = Shared.get('utils.rpc');
rpcChannel = new RPC('anue.auth', config.rpcHost || getRPCHostByUrl());
......@@ -151,7 +155,6 @@ class Auth {
if (host) {
this.mHost = host;
}
// do not attach message event when `isOpener` set to false
if (config.isOpener !== false) {
console.log('[Auth] listen to child frame events ..');
......@@ -485,6 +488,13 @@ class Auth {
};
refreshToken = async () => {
if (this.isInWebView) {
const t = await webview.get.webViewRefreshToken();
console.log('REF', t);
return t;
}
return await Network.getDriver().send<Anue.Auth.RefreshTokenResponse>({
method: 'POST',
url: urly(this.mHost, `/api/v1/user/token/refresh`),
......@@ -559,7 +569,19 @@ class Auth {
return profile;
};
private syncProfileFromRemote = async (): Promise<Anue.Auth.UserProfile | null> => {
syncProfileFromRemote = async (
shouldRenewProfile?: boolean
): Promise<Anue.Auth.UserProfile | null> => {
const target =
typeof window !== 'undefined' ? location.hostname : '.beta.cnyes.cool';
const matched = /\.(beta\.|int\.|stage\.)?(cnyes\.(cool|com)|anue\.(in|com))\/?$/.exec(
target
);
const matchedDomain = matched ? matched[0] : '/';
const memberAPIDomain = matchedDomain
? `https://member-always-right.api${matchedDomain[0]}`
: 'https://member-always-right.api.cnyes.com';
let profile: Anue.Auth.UserProfile | null = null;
if (!this.mSSOToken) {
console.log(
......@@ -568,20 +590,37 @@ class Auth {
);
return profile;
}
if (!this.isProfileValid(this.mProfile)) {
if (shouldRenewProfile || !this.isProfileValid(this.mProfile)) {
// Renew the profile every time if fetching profile API.
delete localStorage['anue.profile'];
rpcChannel.rpcCall('storage', ['delete', 'profile']);
console.log(
'[Auth] get profile from API as local caches missed (slowest)'
);
const response = await Network.getDriver().send<
APIResponse.ServiceResponse<{ user: Anue.Auth.UserProfile }>
>({
url: `${this.mHost}/api/v1/user/profile`,
url: `${memberAPIDomain}/member-always-right/api/v1/user/profile?\
product=${encodeURIComponent('cnyes,driver,video')}`,
headers: {
authorization: `Bearer ${this.mSSOToken}`
}
},
auth: true
});
profile = getty(response, ['items', 'user']);
const user = getty(response, ['items', 'user']);
const m = {
cnyes: getty(response, ['items', 'cnyes']),
driver: getty(response, ['items', 'driver']),
video: getty(response, ['items', 'video'])
};
profile = user && {
...user,
memberships: m
};
if (!this.isProfileValid(profile)) {
console.log(
'[Auth] getProfile failed, profile does not contain an email.',
......@@ -767,6 +806,18 @@ class Auth {
profile: this.mProfile
};
};
setAuthToken = (token: string, flag: string = '') => {
if (token) {
Auth.cacheSSOData(token, flag);
this.mSSOToken = token;
this.syncProfileFromRemote(true);
}
};
setIsInWebView = (isWebView: boolean) => {
this.isInWebView = isWebView;
};
}
Shared.set('packages.auth', Auth);
......
......@@ -10,6 +10,7 @@ import '@utils/paramly';
import '@utils/sso';
import '@utils/res';
import '@utils/urly';
import '@utils/webview';
import urld from '@utils/urld';
import '@libraries/Network/Network.abstract';
import * as Network from '@libraries/Network';
......@@ -39,12 +40,10 @@ anue.standalone = {
if (onChange) {
auth.onStateChange(onChange);
}
auth.host('https://member.prod.cnyes.com/member')
auth.host('https://member.prod.cnyes.com/member');
return await auth.consume();
},
login() {
},
login() {},
async loginWithUI() {
const OTAC = anue.shared.get('packages.otac');
const IMC = anue.shared.get('packages.imc');
......
......@@ -10,6 +10,7 @@ import '@utils/paramly';
import '@utils/sso';
import '@utils/res';
import '@utils/urly';
import '@utils/webview';
import urld from '@utils/urld';
import '@libraries/Network/Network.abstract';
import * as Network from '@libraries/Network';
......
......@@ -7,6 +7,7 @@ import '@utils/eventy';
import '@utils/rpc';
import '@utils/obfuscator';
import '@utils/sso';
import '@utils/webview';
import urld from '@utils/urld';
import '@libraries/Network/Network.abstract';
import * as Network from '@libraries/Network';
......
......@@ -10,6 +10,7 @@ import '@utils/paramly';
import '@utils/sso';
import '@utils/res';
import '@utils/urly';
import '@utils/webview';
import urld from '@utils/urld';
import '@libraries/Network/Network.abstract';
import * as Network from '@libraries/Network';
......
enum EventName {
RefreshToken = 'refreshToken'
}
/**
* Dispatch a event to App. Web->App
*/
const send = (
eventName: Anue.Utils.NativeEventName,
payload: Record<string, any> = {}
) => {
if (typeof native !== 'undefined') {
try {
switch (eventName) {
case 'closeWebView':
native.member_event(JSON.stringify({ action: 'anueCloseWebView' }));
break;
case 'anueRequestRefreshToken':
native.member_event(
JSON.stringify({ action: 'anueRequestRefreshToken' })
);
break;
case 'anueMemberLevelChanged':
native.member_event(
JSON.stringify({ action: 'anueMemberLevelChanged' })
);
break;
case 'anueAlert':
native.member_event(
JSON.stringify({
action: 'anueAlert',
title: payload.title || 'anueAlert is called',
message: payload.message || 'anueAlert is called'
})
);
default:
// TODO: Move the implementation to SDK from fe-portal.
// native.event(JSON.stringify(payload));
}
} catch (e) {
console.warn(`%cError occurs during communicate with APP`, e);
}
}
};
/**
* Init the listener for App. App->Web
*/
const addListener = {
webViewStatus() {
if (typeof window !== 'undefined') {
window.appCallWebCurrentStatus = (status: number) => {
switch (status) {
case 0:
window.webViewStatus = 'onResume';
break;
case 1:
window.webViewStatus = 'onPause';
break;
default:
window.webViewStatus = status;
}
return 'appCallWebCurrentStatus is called from App';
};
}
},
webViewRefreshToken() {
if (typeof window !== 'undefined') {
console.log('%c[WebView] appCallWebRefreshToken', 'color: red;');
window.appCallWebRefreshToken = (token: string) => {
const prevToken = window.webViewToken;
console.log('token received', token);
if (prevToken !== token) {
const event = new CustomEvent(EventName.RefreshToken, {
detail: token
});
window.webViewToken = token;
window.dispatchEvent(event);
}
return 'appCallWebRefreshToken is called from App';
};
}
}
};
interface AnueNativeEvent extends Event {
detail: string;
}
const get = {
webViewStatus: () => window.webViewStatus,
async webViewRefreshToken() {
if (typeof window !== 'undefined') {
return new Promise((resolve, reject) => {
function handleRefreshTokenResponse(event: AnueNativeEvent) {
const token = event.detail;
window.removeEventListener(
EventName.RefreshToken,
handleRefreshTokenResponse as EventListener
);
if (token) {
console.log('Receive Token from App');
send('anueAlert', { title: 'anueAlert - Received token' });
resolve(token);
} else {
reject(new Error('Refresh token failed in the webview'));
send('anueAlert', { title: 'anueAlert - Refresh token timeout!' });
}
}
setTimeout(() => {
alert('Failed');
reject(new Error('Refresh token time out!'));
}, 5000);
window.addEventListener(
EventName.RefreshToken,
handleRefreshTokenResponse as EventListener
);
send('anueRequestRefreshToken');
});
}
}
};
const webview = {
send,
addListener,
get
};
anue.shared.set('utils.webview', webview);
export default webview;
......@@ -706,6 +706,15 @@
dependencies:
chalk "*"
"@types/copy-webpack-plugin@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@types/copy-webpack-plugin/-/copy-webpack-plugin-5.0.0.tgz#db7f9c9763b10b2af5c83f598fa9b5a13733b20b"
integrity sha512-yQHocgdgES7W5Q2UyxJ5cj/E6MrV1zq3MZ8jdApS9NJKqax+rux9IE3QAbBmNCGbgivEsejrkIq3Rm76JLubkg==
dependencies:
"@types/minimatch" "*"
"@types/node" "*"
"@types/webpack" "*"
"@types/http-proxy@^1.16.2":
version "1.17.0"
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.0.tgz#baf82ff6aa2723fd29f90e3ba1384e665006863e"
......@@ -723,10 +732,10 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/lz-string@^1.3.33":
version "1.3.33"
resolved "https://registry.yarnpkg.com/@types/lz-string/-/lz-string-1.3.33.tgz#de2d6105ea7bcaf67dd1d9451d580700d30473fc"
integrity sha512-yWj3OnlKlwNpq9+Jh/nJkVAD3ta8Abk2kIRpjWpVkDlAD43tn6Q6xk5hurp84ndcq54jBDBGCD/WcIR0pspG0A==
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
"@types/node@*", "@types/node@10.12.12":
version "10.12.12"
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment