Skip to content

Commit

Permalink
Merge branch 'feature/webview-provider'
Browse files Browse the repository at this point in the history
  • Loading branch information
vlad-d committed Jun 13, 2023
2 parents 77c5070 + 23a0450 commit c686b91
Show file tree
Hide file tree
Showing 11 changed files with 713 additions and 12 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const authSignature = provider.getSignature();
Event listeners can be set for login/logout events.

```typescript
import {WalletConnectProviderFactory} from "./WalletConnectProviderFactory";
import {WalletConnectProviderFactory} from "@elrond-giants/erdjs-auth";
const provider = new WalletConnectProviderFactory("devnet").createProvider();
Expand Down
307 changes: 301 additions & 6 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@elrond-giants/erdjs-auth",
"version": "0.5.0",
"version": "0.6.0-beta.2",
"description": "Easy to use MultiversX typescript Auth Providers, with a common interface.",
"main": "dist/index.js",
"types": "dist/index.d.js",
Expand All @@ -16,11 +16,12 @@
"author": "ElrondGiants",
"license": "GPL-3.0-or-later",
"dependencies": {
"@multiversx/sdk-core": "^11.4.1",
"@multiversx/sdk-extension-provider": "^2.0.7",
"@multiversx/sdk-hw-provider": "^3.0.3",
"@multiversx/sdk-wallet": "^3.0.0",
"@multiversx/sdk-web-wallet-provider": "^2.2.1",
"@multiversx/sdk-wallet-connect-provider": "^3.1.0",
"@multiversx/sdk-web-wallet-provider": "^2.2.1",
"tslib": "^2.4.0"
},
"devDependencies": {
Expand Down
167 changes: 167 additions & 0 deletions src/AuthProviders/WebviewProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import {
AuthProviderType,
EventHandler,
EventType,
IAuthProvider,
IAuthState,
IEventBus,
Transaction,
WebViewProviderRequestEnums,
WebViewProviderResponseEnums
} from "../types";
import WebviewTransport from "../WebviewTransport";
import {Transaction as CoreTx} from "@multiversx/sdk-core"
import {decodeAuthToken} from "../utils/webview";

export class WebviewProvider implements IAuthProvider {
private token: string | null = null;
private webviewNetwork = new WebviewTransport();
private address: string | null = null;
private signature: string | null = null;

constructor(private eventBus: IEventBus) { }

getAddress(): string | null {
return this.address;
}

getBaseProvider(): any {
}

getSignature(): string | null {
return this.signature;
}

getType(): AuthProviderType {
return AuthProviderType.WEBVIEW;
}

init(): Promise<boolean> {
if (window !== undefined) {
const params = new URLSearchParams(window.location.search);
const token = params.get("accessToken");
if (!token) {
return Promise.resolve(false);
}
this.doLogin(token);
return Promise.resolve(true);
}

return Promise.resolve(false);
}

async login(token?: string): Promise<string> {
const getAccessToken = () => new Promise<string>((resolve, reject) => {
this.webviewNetwork.on(
WebViewProviderResponseEnums.loginResponse,
(message: any) => {
const {accessToken, error} = message;
if (error) {
reject(error);
} else {
resolve(accessToken);
}
}
);
});
this.webviewNetwork.post(WebViewProviderRequestEnums.loginRequest);
try {
const accessToken = await getAccessToken();

return this.doLogin(accessToken);
} catch (e) {
console.error(e);
throw e;
} finally {
this.webviewNetwork.off(WebViewProviderResponseEnums.loginResponse);
}
}

doLogin(accessToken: string): string {
if (!accessToken) {return "";}
this.token = accessToken;
const _token = decodeAuthToken(accessToken);
if (!_token) {return "";}
const {signature, address} = _token;
this.address = address;
this.signature = signature;

this.eventBus.emit("login", {});

return address;
}

logout(): Promise<boolean> {
this.webviewNetwork.post(WebViewProviderRequestEnums.logoutRequest);
this.token = null;
this.address = null;
this.signature = null;

this.eventBus.emit("logout", {});

return Promise.resolve(true);
}

on(event: EventType, handler: EventHandler): void {
this.eventBus.subscribe(event, handler);
}

off(event: EventType, handler: EventHandler): void {
this.eventBus.unsubscribe(event, handler);
}

async signTransaction(tx: Transaction): Promise<Transaction | null> {
const txs = await this.signTransactions([tx]);

return txs[0];
}

async signTransactions(transactions: Transaction[]): Promise<Transaction[]> {
const signTxs = () => new Promise<Transaction[]>((resolve, reject) => {
(window as any).transactionsSigned = (txs: any, error: string) => {
if (error) {
reject(error);
(window as any).transactionsSigned = null;
return;
}
const signedTxs = JSON.parse(txs);
resolve(signedTxs.map((tx: any) => CoreTx.fromPlainObject(tx)));
(window as any).transactionsSigned = null;
};

this.webviewNetwork.on(
WebViewProviderResponseEnums.signTransactionsResponse,
(message: any) => {
const {transactions, error} = message;
if (error) {
reject(error);
} else {
resolve(transactions.map((tx: any) => CoreTx.fromPlainObject(tx)));
}
}
);
});

try {
this.webviewNetwork.post(
WebViewProviderRequestEnums.signTransactionsRequest,
transactions.map((tx) => (tx as CoreTx).toPlainObject())
);

return await signTxs();
} catch (e) {
throw e;
} finally {
this.webviewNetwork.off(WebViewProviderResponseEnums.signTransactionsResponse);
}
}

toJson(): IAuthState {
return {
address: this.address,
authenticated: !!this.address,
authProviderType: this.getType(),
};
}

};
3 changes: 2 additions & 1 deletion src/AuthProviders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
export { LedgerProvider } from "./LedgerProvider";
export { ExtensionProvider } from "./ExtensionProvider";
export { WalletConnectProvider } from "./WalletConnectProvider";
export {WebProvider} from "./WebProvider";
export { WebProvider } from "./WebProvider";
export { WebviewProvider } from "./WebviewProvider";
11 changes: 11 additions & 0 deletions src/Factories/WebviewProviderFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {IAuthProvider, IAuthProviderFactory} from "../types";
import {WebviewProvider} from "../AuthProviders";
import EventsBus from "../EventBus";

export class WebviewProviderFactory implements IAuthProviderFactory {
constructor() {}

createProvider(): IAuthProvider {
return new WebviewProvider(new EventsBus());
}
}
1 change: 1 addition & 0 deletions src/Factories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export { WalletConnectProviderFactory } from "./WalletConnectProviderFactory";
export { WebProviderFactory } from "./WebProviderFactory";
export { LedgerProviderFactory } from "./LedgerProviderFactory";
export { ExtensionProviderFactory } from "./ExtensionProviderFactory";
export { WebviewProviderFactory } from "./WebviewProviderFactory";
// export { PemProviderFactory } from "./PemProviderFactory";
104 changes: 104 additions & 0 deletions src/WebviewTransport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {WebviewPlatforms, WebViewProviderRequestEnums, WebViewProviderResponseEnums} from "./types";
import {detectPlatform} from "./utils/webview";

export default class WebviewTransport {
private platform: WebviewPlatforms
private handlers = new Map<string, (message: any) => void>();
private targetOrigin = "*";

constructor() {
this.targetOrigin = window?.parent?.origin ?? "*";
this.platform = detectPlatform();
if (typeof window !== "undefined") {
(window as any).addEventListener("message", this.handleMessageEvent.bind(this));
document.addEventListener("message", this.handleMessageEvent.bind(this));
}
}

on(type: WebViewProviderResponseEnums, callback: (message: any) => void) {
this.handlers.set(type, callback);
}

off(type: WebViewProviderResponseEnums) {
this.handlers.delete(type);
}

post(requestType: WebViewProviderRequestEnums, data?: any) {
postMessage(this.platform, requestType, this.targetOrigin, data);
}


handleMessageEvent(event: any) {
if (
event.target?.origin !== this.targetOrigin
&& this.platform !== WebviewPlatforms.reactNative
) {
return;
}

try {
const {message, type} = JSON.parse(event.data);
if (!type) {
console.error("Message received without type");
}
const callback = this.handlers.get(type);
if (callback) {callback(message);}

} catch (e) {
if (e instanceof SyntaxError) {
console.error("Error parsing response.");
}
console.error("Failed to handle event.");
}
}

disconnect() {
window.removeEventListener("message", this.handleMessageEvent.bind(this));
document.removeEventListener("message", this.handleMessageEvent.bind(this));
}

};

const postMessage = (
platform: WebviewPlatforms,
type: WebViewProviderRequestEnums,
targetOrigin: string,
message?: any
) => {
switch (platform) {
case WebviewPlatforms.ios:
return postIosMessage(type, targetOrigin, message);
case WebviewPlatforms.reactNative:
return postReactNativeMessage(type, targetOrigin, message);
case WebviewPlatforms.web:
return postWebMessage(type, targetOrigin, message);
default:
const unreachable = (): never => {throw "Unreachable assert failed."}
return unreachable();
}
};

const postReactNativeMessage = (type: WebViewProviderRequestEnums, targetOrigin: string, message?: any) => {
(window as any).ReactNativeWebView.postMessage(JSON.stringify({type, message}));
};

const postWebMessage = (type: WebViewProviderRequestEnums, targetOrigin: string, message?: any) => {
window.postMessage(JSON.stringify({type, message}), targetOrigin);
};

const postIosMessage = (type: WebViewProviderRequestEnums, targetOrigin: string, message?: any) => {
const methodWords = type.split("_").map((s, i) => {
let word = s.toLowerCase();
if (i < 1) {return word;}

return word.charAt(0).toUpperCase() + word.slice(1);
});
methodWords.pop(); // remove "Request" word
const method = methodWords.join("");
if (type === WebViewProviderRequestEnums.signTransactionsRequest) {
(window as any).webkit.messageHandlers[method].postMessage(message, targetOrigin);
} else {
(window as any).webkit.messageHandlers[method].postMessage(message);
}

};
8 changes: 6 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import {WebviewProvider} from "./AuthProviders";

export {
WalletConnectProviderFactory,
WebProviderFactory,
ExtensionProviderFactory,
LedgerProviderFactory
LedgerProviderFactory,
WebviewProviderFactory
} from "./Factories";

export {
WalletConnectProvider,
WebProvider,
ExtensionProvider,
LedgerProvider
LedgerProvider,
WebviewProvider
} from "./AuthProviders";
35 changes: 35 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export enum AuthProviderType {
EXTENSION = "extension",
LEDGER = "ledger",
PEM = "pem",
WEBVIEW = "webview",
NONE = "none",
}

Expand Down Expand Up @@ -96,3 +97,37 @@ export interface IEventBus {
emit(key: EventType, payload: any): void;

}

export enum WebviewPlatforms {
ios = 'ios',
reactNative = 'reactNative',
web = 'web'
}

export enum WebViewProviderRequestEnums {
signTransactionsRequest = 'SIGN_TRANSACTIONS_REQUEST',
signMessageRequest = 'SIGN_MESSAGE_REQUEST',
loginRequest = 'LOGIN_REQUEST',
logoutRequest = 'LOGOUT_REQUEST',
reloginRequest = 'RELOGIN_REQUEST'
}

export enum WebViewProviderResponseEnums {
signTransactionsResponse = 'SIGN_TRANSACTIONS_RESPONSE',
signMessageResponse = 'SIGN_MESSAGE_RESPONSE',
loginResponse = 'LOGIN_RESPONSE',
reloginResponse = 'RELOGIN_RESPONSE'
}

export type DecodedLoginTokenType = {
blockHash: string;
extraInfo?: { timestamp?: number };
origin: string;
ttl: number;
}

export type AuthToken = {
address: string;
body: string;
signature: string;
} & DecodedLoginTokenType;
Loading

0 comments on commit c686b91

Please sign in to comment.