From e0c6fd7bb81f78abd1d364e129f19e732ec1e43e Mon Sep 17 00:00:00 2001 From: Michael Wallace Date: Mon, 20 Mar 2023 18:36:23 +1100 Subject: [PATCH 1/7] feat: Wallet connection manager view WIP --- src/client/ui.ts | 5 +- src/client/views/manage-wallets.ts | 128 ++++++++++++++++++++++++++++- src/client/views/select-issuers.ts | 24 +++--- src/theme/common.scss | 17 +++- src/wallet/Web3WalletProvider.ts | 4 + 5 files changed, 162 insertions(+), 16 deletions(-) diff --git a/src/client/ui.ts b/src/client/ui.ts index 655b155b..082e835b 100644 --- a/src/client/ui.ts +++ b/src/client/ui.ts @@ -6,11 +6,12 @@ import { ViewInterface, ViewComponent, ViewFactory, ViewConstructor } from './vi import { TokenStore } from './tokenStore' import { SelectIssuers } from './views/select-issuers' import { SelectWallet } from './views/select-wallet' +import { ManageWallets } from './views/manage-wallets' export type UIType = 'popup' | 'inline' // TODO: implement modal too export type PopupPosition = 'bottom-right' | 'bottom-left' | 'top-left' | 'top-right' export type UItheme = 'light' | 'dark' -export type ViewType = 'start' | 'main' | 'wallet' | string +export type ViewType = 'start' | 'main' | 'wallet' | 'manage-wallets' | string export interface UIOptionsInterface { uiType?: UIType @@ -136,6 +137,8 @@ export class Ui implements UiInterface { return SelectIssuers case 'wallet': return SelectWallet + case 'manage-wallets': + return ManageWallets } } diff --git a/src/client/views/manage-wallets.ts b/src/client/views/manage-wallets.ts index 0ac71a2a..eaaf09d4 100644 --- a/src/client/views/manage-wallets.ts +++ b/src/client/views/manage-wallets.ts @@ -1,9 +1,131 @@ import { AbstractView } from './view-interface' +import { SupportedBlockchainsParam } from '../interface' +import { UIUpdateEventType } from '../index' +import { add } from 'husky' -class ManageWallets extends AbstractView { - render() { +export class ManageWallets extends AbstractView { + init() { + this.client.registerUiUpdateCallback(UIUpdateEventType.WALLET_DISCONNECTED, async () => { + const wallets = await this.client.getWalletProvider() + + if (wallets.getConnectionCount() === 0) { + this.ui.updateUI('wallet', { viewName: 'wallet' }, { viewTransition: 'slide-in-left' }) + } else { + await this.render() + } + }) + } + + async render() { this.viewContainer.innerHTML = ` -

Wallet Connections Here!

+
+
+
+
+
+ +
+
+

My Wallets

+
+
+ +
+
+
+ +
+
` + + this.setupWalletButton() + this.setupBackButton() + } + + private setupWalletButton() { + const walletBtn = this.viewContainer.querySelector('.dis-wallet-tn') + walletBtn.style.display = 'block' + walletBtn.addEventListener('click', () => { + this.client.disconnectWallet() + }) + } + + private setupBackButton() { + const backBtn = this.viewContainer.querySelector('.back-to-menu-tn') + backBtn.addEventListener('click', () => { + this.ui.updateUI('main', null, { viewTransition: 'slide-in-left' }) + }) + } + + async renderCurrentWalletConnections() { + const wallets = await this.client.getWalletProvider() + + let html = '' + + for (const blockchain of ['evm', 'solana', 'flow'] as SupportedBlockchainsParam[]) { + // If TN has connected wallets of this type, render the header and each wallet connection as a row + if (wallets.hasAnyConnection([blockchain])) { + let typeLabel = '' + + switch (blockchain) { + case 'evm': + typeLabel = 'Ethereum EVM' + break + case 'solana': + typeLabel = 'Solana' + break + case 'flow': + typeLabel = 'Flow' + break + default: + typeLabel = blockchain + } + + html += `

${typeLabel}

` + + for (const connection of wallets.getConnectedWalletData(blockchain)) { + const address = connection.address + + html += ` + + ` + } + } + } + + return html } } diff --git a/src/client/views/select-issuers.ts b/src/client/views/select-issuers.ts index 0353a25a..c0061502 100644 --- a/src/client/views/select-issuers.ts +++ b/src/client/views/select-issuers.ts @@ -48,17 +48,15 @@ export class SelectIssuers extends AbstractView { + + + + + + + ${this.getCustomContent()} @@ -71,7 +69,7 @@ export class SelectIssuers extends AbstractView {
- +
- + ` @@ -61,7 +68,6 @@ export class ManageWallets extends AbstractView { private setupWalletButton() { const walletBtn = this.viewContainer.querySelector('.dis-wallet-tn') - walletBtn.style.display = 'block' walletBtn.addEventListener('click', () => { this.client.disconnectWallet() }) @@ -104,23 +110,23 @@ export class ManageWallets extends AbstractView { const address = connection.address html += ` - + ` } } diff --git a/src/client/views/select-wallet.ts b/src/client/views/select-wallet.ts index 8d30a8a4..be930567 100644 --- a/src/client/views/select-wallet.ts +++ b/src/client/views/select-wallet.ts @@ -10,6 +10,8 @@ export class SelectWallet extends AbstractView { this.client.registerUiUpdateCallback(UIUpdateEventType.WALLET_DISCONNECTED, undefined) } + // TODO: Accept data param to connect specific wallet - + // this is needed when the user clicks load on a token issuer for a wallet type they have not connected. render() { let walletButtons = '' diff --git a/src/core/__tests__/core.spec.ts b/src/core/__tests__/core.spec.ts index 2dd052b1..8abb3bfd 100644 --- a/src/core/__tests__/core.spec.ts +++ b/src/core/__tests__/core.spec.ts @@ -122,4 +122,4 @@ describe('core Spec', () => { const token = readTokenFromMagicUrl('ticket', 'secret', 'id') expect(token.id).toEqual('nicktaras83@gmail.com') }) -}) \ No newline at end of file +}) diff --git a/src/theme/common.scss b/src/theme/common.scss index af8b8541..45bc93d9 100644 --- a/src/theme/common.scss +++ b/src/theme/common.scss @@ -279,18 +279,57 @@ background-color: #efefef; } -.overlay-tn .wallet-address-tn { - white-space: nowrap; - display: inline-block; - padding: 10px; - .ellipsis { - display: inline-block; - vertical-align: bottom; - white-space: nowrap; - width: 100%; - max-width: 90px; - overflow: hidden; - text-overflow: ellipsis; +.overlay-tn .wallet-list-tn { + .wallet-connection-tn { + display: flex; + flex-direction: row; + justify-content: center; + border-bottom: 1px solid #161616; + color: white; + padding: 4px 12px; + + .wallets-header { + padding-bottom: 0 !important; + } + + .wallet-icon-tn { + display: flex; + + svg, + img { + width: 30px; + height: auto; + } + } + + .wallet-info-tn { + flex-grow: 1; + padding: 10px; + + .wallet-address-tn { + white-space: nowrap; + display: inline-block; + .ellipsis { + display: inline-block; + vertical-align: bottom; + white-space: nowrap; + width: 100%; + max-width: 90px; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + + .wallet-disconnect-tn { + display: flex; + flex-direction: column; + justify-content: center; + + button { + margin: 0 !important; + } + } } } diff --git a/src/utils/support/getBrowserData.ts b/src/utils/support/getBrowserData.ts index a0bf08b0..f7e4563c 100644 --- a/src/utils/support/getBrowserData.ts +++ b/src/utils/support/getBrowserData.ts @@ -1,4 +1,8 @@ +let browserData = null + export const getBrowserData = () => { + if (browserData) return browserData + const inBrowser = typeof window !== 'undefined' const UA = inBrowser && window.navigator.userAgent.toLowerCase() @@ -55,7 +59,7 @@ export const getBrowserData = () => { const isMetaMask = isTouchDevice && !!windowEthereum.isMetaMask && !isTrust && !isBrave - return { + browserData = { iE: isIE, iE9: isIE9, edge: isEdge, @@ -87,6 +91,8 @@ export const getBrowserData = () => { mewAndroid: isAndroid && isMyEthereumWallet, imTokenAndroid: isAndroid && isImToken, } + + return browserData } export function isMacOrIOS() { diff --git a/src/wallet/Web3WalletProvider.ts b/src/wallet/Web3WalletProvider.ts index c1e9dd1b..b44e843b 100644 --- a/src/wallet/Web3WalletProvider.ts +++ b/src/wallet/Web3WalletProvider.ts @@ -11,7 +11,7 @@ interface WalletConnectionState { export interface WalletConnection { address: string chainId: number | string - providerType: string + providerType: SupportedWalletProviders blockchain: SupportedBlockchainsParam provider?: ethers.providers.Web3Provider | any // solana(phantom) have different interface ethers?: any @@ -233,7 +233,7 @@ export class Web3WalletProvider { registerNewWalletAddress( address: string, chainId: number | string, - providerType: string, + providerType: SupportedWalletProviders, provider: any, blockchain: SupportedBlockchainsParam, ) { @@ -337,7 +337,7 @@ export class Web3WalletProvider { } } - private async registerEvmProvider(provider: ethers.providers.Web3Provider, providerName: string) { + private async registerEvmProvider(provider: ethers.providers.Web3Provider, providerName: SupportedWalletProviders) { const accounts = await provider.listAccounts() const chainId = (await provider.detectNetwork()).chainId @@ -352,7 +352,7 @@ export class Web3WalletProvider { return curAccount } - private async registerSolanaProvider(provider: any, providerName: string) { + private async registerSolanaProvider(provider: any, providerName: SupportedWalletProviders) { const connection = await provider.connect() const accountAddress: string = connection.publicKey.toBase58() @@ -387,7 +387,7 @@ export class Web3WalletProvider { const provider = new ethers.providers.Web3Provider(window.ethereum, 'any') - return this.registerEvmProvider(provider, 'MetaMask') + return this.registerEvmProvider(provider, SupportedWalletProviders.MetaMask) } else { throw new Error('MetaMask is not available. Please check the extension is supported and active.') } @@ -412,7 +412,7 @@ export class Web3WalletProvider { .then(() => { const provider = new ethers.providers.Web3Provider(walletConnect, 'any') - resolve(this.registerEvmProvider(provider, 'WalletConnect')) + resolve(this.registerEvmProvider(provider, SupportedWalletProviders.WalletConnect)) }) .catch((e) => reject(e)) }) @@ -470,7 +470,7 @@ export class Web3WalletProvider { logger(2, 'WC2 connected.....') QRCodeModal?.close() const provider = new ethers.providers.Web3Provider(universalWalletConnect, 'any') - resolve(this.registerEvmProvider(provider, 'WalletConnectV2')) + resolve(this.registerEvmProvider(provider, SupportedWalletProviders.WalletConnectV2)) }) .catch((e) => { logger(2, 'WC2 connect error...', e) @@ -492,14 +492,14 @@ export class Web3WalletProvider { const provider = new ethers.providers.Web3Provider(torus.provider, 'any') - return this.registerEvmProvider(provider, 'Torus') + return this.registerEvmProvider(provider, SupportedWalletProviders.Torus) } async Phantom(checkConnectionOnly: boolean) { logger(2, 'connect Phantom') if (typeof window.solana !== 'undefined') { - return await this.registerSolanaProvider(window.solana, 'phantom') + return await this.registerSolanaProvider(window.solana, SupportedWalletProviders.Phantom) } else { throw new Error('Phantom is not available. Please check the extension is supported and active.') } @@ -512,7 +512,7 @@ export class Web3WalletProvider { const address = await provider.initSafeConnect() - this.registerNewWalletAddress(address, 1, 'SafeConnect', provider, 'evm') + this.registerNewWalletAddress(address, 1, SupportedWalletProviders.SafeConnect, provider, 'evm') return address } @@ -529,7 +529,7 @@ export class Web3WalletProvider { // TODO set chainID // TODO: Create registerFlowProvider method to create event listeners (see registerEvmProvider) - this.registerNewWalletAddress(currentUser.addr, 1, 'flow', fcl, 'flow') + this.registerNewWalletAddress(currentUser.addr, 1, SupportedWalletProviders.Flow, fcl, 'flow') return currentUser.addr } diff --git a/src/wallet/__test__/wallet.spec.ts b/src/wallet/__test__/wallet.spec.ts index 273023e3..733e4e6c 100644 --- a/src/wallet/__test__/wallet.spec.ts +++ b/src/wallet/__test__/wallet.spec.ts @@ -1,10 +1,11 @@ /* eslint-disable no-mixed-spaces-and-tabs */ import { TextDecoder, TextEncoder } from 'text-encoding' +import { Client } from '../../client/index' +import { SafeConnectProvider } from '../SafeConnectProvider' +import { SupportedWalletProviders, Web3WalletProvider } from '../Web3WalletProvider' + global.TextEncoder = TextEncoder global.TextDecoder = TextDecoder -import { Client } from '../../client/index' -import { SafeConnectOptions, SafeConnectProvider } from '../SafeConnectProvider' -import { Web3WalletProvider } from '../Web3WalletProvider' let tokenNegotiatorClient = new Client({ type: 'active', @@ -68,7 +69,7 @@ describe('wallet spec', () => { web3WalletProvider.registerNewWalletAddress( '0x1263b90F4e1DFe89A8f9E623FF57adb252851fC3'.toLocaleLowerCase(), '1', - 'MetaMask', + SupportedWalletProviders.MetaMask, walletConnect, 'evm', ) @@ -81,7 +82,7 @@ describe('wallet spec', () => { web3WalletProvider.registerNewWalletAddress( '0x1263b90F4e1DFe89A8f9E623FF57adb252851fC3'.toLocaleLowerCase(), '1', - 'MetaMask', + SupportedWalletProviders.MetaMask, walletConnect, 'evm', ) @@ -93,12 +94,26 @@ describe('wallet spec', () => { test('web3WalletProvider method registerNewWalletAddress and getConnectedWalletData', async () => { const walletConnectProvider = await import('../WalletConnectProvider') const walletConnect = await walletConnectProvider.getWalletConnectProviderInstance() - expect(web3WalletProvider.registerNewWalletAddress('0x123', '1', 'MetaMask', walletConnect, 'evm')).toEqual('0x123') + expect( + web3WalletProvider.registerNewWalletAddress( + '0x123', + '1', + SupportedWalletProviders.MetaMask, + walletConnect, + 'evm', + ), + ).toEqual('0x123') const TorusProvider = await import('../TorusProvider') const torus = await TorusProvider.getTorusProviderInstance() - expect(web3WalletProvider.registerNewWalletAddress('0x12345', '1', 'phantom', torus.provider, 'solana')).toEqual( - '0x12345', - ) + expect( + web3WalletProvider.registerNewWalletAddress( + '0x12345', + '1', + SupportedWalletProviders.Torus, + torus.provider, + 'solana', + ), + ).toEqual('0x12345') expect(web3WalletProvider.getConnectedWalletData('evm')).toBeDefined() })*/ From e3da753f4d3bf11ef0cc040c3d3315e3ab8184c5 Mon Sep 17 00:00:00 2001 From: Michael Wallace Date: Tue, 21 Mar 2023 14:22:12 +1100 Subject: [PATCH 3/7] feat: Wallet connection manager view Complete initial multi-wallet management UI & wallet disconnect functions. --- src/client/index.ts | 27 +++++++++---- src/client/ui.ts | 2 + src/client/views/manage-wallets.ts | 28 +++++++++---- src/client/views/select-issuers.ts | 63 +++++++++++++++++++++++------- src/client/views/select-wallet.ts | 7 +++- src/theme/common.scss | 3 +- src/wallet/Web3WalletProvider.ts | 28 +++++++++++-- 7 files changed, 123 insertions(+), 35 deletions(-) diff --git a/src/client/index.ts b/src/client/index.ts index 0f40c9b3..4ff9fda4 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -27,7 +27,7 @@ import { SignedUNChallenge } from './auth/signedUNChallenge' import { TicketZKProof } from './auth/ticketZKProof' import { AuthenticationMethod } from './auth/abstractAuthentication' import { isUserAgentSupported, validateBlockchain } from '../utils/support/isSupported' -import Web3WalletProvider from '../wallet/Web3WalletProvider' +import Web3WalletProvider, { SupportedWalletProviders } from '../wallet/Web3WalletProvider' import { LocalOutlet } from '../outlet/localOutlet' import { Outlet, OutletInterface } from '../outlet' import { shouldUseRedirectMode } from '../utils/support/getBrowserData' @@ -86,7 +86,7 @@ export const defaultConfig: NegotiationInterface = { export const enum UIUpdateEventType { ISSUERS_LOADING, ISSUERS_LOADED, - WALLET_DISCONNECTED, + WALLET_CHANGE, } export enum ClientError { @@ -110,7 +110,7 @@ export class Client { private uiUpdateCallbacks: { [type in UIUpdateEventType] } = { [UIUpdateEventType.ISSUERS_LOADING]: undefined, [UIUpdateEventType.ISSUERS_LOADED]: undefined, - [UIUpdateEventType.WALLET_DISCONNECTED]: undefined, + [UIUpdateEventType.WALLET_CHANGE]: undefined, } private urlParams: URLSearchParams @@ -255,13 +255,26 @@ export class Client { return this.web3WalletProvider } - public async disconnectWallet() { + public async disconnectWallet(walletAddress?: string, providerType?: string) { let wp = await this.getWalletProvider() - wp.deleteConnections() - this.tokenStore.clearCachedTokens() + + if (walletAddress) { + const deleted = wp.deleteConnection(walletAddress, providerType as SupportedWalletProviders) + + if (!deleted) return + + this.tokenStore.clearCachedTokens(true, walletAddress) + } else { + wp.deleteConnections() + this.tokenStore.clearCachedTokens() + } + + // TODO: Deprecate use of connected-wallet events for disconnecting wallet this.eventSender('connected-wallet', null) + + // Emit disconnected wallet details this.eventSender('disconnected-wallet', null) - this.triggerUiUpdateCallback(UIUpdateEventType.WALLET_DISCONNECTED) + this.triggerUiUpdateCallback(UIUpdateEventType.WALLET_CHANGE) } async negotiatorConnectToWallet(walletType: string) { diff --git a/src/client/ui.ts b/src/client/ui.ts index 082e835b..d82a48da 100644 --- a/src/client/ui.ts +++ b/src/client/ui.ts @@ -139,6 +139,8 @@ export class Ui implements UiInterface { return SelectWallet case 'manage-wallets': return ManageWallets + default: + throw new Error("Default view '" + type + "' not found") } } diff --git a/src/client/views/manage-wallets.ts b/src/client/views/manage-wallets.ts index d9547d67..efbb09af 100644 --- a/src/client/views/manage-wallets.ts +++ b/src/client/views/manage-wallets.ts @@ -16,7 +16,7 @@ const DisconnectButtonSVG = ` export class ManageWallets extends AbstractView { init() { - this.client.registerUiUpdateCallback(UIUpdateEventType.WALLET_DISCONNECTED, async () => { + this.client.registerUiUpdateCallback(UIUpdateEventType.WALLET_CHANGE, async () => { const wallets = await this.client.getWalletProvider() if (wallets.getConnectionCount() === 0) { @@ -47,7 +47,7 @@ export class ManageWallets extends AbstractView {
` - this.setupWalletButton() + this.setupWalletButtons() this.setupBackButton() } - private setupWalletButton() { - const walletBtn = this.viewContainer.querySelector('.dis-wallet-tn') - walletBtn.addEventListener('click', () => { - this.client.disconnectWallet() + private setupWalletButtons() { + this.viewContainer.querySelectorAll('.dis-wallet-tn').forEach((elem) => + elem.addEventListener('click', (e) => { + const elem = e.currentTarget + this.client.disconnectWallet( + elem.hasAttribute('data-address') ? elem.getAttribute('data-address') : null, + elem.hasAttribute('data-providertype') ? elem.getAttribute('data-providertype') : null, + ) + }), + ) + + const addWalletBtn = this.viewContainer.querySelector('.add-wallet-tn') + addWalletBtn.addEventListener('click', () => { + this.ui.updateUI('wallet', null, { viewTransition: 'slide-in-right', backButtonView: 'manage-wallets' }) }) } @@ -122,7 +132,9 @@ export class ManageWallets extends AbstractView {
${connection.providerType}
-
diff --git a/src/client/views/select-issuers.ts b/src/client/views/select-issuers.ts index c0061502..0bea505c 100644 --- a/src/client/views/select-issuers.ts +++ b/src/client/views/select-issuers.ts @@ -21,21 +21,26 @@ export class SelectIssuers extends AbstractView { this.render() }) - this.client.registerUiUpdateCallback(UIUpdateEventType.WALLET_DISCONNECTED, () => { + this.client.registerUiUpdateCallback(UIUpdateEventType.WALLET_CHANGE, async () => { if (this.client.getTokenStore().hasOnChainTokens()) { - this.ui.updateUI('wallet', { viewName: 'wallet' }, { viewTransition: 'slide-in-left' }) + const wallets = await this.client.getWalletProvider() + if (wallets.getConnectionCount() === 0) { + this.ui.updateUI('wallet', { viewName: 'wallet' }, { viewTransition: 'slide-in-left' }) + } else { + await this.render() + } } else { this.ui.updateUI('start', { viewName: 'start' }, { viewTransition: 'slide-in-left' }) } }) } - render() { - this.renderContent() + async render() { + await this.renderContent() this.afterRender() } - protected renderContent() { + protected async renderContent() { this.viewContainer.innerHTML = `
@@ -47,16 +52,7 @@ export class SelectIssuers extends AbstractView { - + ${await this.renderWalletButton()}
${this.getCustomContent()} @@ -120,6 +116,43 @@ export class SelectIssuers extends AbstractView { return '' } + async renderWalletButton() { + const hasWallets = this.client.getTokenStore().hasOnChainTokens() + const title = hasWallets ? 'Manage Wallets' : 'Disconnect Wallet' + + return ` + + ` + } + async setupWalletButton() { const walletBtn = this.viewContainer.querySelector('.dis-wallet-tn') walletBtn.style.display = 'block' diff --git a/src/client/views/select-wallet.ts b/src/client/views/select-wallet.ts index be930567..01ca9c20 100644 --- a/src/client/views/select-wallet.ts +++ b/src/client/views/select-wallet.ts @@ -7,7 +7,7 @@ import { getBrowserData } from '../../utils/support/getBrowserData' export class SelectWallet extends AbstractView { init() { - this.client.registerUiUpdateCallback(UIUpdateEventType.WALLET_DISCONNECTED, undefined) + this.client.registerUiUpdateCallback(UIUpdateEventType.WALLET_CHANGE, undefined) } // TODO: Accept data param to connect specific wallet - @@ -84,6 +84,11 @@ export class SelectWallet extends AbstractView { this.viewContainer.querySelectorAll('.wallet-button-tn').forEach((elem: any) => { elem.addEventListener('click', this.connectWallet.bind(this)) }) + + if (this.params.viewOptions.backButtonView) + this.viewContainer.querySelector('.back-to-menu-tn').addEventListener('click', () => { + this.ui.updateUI(this.params.viewOptions.backButtonView, null, { viewTransition: 'slide-in-left' }) + }) } private getWalletButtonHtml(wallet: WalletInfo) { diff --git a/src/theme/common.scss b/src/theme/common.scss index 45bc93d9..541ab334 100644 --- a/src/theme/common.scss +++ b/src/theme/common.scss @@ -314,7 +314,8 @@ vertical-align: bottom; white-space: nowrap; width: 100%; - max-width: 90px; + max-width: 180px; + margin-right: -6px; overflow: hidden; text-overflow: ellipsis; } diff --git a/src/wallet/Web3WalletProvider.ts b/src/wallet/Web3WalletProvider.ts index b44e843b..41164e0c 100644 --- a/src/wallet/Web3WalletProvider.ts +++ b/src/wallet/Web3WalletProvider.ts @@ -1,7 +1,7 @@ import { ethers } from 'ethers' import { logger, strToHexStr, strToUtfBytes } from '../utils' import { SafeConnectOptions } from './SafeConnectProvider' -import { Client } from '../client' +import { Client, UIUpdateEventType } from '../client' import { SupportedBlockchainsParam, WalletOptionsInterface, SignatureSupportedBlockchainsParamList } from '../client/interface' interface WalletConnectionState { @@ -53,6 +53,7 @@ export class Web3WalletProvider { emitSavedConnection(address: string): WalletConnection | null { if (Object.keys(this.connections).length && address) { + this.client.triggerUiUpdateCallback(UIUpdateEventType.WALLET_CHANGE) this.client.eventSender('connected-wallet', this.connections[address.toLowerCase()]) return this.connections[address.toLowerCase()] } else { @@ -122,6 +123,27 @@ export class Web3WalletProvider { sessionStorage.removeItem('CURRENT_USER') } + deleteConnection(address: string, providerType: SupportedWalletProviders) { + address = address.toLowerCase() + + // This address has been connected with a different provider, so we don't want to delete it + if (!this.connections[address] || this.connections[address].providerType !== providerType) return false + + providerType = this.connections[address].providerType + delete this.connections[address] + this.saveConnections() + + switch (providerType) { + case 'WalletConnect': + localStorage.removeItem('walletconnect') + break + case 'Flow': + sessionStorage.removeItem('CURRENT_USER') + } + + return true + } + async loadConnections() { let data = localStorage.getItem(Web3WalletProvider.LOCAL_STORAGE_KEY) @@ -291,7 +313,7 @@ export class Web3WalletProvider { * for now user cant connect to multiple wallets * but do we need it for future? */ - this.client.disconnectWallet() + this.client.disconnectWallet(address, providerType) return } @@ -328,7 +350,7 @@ export class Web3WalletProvider { * for now user cant connect to multiple wallets * but do we need it for future? */ - this.client.disconnectWallet() + this.client.disconnectWallet(address, providerType) }) break default: From ce1fd42edbe23ebc2a81565b676815601353bf20 Mon Sep 17 00:00:00 2001 From: Michael Wallace Date: Tue, 21 Mar 2023 20:24:04 +1100 Subject: [PATCH 4/7] fix: tests --- src/utils/support/__tests__/isSupported.spec.ts | 3 +++ src/utils/support/getBrowserData.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils/support/__tests__/isSupported.spec.ts b/src/utils/support/__tests__/isSupported.spec.ts index d0e54892..5de176dc 100644 --- a/src/utils/support/__tests__/isSupported.spec.ts +++ b/src/utils/support/__tests__/isSupported.spec.ts @@ -1,6 +1,8 @@ // @ts-nocheck import { isUserAgentSupported } from '../isSupported' +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { browserData } from '../getBrowserData' describe('browser simulations', () => { test('check if browser is supported', () => { @@ -21,6 +23,7 @@ describe('browser simulations', () => { writable: true, configurable: true, }) + browserData = null // Clear cached UA data expect( isUserAgentSupported({ diff --git a/src/utils/support/getBrowserData.ts b/src/utils/support/getBrowserData.ts index f7e4563c..ad199c28 100644 --- a/src/utils/support/getBrowserData.ts +++ b/src/utils/support/getBrowserData.ts @@ -1,4 +1,4 @@ -let browserData = null +export let browserData = null export const getBrowserData = () => { if (browserData) return browserData From 8f0a804e77d2d401bbd95bdf8e30c70839decec3 Mon Sep 17 00:00:00 2001 From: Michael Wallace Date: Tue, 28 Mar 2023 16:50:20 +1100 Subject: [PATCH 5/7] feat: Add exception to show select-wallet screen for specific blockchain... When "load" is triggered for a blockchain that does not currently have a wallet connected. --- src/client/index.ts | 12 +++++++----- src/client/ui.ts | 5 +++-- src/client/views/select-issuers.ts | 12 +++++++++--- src/client/views/select-wallet.ts | 16 +++++++++++++--- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/client/index.ts b/src/client/index.ts index 4ff9fda4..b270006f 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -275,6 +275,10 @@ export class Client { // Emit disconnected wallet details this.eventSender('disconnected-wallet', null) this.triggerUiUpdateCallback(UIUpdateEventType.WALLET_CHANGE) + + this.eventSender('tokens-selected', { + selectedTokens: this.tokenStore.getSelectedTokens(), + }) } async negotiatorConnectToWallet(walletType: string) { @@ -348,10 +352,6 @@ export class Client { return this.config.type === 'active' } - private createCurrentUrlWithoutHash(): string { - return window.location.origin + window.location.pathname + window.location.search ?? '?' + window.location.search - } - public getNoTokenMsg(collectionID: string) { const store = this.getTokenStore().getCurrentIssuers() const collectionNoTokenMsg = store[collectionID]?.noTokenMsg @@ -664,7 +664,9 @@ export class Client { // TODO: Collect tokens from all addresses for this blockchain const walletAddress = walletProvider.getConnectedWalletAddresses(issuer.blockchain)?.[0] - requiredParams(walletAddress, 'wallet address is missing.') + if (!walletAddress) { + throw new Error('WALLET_REQUIRED') + } // TODO: Allow API to return tokens for multiple addresses let tokens diff --git a/src/client/ui.ts b/src/client/ui.ts index d82a48da..3d007969 100644 --- a/src/client/ui.ts +++ b/src/client/ui.ts @@ -231,7 +231,7 @@ export class Ui implements UiInterface { } } - updateUI(viewFactory: ViewComponent | ViewType, data?: any, options?: any) { + updateUI(viewFactory: ViewComponent | ViewType, data?: any, viewOpts?: any) { let viewOptions: any = {} let viewName = 'unknown' @@ -248,7 +248,8 @@ export class Ui implements UiInterface { if (data?.viewName) viewName = data.viewName } - if (options) viewOptions = { ...viewOptions, ...options } + // Manually specified view options can override ones set in the viewOverrides config + if (viewOpts) viewOptions = { ...viewOptions, ...viewOpts } if (!this.viewContainer) { logger(3, 'Element .view-content-tn not found: popup not initialized') diff --git a/src/client/views/select-issuers.ts b/src/client/views/select-issuers.ts index 0bea505c..99d12074 100644 --- a/src/client/views/select-issuers.ts +++ b/src/client/views/select-issuers.ts @@ -3,7 +3,7 @@ import { TokenList, TokenListItemInterface } from './token-list' import { IconView } from './icon-view' import { logger } from '../../utils' import { UIUpdateEventType } from '../index' -import { Issuer } from '../interface' +import { Issuer, OnChainIssuer } from '../interface' export class SelectIssuers extends AbstractView { issuerListContainer: any @@ -281,13 +281,19 @@ export class SelectIssuers extends AbstractView { if (!tokens) return // Site is redirecting } catch (err) { logger(2, err) + + if (err.message === 'WALLET_REQUIRED') { + this.ui.dismissLoader() + const issuerConfig = this.client.getTokenStore().getCurrentIssuers(true)[issuer] as OnChainIssuer + this.ui.updateUI('wallet', null, { blockchain: issuerConfig.blockchain ?? 'evm' }) + return + } + this.ui.showError(err) this.client.eventSender('error', { issuer, error: err }) return } - this.ui.dismissLoader() - if (!tokens?.length) { this.ui.showError(`No tokens found! ${this.client.getNoTokenMsg(issuer)}`) return diff --git a/src/client/views/select-wallet.ts b/src/client/views/select-wallet.ts index 01ca9c20..e8e879c3 100644 --- a/src/client/views/select-wallet.ts +++ b/src/client/views/select-wallet.ts @@ -10,12 +10,22 @@ export class SelectWallet extends AbstractView { this.client.registerUiUpdateCallback(UIUpdateEventType.WALLET_CHANGE, undefined) } + private shouldShowBlockchain(blockchain: 'evm' | 'solana' | 'flow') { + console.log('Requested blockchain: ', this.params) + + if (this.params.viewOptions.blockchain) { + return this.params.viewOptions.blockchain === blockchain + } + + return this.client.hasIssuerForBlockchain(blockchain) + } + // TODO: Accept data param to connect specific wallet - // this is needed when the user clicks load on a token issuer for a wallet type they have not connected. render() { let walletButtons = '' - if (this.client.hasIssuerForBlockchain('evm')) { + if (this.shouldShowBlockchain('evm')) { if (this.client.safeConnectAvailable()) { const safeConnect = getWalletInfo(SupportedWalletProviders.SafeConnect) walletButtons += this.getWalletButtonHtml(safeConnect) @@ -41,12 +51,12 @@ export class SelectWallet extends AbstractView { } } - if (this.client.hasIssuerForBlockchain('solana')) { + if (this.shouldShowBlockchain('solana')) { const phantom = getWalletInfo(SupportedWalletProviders.Phantom) walletButtons += this.getWalletButtonHtml(phantom) } - if (this.client.hasIssuerForBlockchain('flow')) { + if (this.shouldShowBlockchain('flow')) { const flow = getWalletInfo(SupportedWalletProviders.Flow) walletButtons += this.getWalletButtonHtml(flow) } From 972ac571cdff0400560fe8238ecf341a1934e63c Mon Sep 17 00:00:00 2001 From: Michael Wallace Date: Tue, 28 Mar 2023 16:55:14 +1100 Subject: [PATCH 6/7] chore: remove redundant logging --- src/client/views/select-wallet.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/client/views/select-wallet.ts b/src/client/views/select-wallet.ts index e8e879c3..97758879 100644 --- a/src/client/views/select-wallet.ts +++ b/src/client/views/select-wallet.ts @@ -11,8 +11,6 @@ export class SelectWallet extends AbstractView { } private shouldShowBlockchain(blockchain: 'evm' | 'solana' | 'flow') { - console.log('Requested blockchain: ', this.params) - if (this.params.viewOptions.blockchain) { return this.params.viewOptions.blockchain === blockchain } From af4b4b771e5f848319ae6fb4a3b93a9d1d3e43a3 Mon Sep 17 00:00:00 2001 From: Michael Wallace Date: Wed, 3 May 2023 17:08:29 +1000 Subject: [PATCH 7/7] fix: various fixes for multi-wallet/multi-blockchain support - Specify address & providerType param in client.disconnect() usages in WalletProvider. - Load tokens from multiple wallet addresses for single issuer i.e. when more than one EVM wallet is connected - Ensure signedUNChallenge uses wallet based on walletAddress in the token data. --- src/client/auth/signedUNChallenge.ts | 6 ++--- src/client/index.ts | 34 ++++++++++++++++------------ src/wallet/Web3WalletProvider.ts | 8 +++---- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/client/auth/signedUNChallenge.ts b/src/client/auth/signedUNChallenge.ts index c5f22699..5344ee88 100644 --- a/src/client/auth/signedUNChallenge.ts +++ b/src/client/auth/signedUNChallenge.ts @@ -15,8 +15,8 @@ export class SignedUNChallenge extends AbstractAuthentication implements Authent ): Promise { let web3WalletProvider = await this.client.getWalletProvider() - // TODO: Update once Flow & Solana signing support is added - let connection = web3WalletProvider.getSingleSignatureCompatibleConnection() + let connection = + web3WalletProvider.getConnectionByAddress(_tokens[0].walletAddress) ?? web3WalletProvider.getSingleSignatureCompatibleConnection() if (!connection) { throw new Error('WALLET_REQUIRED') } @@ -32,7 +32,7 @@ export class SignedUNChallenge extends AbstractAuthentication implements Authent if (currentProof) { let unChallenge = currentProof?.data as UNInterface - if (unChallenge.expiration < Date.now() || !UN.verifySignature(unChallenge)) { + if (unChallenge.expiration < Date.now() || !(await UN.verifySignature(unChallenge))) { this.deleteProof(address) currentProof = null } diff --git a/src/client/index.ts b/src/client/index.ts index b270006f..1b0ae06f 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -661,28 +661,32 @@ export class Client { private async loadOnChainTokens(issuer: OnChainIssuer): Promise { let walletProvider = await this.getWalletProvider() - // TODO: Collect tokens from all addresses for this blockchain - const walletAddress = walletProvider.getConnectedWalletAddresses(issuer.blockchain)?.[0] + const walletAddresses = walletProvider.getConnectedWalletAddresses(issuer.blockchain) - if (!walletAddress) { + if (!walletAddresses.length) { throw new Error('WALLET_REQUIRED') } - // TODO: Allow API to return tokens for multiple addresses - let tokens + const combinedTokens = [] - if (issuer.fungible) { - tokens = await getFungibleTokenBalances(issuer, walletAddress) - } else { - tokens = await getNftTokens(issuer, walletAddress) - } + for (const walletAddress of walletAddresses) { + let tokens - tokens.map((token) => { - token.walletAddress = walletAddress - return token - }) + if (issuer.fungible) { + tokens = await getFungibleTokenBalances(issuer, walletAddress) + } else { + tokens = await getNftTokens(issuer, walletAddress) + } - return tokens + tokens.map((token) => { + token.walletAddress = walletAddress + return token + }) + + combinedTokens.push(...tokens) + } + + return combinedTokens } private async loadRemoteOutletTokens(issuer: OffChainTokenConfig): Promise { diff --git a/src/wallet/Web3WalletProvider.ts b/src/wallet/Web3WalletProvider.ts index 41164e0c..f442cc57 100644 --- a/src/wallet/Web3WalletProvider.ts +++ b/src/wallet/Web3WalletProvider.ts @@ -265,7 +265,7 @@ export class Web3WalletProvider { provider.on('connect', (publicKey) => { let newAddress = publicKey.toBase58() logger(2, 'connected wallet: ', newAddress) - this.registerNewWalletAddress(newAddress, 'mainnet-beta', 'phantom', window.solana, 'solana') + this.registerNewWalletAddress(newAddress, 'mainnet-beta', SupportedWalletProviders.Phantom, window.solana, 'solana') }) provider.on('disconnect', () => { @@ -276,7 +276,7 @@ export class Web3WalletProvider { * for now user cant connect to multiple wallets * but do we need it for future? */ - this.client.disconnectWallet() + this.client.disconnectWallet(address, providerType) }) provider.on('accountChanged', (publicKey) => { @@ -284,7 +284,7 @@ export class Web3WalletProvider { if (publicKey) { // Set new public key and continue as usual logger(2, `Switched to account ${publicKey.toBase58()}`) - this.registerNewWalletAddress(publicKey.toBase58(), 'mainnet-beta', 'phantom', window.solana, 'solana') + this.registerNewWalletAddress(publicKey.toBase58(), 'mainnet-beta', SupportedWalletProviders.Phantom, window.solana, 'solana') } else { logger(2, 'Disconnected from wallet') delete this.connections[address.toLowerCase()] @@ -293,7 +293,7 @@ export class Web3WalletProvider { * for now user cant connect to multiple wallets * but do we need it for future? */ - this.client.disconnectWallet() + this.client.disconnectWallet(address, providerType) } }) break