Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MobileRelay #903

Merged
merged 3 commits into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/wallet-sdk/src/CoinbaseWalletSDK.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
CoinbaseWalletProvider,
CoinbaseWalletProviderOptions,
} from "./provider/CoinbaseWalletProvider";
import { WalletSDKRelay } from "./relay/WalletSDKRelay";
import { WalletLinkRelay } from "./relay/WalletLinkRelay";
import { WalletSDKRelayEventManager } from "./relay/WalletSDKRelayEventManager";

jest.mock("./provider/WalletSDKUI");
Expand Down Expand Up @@ -52,7 +52,7 @@ describe("CoinbaseWalletSDK", () => {

test("@disconnect", () => {
const relayResetMock = jest
.spyOn(WalletSDKRelay.prototype, "resetAndReload")
.spyOn(WalletLinkRelay.prototype, "resetAndReload")
.mockImplementation(() => "resetAndReload");
coinbaseWalletSDK2.disconnect();

Expand All @@ -61,7 +61,7 @@ describe("CoinbaseWalletSDK", () => {

test("@setAppInfo", () => {
const relaySetAppInfoMock = jest
.spyOn(WalletSDKRelay.prototype, "setAppInfo")
.spyOn(WalletLinkRelay.prototype, "setAppInfo")
.mockImplementation(() => "setAppInfo");
coinbaseWalletSDK2.setAppInfo("sdk", "http://sdk-image.png");

Expand Down Expand Up @@ -179,7 +179,7 @@ describe("CoinbaseWalletSDK", () => {

test("@setAppInfo", () => {
const relaySetAppInfoMock = jest
.spyOn(WalletSDKRelay.prototype, "setAppInfo")
.spyOn(WalletLinkRelay.prototype, "setAppInfo")
.mockImplementation(() => "setAppInfo");
coinbaseWalletSDK2.setAppInfo("cipher", "http://cipher-image.png");
expect(relaySetAppInfoMock).not.toBeCalled();
Expand Down
19 changes: 14 additions & 5 deletions packages/wallet-sdk/src/CoinbaseWalletSDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import { ScopedLocalStorage } from "./lib/ScopedLocalStorage";
import { CoinbaseWalletProvider } from "./provider/CoinbaseWalletProvider";
import { WalletSDKUI } from "./provider/WalletSDKUI";
import { WalletUI, WalletUIOptions } from "./provider/WalletUI";
import { WalletSDKRelay } from "./relay/WalletSDKRelay";
import { MobileRelay } from "./relay/MobileRelay";
import { WalletLinkRelay } from "./relay/WalletLinkRelay";
import { WalletSDKRelayEventManager } from "./relay/WalletSDKRelayEventManager";
import { getFavicon } from "./util";
import { getFavicon, isMobileWeb } from "./util";

const LINK_API_URL = process.env.LINK_API_URL || "https://www.walletlink.org";
const SDK_VERSION =
Expand Down Expand Up @@ -43,14 +44,16 @@ export interface CoinbaseWalletSDKOptions {
headlessMode?: boolean;
/** @optional whether or not to reload dapp automatically after disconnect, defaults to true */
reloadOnDisconnect?: boolean;
/** @optional whether to connect mobile web app via WalletLink, defaults to false */
useMobileWalletLink?: boolean;
}

export class CoinbaseWalletSDK {
public static VERSION = SDK_VERSION;

private _appName = "";
private _appLogoUrl: string | null = null;
private _relay: WalletSDKRelay | null = null;
private _relay: WalletLinkRelay | null = null;
private _relayEventManager: WalletSDKRelayEventManager | null = null;
private _storage: ScopedLocalStorage;
private _overrideIsMetaMask: boolean;
Expand Down Expand Up @@ -110,7 +113,7 @@ export class CoinbaseWalletSDK {

this._relayEventManager = new WalletSDKRelayEventManager();

this._relay = new WalletSDKRelay({
const relayOption = {
linkAPIUrl,
version: SDK_VERSION,
darkMode: !!options.darkMode,
Expand All @@ -119,7 +122,13 @@ export class CoinbaseWalletSDK {
relayEventManager: this._relayEventManager,
diagnosticLogger: this._diagnosticLogger,
reloadOnDisconnect: this._reloadOnDisconnect,
});
useMobileWalletLink: options.useMobileWalletLink,
};

this._relay = isMobileWeb()
? new MobileRelay(relayOption)
: new WalletLinkRelay(relayOption);
Comment on lines +128 to +130
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

branching here for dedicated relay logic


this.setAppInfo(options.appName, options.appLogoUrl);

if (!!options.headlessMode) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const REQUEST_TIMEOUT = 60000;
/**
* Coinbase Wallet Connection
*/
export class WalletSDKConnection {
export class WalletLinkConnection {
private ws: RxWebSocket<ServerMessage>;
private subscriptions = new Subscription();
private destroyed = false;
Expand Down
41 changes: 41 additions & 0 deletions packages/wallet-sdk/src/relay/MobileRelay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { isInIFrame } from "../util";
import { WalletLinkRelay, WalletLinkRelayOptions } from "./WalletLinkRelay";
import { CancelablePromise } from "./WalletSDKRelayAbstract";
import { RequestEthereumAccountsResponse } from "./Web3Response";

export class MobileRelay extends WalletLinkRelay {
private _useMobileWalletLink: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of typescript privates we can start using javascript private methods

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields

  #useMobileWalletLink: boolean;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for flagging that.
will address that during overall refactor.


constructor(options: Readonly<WalletLinkRelayOptions>) {
super(options);
this._useMobileWalletLink = options.useMobileWalletLink ?? false;
}

// override
public requestEthereumAccounts(): CancelablePromise<RequestEthereumAccountsResponse> {
if (this._useMobileWalletLink) {
return super.requestEthereumAccounts();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: deeplink navigation

}

// TODO: Implement & present in-page wallet picker instead of navigating to www.coinbase.com/connect-dapp
return {
promise: new Promise(() => {
let location: Location;
try {
if (isInIFrame() && window.top) {
location = window.top.location;
} else {
location = window.location;
}
} catch (e) {
location = window.location;
}

location.href = `https://www.coinbase.com/connect-dapp?uri=${encodeURIComponent(
location.href,
)}`;
}),
Comment on lines +22 to +37
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

migrated from WalletLinkRelay/requestEthereumAccounts

image

cancel: () => {},
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { DiagnosticLogger, EVENTS } from "../connection/DiagnosticLogger";
import { EventListener } from "../connection/EventListener";
import { ServerMessageEvent } from "../connection/ServerMessage";
import { SessionConfig } from "../connection/SessionConfig";
import { WalletSDKConnection } from "../connection/WalletSDKConnection";
import { WalletLinkConnection } from "../connection/WalletLinkConnection";
import {
ErrorType,
getErrorCode,
Expand All @@ -33,7 +33,6 @@ import {
bigIntStringFromBN,
createQrUrl,
hexStringFromBuffer,
isInIFrame,
randomBytesHex,
} from "../util";
import * as aes256gcm from "./aes256gcm";
Expand Down Expand Up @@ -82,7 +81,7 @@ import {
Web3ResponseMessage,
} from "./Web3ResponseMessage";

export interface WalletSDKRelayOptions {
export interface WalletLinkRelayOptions {
linkAPIUrl: string;
version: string;
darkMode: boolean;
Expand All @@ -92,17 +91,18 @@ export interface WalletSDKRelayOptions {
diagnosticLogger?: DiagnosticLogger;
eventListener?: EventListener;
reloadOnDisconnect?: boolean;
useMobileWalletLink?: boolean;
}

export class WalletSDKRelay extends WalletSDKRelayAbstract {
export class WalletLinkRelay extends WalletSDKRelayAbstract {
private static accountRequestCallbackIds = new Set<string>();

private readonly linkAPIUrl: string;
protected readonly storage: ScopedLocalStorage;
private _session: Session;
private readonly relayEventManager: WalletSDKRelayEventManager;
protected readonly diagnostic?: DiagnosticLogger;
private connection: WalletSDKConnection;
private connection: WalletLinkConnection;
private accountsCallback:
| ((account: string[], isDisconnect?: boolean) => void)
| null = null;
Expand All @@ -111,7 +111,7 @@ export class WalletSDKRelay extends WalletSDKRelayAbstract {
| null = null;
private dappDefaultChainSubject = new BehaviorSubject(1);
private dappDefaultChain = 1;
private readonly options: WalletSDKRelayOptions;
private readonly options: WalletLinkRelayOptions;

private ui: WalletUI;

Expand All @@ -122,7 +122,7 @@ export class WalletSDKRelay extends WalletSDKRelayAbstract {
isLinked: boolean | undefined;
isUnlinkedErrorState: boolean | undefined;

constructor(options: Readonly<WalletSDKRelayOptions>) {
constructor(options: Readonly<WalletLinkRelayOptions>) {
super();
this.linkAPIUrl = options.linkAPIUrl;
this.storage = options.storage;
Expand Down Expand Up @@ -167,7 +167,7 @@ export class WalletSDKRelay extends WalletSDKRelayAbstract {
const session =
Session.load(this.storage) || new Session(this.storage).save();

const connection = new WalletSDKConnection(
const connection = new WalletLinkConnection(
session.id,
session.key,
this.linkAPIUrl,
Expand Down Expand Up @@ -340,12 +340,12 @@ export class WalletSDKRelay extends WalletSDKRelayAbstract {
this.accountsCallback([selectedAddress]);
}

if (WalletSDKRelay.accountRequestCallbackIds.size > 0) {
if (WalletLinkRelay.accountRequestCallbackIds.size > 0) {
// We get the ethereum address from the metadata. If for whatever
// reason we don't get a response via an explicit web3 message
// we can still fulfill the eip1102 request.
Array.from(
WalletSDKRelay.accountRequestCallbackIds.values(),
WalletLinkRelay.accountRequestCallbackIds.values(),
).forEach(id => {
const message = Web3ResponseMessage({
id,
Expand All @@ -355,7 +355,7 @@ export class WalletSDKRelay extends WalletSDKRelayAbstract {
});
this.invokeCallback({ ...message, id });
});
WalletSDKRelay.accountRequestCallbackIds.clear();
WalletLinkRelay.accountRequestCallbackIds.clear();
}
},
error: () => {
Expand Down Expand Up @@ -821,10 +821,10 @@ export class WalletSDKRelay extends WalletSDKRelayAbstract {
sessionIdHash: this.getSessionIdHash(),
});
if (isRequestEthereumAccountsResponse(response)) {
WalletSDKRelay.accountRequestCallbackIds.forEach(id =>
WalletLinkRelay.accountRequestCallbackIds.forEach(id =>
this.invokeCallback({ ...message, id }),
);
WalletSDKRelay.accountRequestCallbackIds.clear();
WalletLinkRelay.accountRequestCallbackIds.clear();
return;
}

Expand Down Expand Up @@ -892,30 +892,6 @@ export class WalletSDKRelay extends WalletSDKRelayAbstract {
resolve(response as RequestEthereumAccountsResponse);
});

const userAgent = window?.navigator?.userAgent || null;
if (
userAgent &&
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
userAgent,
)
) {
let location: Location;
try {
if (isInIFrame() && window.top) {
location = window.top.location;
} else {
location = window.location;
}
} catch (e) {
location = window.location;
}

location.href = `https://www.coinbase.com/connect-dapp?uri=${encodeURIComponent(
location.href,
)}`;
return;
}

if (this.ui.inlineAccountsResponse()) {
const onAccounts = (accounts: [AddressString]) => {
this.handleWeb3ResponseMessage(
Expand All @@ -940,7 +916,7 @@ export class WalletSDKRelay extends WalletSDKRelayAbstract {
});
}

WalletSDKRelay.accountRequestCallbackIds.add(id);
WalletLinkRelay.accountRequestCallbackIds.add(id);

if (!this.ui.inlineAccountsResponse() && !this.ui.isStandalone()) {
this.publishWeb3RequestEvent(id, request);
Expand Down
33 changes: 33 additions & 0 deletions packages/wallet-sdk/src/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
hexStringToUint8Array,
intNumberFromHexString,
isHexString,
isMobileWeb,
prepend0x,
randomBytesHex,
range,
Expand Down Expand Up @@ -242,4 +243,36 @@ describe("util", () => {
"https://www.walletlink.org/#/link?parent-id=1dc7878268586cbcaf041c6817d446d3&secret=b9a1d5933eae7064fc6e1a673235f648&server=https%3A%2F%2Fwww.walletlink.org&v=1&chainId=1",
);
});

test("isMobileWeb", () => {
const testCases = [
{
userAgent:
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1",
expected: true,
},
{
userAgent:
"Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.130 Mobile Safari/537.36",
expected: true,
},
{
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
expected: false,
},
{
userAgent: undefined,
expected: false,
},
];

testCases.forEach(testCase => {
Object.defineProperty(window.navigator, "userAgent", {
writable: true,
value: testCase.userAgent,
});
expect(isMobileWeb()).toEqual(testCase.expected);
});
});
});
6 changes: 6 additions & 0 deletions packages/wallet-sdk/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,9 @@ export function isInIFrame(): boolean {
return false;
}
}

export function isMobileWeb(): boolean {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
window?.navigator?.userAgent,
);
}