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 0f40c9b3..1b0ae06f 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,30 @@ 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) + + this.eventSender('tokens-selected', { + selectedTokens: this.tokenStore.getSelectedTokens(), + }) } async negotiatorConnectToWallet(walletType: string) { @@ -335,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 @@ -648,26 +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 (!walletAddresses.length) { + throw new Error('WALLET_REQUIRED') + } - requiredParams(walletAddress, 'wallet address is missing.') + const combinedTokens = [] - // TODO: Allow API to return tokens for multiple addresses - let tokens + for (const walletAddress of walletAddresses) { + let tokens - if (issuer.fungible) { - tokens = await getFungibleTokenBalances(issuer, walletAddress) - } else { - tokens = await getNftTokens(issuer, walletAddress) - } + if (issuer.fungible) { + tokens = await getFungibleTokenBalances(issuer, walletAddress) + } else { + tokens = await getNftTokens(issuer, walletAddress) + } - tokens.map((token) => { - token.walletAddress = walletAddress - return token - }) + tokens.map((token) => { + token.walletAddress = walletAddress + return token + }) - return tokens + combinedTokens.push(...tokens) + } + + return combinedTokens } private async loadRemoteOutletTokens(issuer: OffChainTokenConfig): Promise { diff --git a/src/client/ui.ts b/src/client/ui.ts index 655b155b..3d007969 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,10 @@ export class Ui implements UiInterface { return SelectIssuers case 'wallet': return SelectWallet + case 'manage-wallets': + return ManageWallets + default: + throw new Error("Default view '" + type + "' not found") } } @@ -226,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' @@ -243,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/manage-wallets.ts b/src/client/views/manage-wallets.ts index 0ac71a2a..efbb09af 100644 --- a/src/client/views/manage-wallets.ts +++ b/src/client/views/manage-wallets.ts @@ -1,9 +1,149 @@ import { AbstractView } from './view-interface' +import { SupportedBlockchainsParam } from '../interface' +import { UIUpdateEventType } from '../index' +import { getWalletInfo } from './utils/wallet-info' -class ManageWallets extends AbstractView { - render() { +const DisconnectButtonSVG = ` + + + + + + + + +` + +export class ManageWallets extends AbstractView { + init() { + this.client.registerUiUpdateCallback(UIUpdateEventType.WALLET_CHANGE, 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

+
+
+ + +
+
+
+
+ ${await this.renderCurrentWalletConnections()} +
+
+
` + + this.setupWalletButtons() + this.setupBackButton() + } + + 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' }) + }) + } + + 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 += ` +
+
+ ${getWalletInfo(connection.providerType)?.imgBig} +
+
+ + ${address.substring(0, address.length - 5)} + ${address.substring(address.length - 5, address.length)} + +
${connection.providerType}
+
+
+ +
+
+ ` + } + } + } + + return html } } diff --git a/src/client/views/select-issuers.ts b/src/client/views/select-issuers.ts index 0353a25a..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 @@ -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,18 +52,7 @@ export class SelectIssuers extends AbstractView { - + ${await this.renderWalletButton()}
${this.getCustomContent()} @@ -71,7 +65,7 @@ export class SelectIssuers extends AbstractView {