From 7e691d4ef9310f19f716d5ddf47aabaa056ed71b Mon Sep 17 00:00:00 2001 From: tomiir Date: Thu, 13 Feb 2025 15:26:23 +0100 Subject: [PATCH 1/2] feat: always use portfolio screen. Fix issue with account switching --- packages/appkit/src/client.ts | 12 +- .../tests/client/public-methods.test.ts | 3 +- packages/scaffold-ui/exports/index.ts | 1 - .../index.ts | 336 ------------------ .../src/views/w3m-account-view/index.ts | 321 ++++++++++++++++- .../w3m-account-view}/styles.ts | 0 .../src/views/w3m-profile-view/index.ts | 7 +- ...w3m-account-wallet-features-widget.test.ts | 148 -------- 8 files changed, 317 insertions(+), 511 deletions(-) delete mode 100644 packages/scaffold-ui/src/partials/w3m-account-wallet-features-widget/index.ts rename packages/scaffold-ui/src/{partials/w3m-account-wallet-features-widget => views/w3m-account-view}/styles.ts (100%) delete mode 100644 packages/scaffold-ui/test/partials/w3m-account-wallet-features-widget.test.ts diff --git a/packages/appkit/src/client.ts b/packages/appkit/src/client.ts index 04dc7097da..ad9cc420ac 100644 --- a/packages/appkit/src/client.ts +++ b/packages/appkit/src/client.ts @@ -355,10 +355,6 @@ export class AppKit { return AccountController.subscribeKey('connectedWalletInfo', callback) } - public subscribeShouldUpdateToAddress(callback: (newState?: string) => void) { - AccountController.subscribeKey('shouldUpdateToAddress', callback) - } - public subscribeCaipNetworkChange(callback: (newState?: CaipNetwork) => void) { ChainController.subscribeKey('activeCaipNetwork', callback) } @@ -857,6 +853,14 @@ export class AppKit { if (options.excludeWalletIds) { ApiController.initializeExcludedWalletRdns({ ids: options.excludeWalletIds }) } + + AccountController.subscribeKey('shouldUpdateToAddress', address => { + const chainNamespace = ChainController.state.activeChain as ChainNamespace + const chainId = this.getChainId() + if (address && chainNamespace && chainId) { + this.syncAccount({ address, chainNamespace, chainId }) + } + }) } private getDefaultMetaData() { diff --git a/packages/appkit/tests/client/public-methods.test.ts b/packages/appkit/tests/client/public-methods.test.ts index 18f2b84d89..4bee015aa7 100644 --- a/packages/appkit/tests/client/public-methods.test.ts +++ b/packages/appkit/tests/client/public-methods.test.ts @@ -201,8 +201,7 @@ describe('Base Public methods', () => { const subscribe = vi.spyOn(AccountController, 'subscribeKey') const callback = vi.fn() - const appKit = new AppKit(mockOptions) - appKit.subscribeShouldUpdateToAddress(callback) + new AppKit(mockOptions) expect(subscribe).toHaveBeenCalledWith('shouldUpdateToAddress', callback) }) diff --git a/packages/scaffold-ui/exports/index.ts b/packages/scaffold-ui/exports/index.ts index b255577c74..375d07e668 100644 --- a/packages/scaffold-ui/exports/index.ts +++ b/packages/scaffold-ui/exports/index.ts @@ -91,7 +91,6 @@ export * from '../src/partials/w3m-snackbar/index.js' export * from '../src/partials/w3m-alertbar/index.js' export * from '../src/partials/w3m-email-login-widget/index.js' export * from '../src/partials/w3m-account-default-widget/index.js' -export * from '../src/partials/w3m-account-wallet-features-widget/index.js' export * from '../src/partials/w3m-account-activity-widget/index.js' export * from '../src/partials/w3m-account-nfts-widget/index.js' export * from '../src/partials/w3m-account-tokens-widget/index.js' diff --git a/packages/scaffold-ui/src/partials/w3m-account-wallet-features-widget/index.ts b/packages/scaffold-ui/src/partials/w3m-account-wallet-features-widget/index.ts deleted file mode 100644 index 648a6087f4..0000000000 --- a/packages/scaffold-ui/src/partials/w3m-account-wallet-features-widget/index.ts +++ /dev/null @@ -1,336 +0,0 @@ -import { LitElement, html } from 'lit' -import { state } from 'lit/decorators.js' -import { ifDefined } from 'lit/directives/if-defined.js' - -import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common' -import { - AccountController, - AssetController, - AssetUtil, - ChainController, - ConstantsUtil as CoreConstantsUtil, - CoreHelperUtil, - EventsController, - ModalController, - OptionsController, - RouterController -} from '@reown/appkit-core' -import { customElement } from '@reown/appkit-ui' -import { W3mFrameRpcConstants } from '@reown/appkit-wallet' - -import { ConstantsUtil } from '../../utils/ConstantsUtil.js' -import styles from './styles.js' - -const TABS = 3 -const TABS_PADDING = 48 -const MODAL_MOBILE_VIEW_PX = 430 - -@customElement('w3m-account-wallet-features-widget') -export class W3mAccountWalletFeaturesWidget extends LitElement { - public static override styles = styles - - // -- Members ------------------------------------------- // - @state() private watchTokenBalance?: ReturnType - - private unsubscribe: (() => void)[] = [] - - // -- State & Properties -------------------------------- // - @state() private address = AccountController.state.address - - @state() private profileImage = AccountController.state.profileImage - - @state() private profileName = AccountController.state.profileName - - @state() private network = ChainController.state.activeCaipNetwork - - @state() private currentTab = AccountController.state.currentTab - - @state() private tokenBalance = AccountController.state.tokenBalance - - @state() private features = OptionsController.state.features - - @state() private networkImage = AssetUtil.getNetworkImage(this.network) - - public constructor() { - super() - this.unsubscribe.push( - ...[ - AssetController.subscribeNetworkImages(() => { - this.networkImage = AssetUtil.getNetworkImage(this.network) - }), - AccountController.subscribe(val => { - if (val.address) { - this.address = val.address - this.profileImage = val.profileImage - this.profileName = val.profileName - this.currentTab = val.currentTab - this.tokenBalance = val.tokenBalance - } else { - ModalController.close() - } - }) - ], - ChainController.subscribeKey('activeCaipNetwork', val => (this.network = val)), - OptionsController.subscribeKey('features', val => (this.features = val)) - ) - this.watchSwapValues() - } - - public override disconnectedCallback() { - this.unsubscribe.forEach(unsubscribe => unsubscribe()) - clearInterval(this.watchTokenBalance) - } - - public override firstUpdated() { - AccountController.fetchTokenBalance() - } - - // -- Render -------------------------------------------- // - public override render() { - if (!this.address) { - throw new Error('w3m-account-view: No account provided') - } - - return html` - ${this.network && html``} - - - ${this.tokenBalanceTemplate()} ${this.orderedWalletFeatures()} - - - ${this.listContentTemplate()} - ` - } - - // -- Private ------------------------------------------- // - private orderedWalletFeatures() { - const walletFeaturesOrder = - this.features?.walletFeaturesOrder || CoreConstantsUtil.DEFAULT_FEATURES.walletFeaturesOrder - const isAllDisabled = walletFeaturesOrder.every(feature => !this.features?.[feature]) - - if (isAllDisabled) { - return null - } - - return html` - ${walletFeaturesOrder.map(feature => { - switch (feature) { - case 'onramp': - return this.onrampTemplate() - case 'swaps': - return this.swapsTemplate() - case 'receive': - return this.receiveTemplate() - case 'send': - return this.sendTemplate() - default: - return null - } - })} - ` - } - - private onrampTemplate() { - const onramp = this.features?.onramp - - if (!onramp) { - return null - } - - return html` - - - - ` - } - - private swapsTemplate() { - const swaps = this.features?.swaps - const isEvm = ChainController.state.activeChain === CommonConstantsUtil.CHAIN.EVM - - if (!swaps || !isEvm) { - return null - } - - return html` - - - - - ` - } - - private receiveTemplate() { - const receive = this.features?.receive - - if (!receive) { - return null - } - - return html` - - - - - ` - } - - private sendTemplate() { - const send = this.features?.send - const isEvm = ChainController.state.activeChain === CommonConstantsUtil.CHAIN.EVM - - if (!send || !isEvm) { - return null - } - - return html` - - - - ` - } - - private watchSwapValues() { - this.watchTokenBalance = setInterval( - () => AccountController.fetchTokenBalance(error => this.onTokenBalanceError(error)), - 10_000 - ) - } - - private onTokenBalanceError(error: unknown) { - if (error instanceof Error && error.cause instanceof Response) { - const statusCode = error.cause.status - - if (statusCode === CommonConstantsUtil.HTTP_STATUS_CODES.SERVICE_UNAVAILABLE) { - clearInterval(this.watchTokenBalance) - } - } - } - - private listContentTemplate() { - if (this.currentTab === 0) { - return html`` - } - if (this.currentTab === 1) { - return html`` - } - if (this.currentTab === 2) { - return html`` - } - - return html`` - } - - private tokenBalanceTemplate() { - if (this.tokenBalance && this.tokenBalance?.length >= 0) { - const value = CoreHelperUtil.calculateBalance(this.tokenBalance) - const { dollars = '0', pennies = '00' } = CoreHelperUtil.formatTokenBalance(value) - - return html`` - } - - return html`` - } - - private onTabChange(index: number) { - AccountController.setCurrentTab(index) - } - - private onProfileButtonClick() { - const { allAccounts } = AccountController.state - - if (allAccounts.length > 1) { - RouterController.push('Profile') - } else { - RouterController.push('AccountSettings') - } - } - - private onBuyClick() { - RouterController.push('OnRampProviders') - } - - private onSwapClick() { - if ( - this.network?.caipNetworkId && - !CoreConstantsUtil.SWAP_SUPPORTED_NETWORKS.includes(this.network?.caipNetworkId) - ) { - RouterController.push('UnsupportedChain', { - swapUnsupportedChain: true - }) - } else { - EventsController.sendEvent({ - type: 'track', - event: 'OPEN_SWAP', - properties: { - network: this.network?.caipNetworkId || '', - isSmartAccount: - AccountController.state.preferredAccountType === - W3mFrameRpcConstants.ACCOUNT_TYPES.SMART_ACCOUNT - } - }) - RouterController.push('Swap') - } - } - - private onReceiveClick() { - RouterController.push('WalletReceive') - } - - private onSendClick() { - EventsController.sendEvent({ - type: 'track', - event: 'OPEN_SEND', - properties: { - network: this.network?.caipNetworkId || '', - isSmartAccount: - AccountController.state.preferredAccountType === - W3mFrameRpcConstants.ACCOUNT_TYPES.SMART_ACCOUNT - } - }) - RouterController.push('WalletSend') - } -} - -declare global { - interface HTMLElementTagNameMap { - 'w3m-account-wallet-features-widget': W3mAccountWalletFeaturesWidget - } -} diff --git a/packages/scaffold-ui/src/views/w3m-account-view/index.ts b/packages/scaffold-ui/src/views/w3m-account-view/index.ts index 236d2758e9..ccd6dfae99 100644 --- a/packages/scaffold-ui/src/views/w3m-account-view/index.ts +++ b/packages/scaffold-ui/src/views/w3m-account-view/index.ts @@ -1,51 +1,336 @@ import { LitElement, html } from 'lit' import { state } from 'lit/decorators.js' +import { ifDefined } from 'lit/directives/if-defined.js' import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common' -import { ChainController, ConnectorController, StorageUtil } from '@reown/appkit-core' +import { + AccountController, + AssetController, + AssetUtil, + ChainController, + ConstantsUtil as CoreConstantsUtil, + CoreHelperUtil, + EventsController, + ModalController, + OptionsController, + RouterController +} from '@reown/appkit-core' import { customElement } from '@reown/appkit-ui' +import { W3mFrameRpcConstants } from '@reown/appkit-wallet' + +import { ConstantsUtil } from '../../utils/ConstantsUtil.js' +import styles from './styles.js' + +const TABS = 3 +const TABS_PADDING = 48 +const MODAL_MOBILE_VIEW_PX = 430 @customElement('w3m-account-view') export class W3mAccountView extends LitElement { + public static override styles = styles + // -- Members ------------------------------------------- // + @state() private watchTokenBalance?: ReturnType + private unsubscribe: (() => void)[] = [] - @state() namespace = ChainController.state.activeChain + // -- State & Properties -------------------------------- // + @state() private address = AccountController.state.address + + @state() private profileImage = AccountController.state.profileImage + + @state() private profileName = AccountController.state.profileName + + @state() private network = ChainController.state.activeCaipNetwork + + @state() private currentTab = AccountController.state.currentTab + + @state() private tokenBalance = AccountController.state.tokenBalance + + @state() private features = OptionsController.state.features + + @state() private networkImage = AssetUtil.getNetworkImage(this.network) + + @state() private namespace = ChainController.state.activeChain - // -- Lifecycle ----------------------------------------- // public constructor() { super() this.unsubscribe.push( - ChainController.subscribeKey('activeChain', namespace => { - this.namespace = namespace - }) + ...[ + AssetController.subscribeNetworkImages(() => { + this.networkImage = AssetUtil.getNetworkImage(this.network) + }), + AccountController.subscribe(val => { + if (val.address) { + this.address = val.address + this.profileImage = val.profileImage + this.profileName = val.profileName + this.currentTab = val.currentTab + this.tokenBalance = val.tokenBalance + } else { + ModalController.close() + } + }), + ChainController.subscribeKey('activeChain', namespace => { + this.namespace = namespace + }) + ], + ChainController.subscribeKey('activeCaipNetwork', val => (this.network = val)), + OptionsController.subscribeKey('features', val => (this.features = val)) ) + this.watchSwapValues() } - // -- Render -------------------------------------------- // + public override disconnectedCallback() { + this.unsubscribe.forEach(unsubscribe => unsubscribe()) + clearInterval(this.watchTokenBalance) + } + + public override firstUpdated() { + AccountController.fetchTokenBalance() + } + // -- Render -------------------------------------------- // public override render() { - if (!this.namespace) { + if (!this.address || !this.namespace) { + throw new Error('w3m-account-view: No account or namespace provided') + } + + return html` + ${this.network && html``} + + + ${this.tokenBalanceTemplate()} ${this.orderedWalletFeatures()} + + + ${this.listContentTemplate()} + ` + } + + // -- Private ------------------------------------------- // + private orderedWalletFeatures() { + const walletFeaturesOrder = + this.features?.walletFeaturesOrder || CoreConstantsUtil.DEFAULT_FEATURES.walletFeaturesOrder + const isAllDisabled = walletFeaturesOrder.every(feature => !this.features?.[feature]) + + if (isAllDisabled) { return null } - const connectorId = StorageUtil.getConnectedConnectorId(this.namespace) - const authConnector = ConnectorController.getAuthConnector() + return html` + ${walletFeaturesOrder.map(feature => { + switch (feature) { + case 'onramp': + return this.onrampTemplate() + case 'swaps': + return this.swapsTemplate() + case 'receive': + return this.receiveTemplate() + case 'send': + return this.sendTemplate() + default: + return null + } + })} + ` + } + + private onrampTemplate() { + const onramp = this.features?.onramp + + if (!onramp) { + return null + } return html` - ${authConnector && connectorId === CommonConstantsUtil.CONNECTOR_ID.AUTH - ? this.walletFeaturesTemplate() - : this.defaultTemplate()} + + + ` } - // -- Private ------------------------------------------- // - private walletFeaturesTemplate() { - return html`` + private swapsTemplate() { + const swaps = this.features?.swaps + const isEvm = ChainController.state.activeChain === CommonConstantsUtil.CHAIN.EVM + + if (!swaps || !isEvm) { + return null + } + + return html` + + + + + ` + } + + private receiveTemplate() { + const receive = this.features?.receive + + if (!receive) { + return null + } + + return html` + + + + + ` + } + + private sendTemplate() { + const send = this.features?.send + const isEvm = ChainController.state.activeChain === CommonConstantsUtil.CHAIN.EVM + + if (!send || !isEvm) { + return null + } + + return html` + + + + ` + } + + private watchSwapValues() { + this.watchTokenBalance = setInterval( + () => AccountController.fetchTokenBalance(error => this.onTokenBalanceError(error)), + 10_000 + ) + } + + private onTokenBalanceError(error: unknown) { + if (error instanceof Error && error.cause instanceof Response) { + const statusCode = error.cause.status + + if (statusCode === CommonConstantsUtil.HTTP_STATUS_CODES.SERVICE_UNAVAILABLE) { + clearInterval(this.watchTokenBalance) + } + } + } + + private listContentTemplate() { + if (this.currentTab === 0) { + return html`` + } + if (this.currentTab === 1) { + return html`` + } + if (this.currentTab === 2) { + return html`` + } + + return html`` + } + + private tokenBalanceTemplate() { + if (this.tokenBalance && this.tokenBalance?.length >= 0) { + const value = CoreHelperUtil.calculateBalance(this.tokenBalance) + const { dollars = '0', pennies = '00' } = CoreHelperUtil.formatTokenBalance(value) + + return html`` + } + + return html`` + } + + private onTabChange(index: number) { + AccountController.setCurrentTab(index) + } + + private onProfileButtonClick() { + const { allAccounts } = AccountController.state + + if (allAccounts.length > 1) { + RouterController.push('Profile') + } else { + RouterController.push('AccountSettings') + } + } + + private onBuyClick() { + RouterController.push('OnRampProviders') + } + + private onSwapClick() { + if ( + this.network?.caipNetworkId && + !CoreConstantsUtil.SWAP_SUPPORTED_NETWORKS.includes(this.network?.caipNetworkId) + ) { + RouterController.push('UnsupportedChain', { + swapUnsupportedChain: true + }) + } else { + EventsController.sendEvent({ + type: 'track', + event: 'OPEN_SWAP', + properties: { + network: this.network?.caipNetworkId || '', + isSmartAccount: + AccountController.state.preferredAccountType === + W3mFrameRpcConstants.ACCOUNT_TYPES.SMART_ACCOUNT + } + }) + RouterController.push('Swap') + } + } + + private onReceiveClick() { + RouterController.push('WalletReceive') } - private defaultTemplate() { - return html`` + private onSendClick() { + EventsController.sendEvent({ + type: 'track', + event: 'OPEN_SEND', + properties: { + network: this.network?.caipNetworkId || '', + isSmartAccount: + AccountController.state.preferredAccountType === + W3mFrameRpcConstants.ACCOUNT_TYPES.SMART_ACCOUNT + } + }) + RouterController.push('WalletSend') } } diff --git a/packages/scaffold-ui/src/partials/w3m-account-wallet-features-widget/styles.ts b/packages/scaffold-ui/src/views/w3m-account-view/styles.ts similarity index 100% rename from packages/scaffold-ui/src/partials/w3m-account-wallet-features-widget/styles.ts rename to packages/scaffold-ui/src/views/w3m-account-view/styles.ts diff --git a/packages/scaffold-ui/src/views/w3m-profile-view/index.ts b/packages/scaffold-ui/src/views/w3m-profile-view/index.ts index 40b27513a2..ce8728c3aa 100644 --- a/packages/scaffold-ui/src/views/w3m-profile-view/index.ts +++ b/packages/scaffold-ui/src/views/w3m-profile-view/index.ts @@ -2,6 +2,7 @@ import { LitElement, html } from 'lit' import { state } from 'lit/decorators.js' import { ifDefined } from 'lit/directives/if-defined.js' +import { ConstantsUtil } from '@reown/appkit-common' import { AccountController, type AccountType, @@ -132,8 +133,10 @@ export class W3mProfileView extends LitElement { private async onSwitchAccount(account: AccountType) { this.loading = true - const emailConnector = ConnectorController.getAuthConnector() - if (emailConnector) { + const isConnectedWithAuth = + ConnectorController.state.activeConnector?.id === ConstantsUtil.CONNECTOR_ID.AUTH + + if (isConnectedWithAuth) { const type = account.type as W3mFrameTypes.AccountType await ConnectionController.setPreferredAccountType(type) } diff --git a/packages/scaffold-ui/test/partials/w3m-account-wallet-features-widget.test.ts b/packages/scaffold-ui/test/partials/w3m-account-wallet-features-widget.test.ts deleted file mode 100644 index 2ecf92abad..0000000000 --- a/packages/scaffold-ui/test/partials/w3m-account-wallet-features-widget.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { elementUpdated, fixture } from '@open-wc/testing' -import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest' - -import { html } from 'lit' - -import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common' -import { AccountController, CoreHelperUtil, RouterController } from '@reown/appkit-core' - -import { W3mAccountWalletFeaturesWidget } from '../../src/partials/w3m-account-wallet-features-widget' -import { HelpersUtil } from '../utils/HelpersUtil' - -// --- Constants ---------------------------------------------------- // -const WALLET_FEATURE_WIDGET_TEST_ID = 'w3m-account-wallet-features-widget' -const MOCK_ADDRESS = '0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826' -const PROFILE_BUTTON = 'w3m-profile-button' - -const SERVICE_UNAVAILABLE_MESSAGE = 'Service Unavailable' - -const ACCOUNT = { - namespace: 'eip155', - address: '0x123', - type: 'eoa' -} as const - -describe('W3mAccountWalletFeaturesWidget', () => { - beforeAll(() => { - vi.spyOn(CoreHelperUtil, 'isMobile').mockReturnValue(false) - }) - - afterEach(() => { - vi.resetAllMocks() - }) - - it('it should not return any components if address is not provided in AccountController', () => { - vi.spyOn(AccountController, 'state', 'get').mockReturnValue({ - ...AccountController.state, - address: undefined - }) - expect(() => - fixture(html``) - ).rejects.toThrow('w3m-account-view: No account provided') - }) - - it('it should return all components if address is provided in AccountsController', async () => { - vi.spyOn(AccountController, 'state', 'get').mockReturnValue({ - ...AccountController.state, - address: MOCK_ADDRESS - }) - - const element: W3mAccountWalletFeaturesWidget = await fixture( - html`` - ) - - element.requestUpdate() - await elementUpdated(element) - - expect(HelpersUtil.getByTestId(element, WALLET_FEATURE_WIDGET_TEST_ID)).not.toBeNull() - }) - - it('should redirect to AccountSettings view if has one account', async () => { - vi.spyOn(AccountController, 'state', 'get').mockReturnValue({ - ...AccountController.state, - address: ACCOUNT.address - }) - vi.spyOn(AccountController, 'state', 'get').mockReturnValue({ - ...AccountController.state, - address: ACCOUNT.address, - allAccounts: [ACCOUNT] - }) - - const pushSpy = vi.spyOn(RouterController, 'push') - - const element: W3mAccountWalletFeaturesWidget = await fixture( - html`` - ) - - const profileButton = HelpersUtil.getByTestId(element, PROFILE_BUTTON) - - expect(profileButton).not.toBeNull() - - const button = HelpersUtil.querySelect(profileButton, 'button') - - button.click() - - expect(pushSpy).toHaveBeenCalledWith('AccountSettings') - }) - - it('should redirect to Profile view if has more than one account', async () => { - vi.spyOn(AccountController, 'state', 'get').mockReturnValue({ - ...AccountController.state, - address: ACCOUNT.address - }) - vi.spyOn(AccountController, 'state', 'get').mockReturnValue({ - ...AccountController.state, - address: ACCOUNT.address, - allAccounts: [ACCOUNT, ACCOUNT] - }) - - const pushSpy = vi.spyOn(RouterController, 'push') - - const element: W3mAccountWalletFeaturesWidget = await fixture( - html`` - ) - - const profileButton = HelpersUtil.getByTestId(element, PROFILE_BUTTON) - - expect(profileButton).not.toBeNull() - - const button = HelpersUtil.querySelect(profileButton, 'button') - - button.click() - - expect(pushSpy).toHaveBeenCalledWith('Profile') - }) - - it('should clearInterval when fetchTokenBalance fails after 10 seconds', async () => { - vi.useFakeTimers() - vi.spyOn(global, 'setInterval') - vi.spyOn(global, 'clearInterval') - vi.spyOn(AccountController, 'state', 'get').mockReturnValue({ - ...AccountController.state, - address: ACCOUNT.address - }) - - const element: W3mAccountWalletFeaturesWidget = await fixture( - html`` - ) - - expect(setInterval).toHaveBeenCalled() - - const response = new Response(SERVICE_UNAVAILABLE_MESSAGE, { - status: CommonConstantsUtil.HTTP_STATUS_CODES.SERVICE_UNAVAILABLE - }) - - const error = new Error(SERVICE_UNAVAILABLE_MESSAGE, { cause: response }) - - vi.spyOn(AccountController, 'fetchTokenBalance').mockImplementation(async callback => { - callback?.(error) - return [] - }) - - vi.advanceTimersByTime(10_000) - - expect(clearInterval).toHaveBeenCalledWith((element as any).watchTokenBalance) - - vi.useRealTimers() - }) -}) From 2a6a1731200dc164a9a174c22c4bc28388680a01 Mon Sep 17 00:00:00 2001 From: tomiir Date: Thu, 13 Feb 2025 15:36:04 +0100 Subject: [PATCH 2/2] chore: remove default widget --- packages/scaffold-ui/exports/index.ts | 1 - .../w3m-account-default-widget/index.ts | 452 ------------------ .../w3m-account-default-widget/styles.ts | 118 ----- .../w3m-default-account-widget.test.ts | 301 ------------ 4 files changed, 872 deletions(-) delete mode 100644 packages/scaffold-ui/src/partials/w3m-account-default-widget/index.ts delete mode 100644 packages/scaffold-ui/src/partials/w3m-account-default-widget/styles.ts delete mode 100644 packages/scaffold-ui/test/partials/w3m-default-account-widget.test.ts diff --git a/packages/scaffold-ui/exports/index.ts b/packages/scaffold-ui/exports/index.ts index 375d07e668..b158d1d856 100644 --- a/packages/scaffold-ui/exports/index.ts +++ b/packages/scaffold-ui/exports/index.ts @@ -90,7 +90,6 @@ export * from '../src/partials/w3m-onramp-providers-footer/index.js' export * from '../src/partials/w3m-snackbar/index.js' export * from '../src/partials/w3m-alertbar/index.js' export * from '../src/partials/w3m-email-login-widget/index.js' -export * from '../src/partials/w3m-account-default-widget/index.js' export * from '../src/partials/w3m-account-activity-widget/index.js' export * from '../src/partials/w3m-account-nfts-widget/index.js' export * from '../src/partials/w3m-account-tokens-widget/index.js' diff --git a/packages/scaffold-ui/src/partials/w3m-account-default-widget/index.ts b/packages/scaffold-ui/src/partials/w3m-account-default-widget/index.ts deleted file mode 100644 index 4c000b1da2..0000000000 --- a/packages/scaffold-ui/src/partials/w3m-account-default-widget/index.ts +++ /dev/null @@ -1,452 +0,0 @@ -import { LitElement, html } from 'lit' -import { state } from 'lit/decorators.js' -import { ifDefined } from 'lit/directives/if-defined.js' - -import { type ChainNamespace, ConstantsUtil } from '@reown/appkit-common' -import { - AccountController, - type AccountType, - ChainController, - ConnectionController, - ConnectorController, - ConstantsUtil as CoreConstantsUtil, - CoreHelperUtil, - EventsController, - ModalController, - OptionsController, - RouterController, - SnackController, - StorageUtil -} from '@reown/appkit-core' -import { UiHelperUtil, customElement } from '@reown/appkit-ui' -import { W3mFrameRpcConstants } from '@reown/appkit-wallet' - -import styles from './styles.js' - -@customElement('w3m-account-default-widget') -export class W3mAccountDefaultWidget extends LitElement { - public static override styles = styles - - // -- Members ------------------------------------------- // - private unsubscribe: (() => void)[] = [] - - // -- State & Properties -------------------------------- // - @state() public caipAddress = AccountController.state.caipAddress - - @state() public address = CoreHelperUtil.getPlainAddress(AccountController.state.caipAddress) - - @state() public allAccounts: AccountType[] = AccountController.state.allAccounts - - @state() private profileImage = AccountController.state.profileImage - - @state() private profileName = AccountController.state.profileName - - @state() private disconnecting = false - - @state() private balance = AccountController.state.balance - - @state() private balanceSymbol = AccountController.state.balanceSymbol - - @state() private features = OptionsController.state.features - - @state() private namespace = ChainController.state.activeChain - - @state() private chainId = ChainController.state.activeCaipNetwork?.id - - public constructor() { - super() - this.unsubscribe.push( - ...[ - AccountController.subscribeKey('caipAddress', val => { - this.address = CoreHelperUtil.getPlainAddress(val) - this.caipAddress = val - }), - AccountController.subscribeKey('balance', val => (this.balance = val)), - AccountController.subscribeKey('balanceSymbol', val => (this.balanceSymbol = val)), - AccountController.subscribeKey('profileName', val => (this.profileName = val)), - AccountController.subscribeKey('profileImage', val => (this.profileImage = val)), - OptionsController.subscribeKey('features', val => (this.features = val)), - AccountController.subscribeKey('allAccounts', allAccounts => { - this.allAccounts = allAccounts - }), - ChainController.subscribeKey('activeChain', val => (this.namespace = val)), - ChainController.subscribeKey('activeCaipNetwork', val => { - if (val) { - const [namespace, chainId] = val?.caipNetworkId?.split(':') || [] - if (namespace && chainId) { - this.namespace = namespace as ChainNamespace - this.chainId = chainId - } - } - }) - ] - ) - } - - public override disconnectedCallback() { - this.unsubscribe.forEach(unsubscribe => unsubscribe()) - } - - // -- Render -------------------------------------------- // - public override render() { - if (!this.caipAddress) { - return null - } - - const shouldShowMultiAccount = - ChainController.state.activeChain !== ConstantsUtil.CHAIN.SOLANA && - this.allAccounts.length > 1 - - return html` - ${shouldShowMultiAccount ? this.multiAccountTemplate() : this.singleAccountTemplate()} - - - ${CoreHelperUtil.formatBalance(this.balance, this.balanceSymbol)} - - - ${this.explorerBtnTemplate()} - - - - ${this.authCardTemplate()} - ${this.orderedFeaturesTemplate()} ${this.activityTemplate()} - - Disconnect - - ` - } - - // -- Private ------------------------------------------- // - private onrampTemplate() { - if (!this.namespace) { - return null - } - - const onramp = this.features?.onramp - const hasNetworkSupport = CoreConstantsUtil.ONRAMP_SUPPORTED_CHAIN_NAMESPACES.includes( - this.namespace - ) - - if (!onramp || !hasNetworkSupport) { - return null - } - - return html` - - Buy crypto - - ` - } - - private orderedFeaturesTemplate() { - const featuresOrder = - this.features?.walletFeaturesOrder || CoreConstantsUtil.DEFAULT_FEATURES.walletFeaturesOrder - - return featuresOrder.map(feature => { - switch (feature) { - case 'onramp': - return this.onrampTemplate() - case 'swaps': - return this.swapsTemplate() - case 'send': - return this.sendTemplate() - default: - return null - } - }) - } - - private activityTemplate() { - if (!this.namespace) { - return null - } - - const isSolana = ChainController.state.activeChain === ConstantsUtil.CHAIN.SOLANA - const isEnabled = - this.features?.history && - CoreConstantsUtil.ACTIVITY_ENABLED_CHAIN_NAMESPACES.includes(this.namespace) - - return isEnabled - ? html` - - Activity - - ${isSolana ? html`Coming soon` : ''} - ` - : null - } - - private swapsTemplate() { - const swaps = this.features?.swaps - const isEvm = ChainController.state.activeChain === ConstantsUtil.CHAIN.EVM - - if (!swaps || !isEvm) { - return null - } - - return html` - - Swap - - ` - } - - private sendTemplate() { - const send = this.features?.send - const isEvm = ChainController.state.activeChain === ConstantsUtil.CHAIN.EVM - - if (!send || !isEvm) { - return null - } - - return html` - - Send - - ` - } - - private authCardTemplate() { - const namespace = ChainController.state.activeChain as ChainNamespace - const connectorId = StorageUtil.getConnectedConnectorId(namespace) - const authConnector = ConnectorController.getAuthConnector() - const { origin } = location - if ( - !authConnector || - connectorId !== ConstantsUtil.CONNECTOR_ID.AUTH || - origin.includes(CoreConstantsUtil.SECURE_SITE) - ) { - return null - } - - return html` - - ` - } - - private handleSwitchAccountsView() { - RouterController.push('SwitchAddress') - } - - private handleClickPay() { - RouterController.push('OnRampProviders') - } - - private handleClickSwap() { - RouterController.push('Swap') - } - - private handleClickSend() { - RouterController.push('WalletSend') - } - - private explorerBtnTemplate() { - const addressExplorerUrl = AccountController.state.addressExplorerUrl - - if (!addressExplorerUrl) { - return null - } - - return html` - - - Block Explorer - - - ` - } - - private singleAccountTemplate() { - return html` - - - - - ${this.profileName - ? UiHelperUtil.getTruncateString({ - string: this.profileName, - charsStart: 20, - charsEnd: 0, - truncate: 'end' - }) - : UiHelperUtil.getTruncateString({ - string: this.address || '', - charsStart: 4, - charsEnd: 4, - truncate: 'middle' - })} - - - ` - } - - private multiAccountTemplate() { - if (!this.address) { - throw new Error('w3m-account-view: No account provided') - } - - const account = this.allAccounts.find(acc => acc.address === this.address) - const label = AccountController.state.addressLabels.get(this.address) - if (this.namespace === 'bip122') { - return this.btcAccountsTemplate() - } - - return html` - - ` - } - - private btcAccountsTemplate() { - return html` - - - AccountController.setCaipAddress( - `bip122:${this.chainId}:${this.allAccounts[index]?.address || ''}`, - this.namespace - )} - > - - - ${UiHelperUtil.getTruncateString({ - string: this.profileName || this.address || '', - charsStart: this.profileName ? 18 : 4, - charsEnd: this.profileName ? 0 : 4, - truncate: this.profileName ? 'end' : 'middle' - })} - - - - ` - } - - private onCopyAddress() { - try { - if (this.address) { - CoreHelperUtil.copyToClopboard(this.address) - SnackController.showSuccess('Address copied') - } - } catch { - SnackController.showError('Failed to copy') - } - } - - private onTransactions() { - EventsController.sendEvent({ - type: 'track', - event: 'CLICK_TRANSACTIONS', - properties: { - isSmartAccount: - AccountController.state.preferredAccountType === - W3mFrameRpcConstants.ACCOUNT_TYPES.SMART_ACCOUNT - } - }) - RouterController.push('Transactions') - } - - private async onDisconnect() { - try { - this.disconnecting = true - await ConnectionController.disconnect() - EventsController.sendEvent({ type: 'track', event: 'DISCONNECT_SUCCESS' }) - ModalController.close() - } catch { - EventsController.sendEvent({ type: 'track', event: 'DISCONNECT_ERROR' }) - SnackController.showError('Failed to disconnect') - } finally { - this.disconnecting = false - } - } - - private onExplorer() { - const addressExplorerUrl = AccountController.state.addressExplorerUrl - - if (addressExplorerUrl) { - CoreHelperUtil.openHref(addressExplorerUrl, '_blank') - } - } - - private onGoToUpgradeView() { - EventsController.sendEvent({ type: 'track', event: 'EMAIL_UPGRADE_FROM_MODAL' }) - RouterController.push('UpgradeEmailWallet') - } -} - -declare global { - interface HTMLElementTagNameMap { - 'w3m-account-default-widget': W3mAccountDefaultWidget - } -} diff --git a/packages/scaffold-ui/src/partials/w3m-account-default-widget/styles.ts b/packages/scaffold-ui/src/partials/w3m-account-default-widget/styles.ts deleted file mode 100644 index 50df15cf54..0000000000 --- a/packages/scaffold-ui/src/partials/w3m-account-default-widget/styles.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { css } from 'lit' - -export default css` - wui-flex { - width: 100%; - } - - :host > wui-flex:first-child { - transform: translateY(calc(var(--wui-spacing-xxs) * -1)); - } - - wui-icon-link { - margin-right: calc(var(--wui-icon-box-size-md) * -1); - } - - wui-notice-card { - margin-bottom: var(--wui-spacing-3xs); - } - - wui-list-item > wui-text { - flex: 1; - } - - w3m-transactions-view { - max-height: 200px; - } - - .tab-content-container { - height: 300px; - overflow-y: auto; - overflow-x: hidden; - scrollbar-width: none; - } - - .tab-content-container::-webkit-scrollbar { - display: none; - } - - .account-button { - width: auto; - border: none; - display: flex; - align-items: center; - justify-content: center; - gap: var(--wui-spacing-s); - height: 48px; - padding: var(--wui-spacing-xs); - padding-right: var(--wui-spacing-s); - box-shadow: inset 0 0 0 1px var(--wui-color-gray-glass-002); - background-color: var(--wui-color-gray-glass-002); - border-radius: 24px; - transition: background-color 0.2s linear; - } - - .account-button:hover { - background-color: var(--wui-color-gray-glass-005); - } - - .avatar-container { - position: relative; - } - - wui-avatar.avatar { - width: 32px; - height: 32px; - box-shadow: 0 0 0 2px var(--wui-color-gray-glass-005); - } - - wui-avatar.network-avatar { - width: 16px; - height: 16px; - position: absolute; - left: 100%; - top: 100%; - transform: translate(-75%, -75%); - box-shadow: 0 0 0 2px var(--wui-color-gray-glass-005); - } - - .account-links { - display: flex; - justify-content: space-between; - align-items: center; - } - - .account-links wui-flex { - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - flex: 1; - background: red; - align-items: center; - justify-content: center; - height: 48px; - padding: 10px; - flex: 1 0 0; - border-radius: var(--XS, 16px); - border: 1px solid var(--dark-accent-glass-010, rgba(71, 161, 255, 0.1)); - background: var(--dark-accent-glass-010, rgba(71, 161, 255, 0.1)); - transition: - background-color var(--wui-ease-out-power-1) var(--wui-duration-md), - opacity var(--wui-ease-out-power-1) var(--wui-duration-md); - will-change: background-color, opacity; - } - - .account-links wui-flex:hover { - background: var(--dark-accent-glass-015, rgba(71, 161, 255, 0.15)); - } - - .account-links wui-flex wui-icon { - width: var(--S, 20px); - height: var(--S, 20px); - } - - .account-links wui-flex wui-icon svg path { - stroke: #667dff; - } -` diff --git a/packages/scaffold-ui/test/partials/w3m-default-account-widget.test.ts b/packages/scaffold-ui/test/partials/w3m-default-account-widget.test.ts deleted file mode 100644 index bc8d4ae296..0000000000 --- a/packages/scaffold-ui/test/partials/w3m-default-account-widget.test.ts +++ /dev/null @@ -1,301 +0,0 @@ -import { fixture } from '@open-wc/testing' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -import { html } from 'lit' - -import { ConstantsUtil } from '@reown/appkit-common' -import { - AccountController, - ChainController, - ConnectionController, - ConnectorController, - CoreHelperUtil, - EventsController, - ModalController, - OptionsController, - RouterController, - SnackController, - StorageUtil -} from '@reown/appkit-core' -import type { - AccountControllerState, - AuthConnector, - ChainControllerState -} from '@reown/appkit-core' - -import type { W3mAccountDefaultWidget } from '../../exports' -import { HelpersUtil } from '../utils/HelpersUtil' - -describe('W3mAccountDefaultWidget', () => { - const mockCaipAddress = 'eip155:1:0x123' - const mockAddress = '0x123' - const mockProfileName = 'Test Account' - const mockProfileImage = 'profile.jpg' - - beforeEach(() => { - // Mock AccountController state - vi.spyOn(AccountController, 'state', 'get').mockReturnValue({ - caipAddress: mockCaipAddress, - address: mockAddress, - profileName: mockProfileName, - profileImage: mockProfileImage, - balance: '100', - balanceSymbol: 'ETH', - allAccounts: [{ address: mockAddress, type: 'eoa' }], - addressExplorerUrl: 'https://etherscan.io', - addressLabels: new Map(), - preferredAccountType: 'eoa' - } as unknown as AccountControllerState) - - // Mock ChainController state - vi.spyOn(ChainController, 'state', 'get').mockReturnValue({ - activeChain: ConstantsUtil.CHAIN.EVM, - activeCaipNetwork: { - id: '1', - caipNetworkId: 'eip155:1' - } - } as unknown as ChainControllerState) - - // Mock OptionsController state - vi.spyOn(OptionsController, 'state', 'get').mockReturnValue({ - features: { - onramp: true, - swaps: true, - send: true, - walletFeaturesOrder: ['onramp', 'swaps', 'send'] - } - } as any) - - // Mock other controllers - vi.spyOn(StorageUtil, 'getConnectedConnectorId').mockReturnValue('test') - vi.spyOn(ConnectorController, 'getAuthConnector').mockReturnValue(undefined) - vi.spyOn(CoreHelperUtil, 'copyToClopboard').mockImplementation(vi.fn()) - vi.spyOn(CoreHelperUtil, 'openHref').mockImplementation(vi.fn()) - vi.spyOn(RouterController, 'push').mockImplementation(vi.fn()) - vi.spyOn(ModalController, 'close').mockImplementation(vi.fn()) - vi.spyOn(EventsController, 'sendEvent').mockImplementation(vi.fn()) - vi.spyOn(SnackController, 'showSuccess').mockImplementation(vi.fn()) - vi.spyOn(SnackController, 'showError').mockImplementation(vi.fn()) - }) - - afterEach(() => { - vi.clearAllMocks() - }) - - describe('Rendering', () => { - it('renders nothing when no caipAddress', async () => { - vi.spyOn(AccountController, 'state', 'get').mockReturnValue({ - caipAddress: null - } as unknown as AccountControllerState) - - const element: W3mAccountDefaultWidget = await fixture( - html`` - ) - // Should only have styles tag - expect(element.shadowRoot?.children.length).toBe(1) - }) - - it('renders single account view for Solana chain', async () => { - vi.spyOn(ChainController, 'state', 'get').mockReturnValue({ - activeChain: ConstantsUtil.CHAIN.SOLANA - } as unknown as ChainControllerState) - - const element: W3mAccountDefaultWidget = await fixture( - html`` - ) - expect(HelpersUtil.querySelect(element, '[data-testid="single-account-avatar"]')).toBeTruthy() - }) - - it('renders multi account view for EVM with multiple accounts', async () => { - vi.spyOn(AccountController, 'state', 'get').mockReturnValue({ - ...AccountController.state, - allAccounts: [ - { address: '0x123', type: 'eoa' }, - { address: '0x456', type: 'eoa' } - ] - } as AccountControllerState) - - const element: W3mAccountDefaultWidget = await fixture( - html`` - ) - expect(HelpersUtil.querySelect(element, 'wui-profile-button-v2')).toBeTruthy() - }) - - it('renders BTC accounts template for bip122 namespace with multiple accounts', async () => { - vi.spyOn(AccountController, 'state', 'get').mockReturnValue({ - ...AccountController.state, - allAccounts: [ - { address: '0x123', type: 'eoa' }, - { address: '0x456', type: 'eoa' } - ] - } as AccountControllerState) - - vi.spyOn(ChainController, 'state', 'get').mockReturnValue({ - activeChain: 'bip122', - activeCaipNetwork: { id: '1', caipNetworkId: 'bip122:1' } - } as unknown as ChainControllerState) - - const element: W3mAccountDefaultWidget = await fixture( - html`` - ) - expect(HelpersUtil.querySelect(element, 'wui-tabs')).toBeTruthy() - }) - }) - - describe('Features', () => { - it('shows onramp button when enabled for supported chain', async () => { - const element: W3mAccountDefaultWidget = await fixture( - html`` - ) - expect( - HelpersUtil.querySelect(element, '[data-testid="w3m-account-default-onramp-button"]') - ).toBeTruthy() - }) - - it('should not show onramp button when disabled', async () => { - vi.spyOn(OptionsController, 'state', 'get').mockReturnValue({ - features: { - onramp: false - } - } as any) - - const element: W3mAccountDefaultWidget = await fixture( - html`` - ) - expect(HelpersUtil.getByTestId(element, 'w3m-account-default-onramp-button')).toBeFalsy() - }) - - it('should not show onramp button for non-enabled chain', async () => { - vi.spyOn(ChainController, 'state', 'get').mockReturnValue({ - activeChain: ConstantsUtil.CHAIN.BITCOIN - } as unknown as ChainControllerState) - - const element: W3mAccountDefaultWidget = await fixture( - html`` - ) - expect(HelpersUtil.getByTestId(element, 'w3m-account-default-onramp-button')).toBeFalsy() - }) - - it('shows swap button for EVM chain', async () => { - const element: W3mAccountDefaultWidget = await fixture( - html`` - ) - const text = HelpersUtil.querySelectAll(element, 'wui-text') - expect([...text].some(t => t.textContent?.includes('Swap'))).toBeTruthy() - }) - - it('does not show swap button when disabled', async () => { - vi.spyOn(OptionsController, 'state', 'get').mockReturnValue({ - features: { - swaps: false - } - } as any) - - const element: W3mAccountDefaultWidget = await fixture( - html`` - ) - const texts = HelpersUtil.querySelectAll(element, 'wui-text') - expect([...texts].some(text => text?.textContent?.includes('Swap'))).toBeFalsy() - }) - - it('does not show swap button for non-EVM chain', async () => { - vi.spyOn(ChainController, 'state', 'get').mockReturnValue({ - activeChain: ConstantsUtil.CHAIN.SOLANA - } as unknown as ChainControllerState) - - const element: W3mAccountDefaultWidget = await fixture( - html`` - ) - const texts = HelpersUtil.querySelectAll(element, 'wui-text') - expect([...texts].some(text => text?.textContent?.includes('Swap'))).toBeFalsy() - }) - - it('shows auth card for AUTH connector', async () => { - vi.spyOn(StorageUtil, 'getConnectedConnectorId').mockReturnValue( - ConstantsUtil.CONNECTOR_ID.AUTH - ) - vi.spyOn(ConnectorController, 'getAuthConnector').mockReturnValue({ - id: 'auth', - provider: { - getEmail: () => 'email@email.com' - } - } as AuthConnector) - - const element: W3mAccountDefaultWidget = await fixture( - html`` - ) - expect( - HelpersUtil.querySelect(element, '[data-testid="w3m-wallet-upgrade-card"]') - ).toBeTruthy() - }) - }) - - describe('Interactions', () => { - it('copies address and shows success message', async () => { - const element: W3mAccountDefaultWidget = await fixture( - html`` - ) - const copyButton = HelpersUtil.querySelect(element, 'wui-icon-link') - await copyButton?.click() - - expect(CoreHelperUtil.copyToClopboard).toHaveBeenCalledWith(mockAddress) - expect(SnackController.showSuccess).toHaveBeenCalledWith('Address copied') - }) - - it('disconnects wallet successfully', async () => { - vi.spyOn(ConnectionController, 'disconnect').mockResolvedValue() - - const element: W3mAccountDefaultWidget = await fixture( - html`` - ) - const disconnectButton = HelpersUtil.querySelect(element, '[data-testid="disconnect-button"]') - await disconnectButton?.click() - - expect(ConnectionController.disconnect).toHaveBeenCalled() - expect(EventsController.sendEvent).toHaveBeenCalledWith({ - type: 'track', - event: 'DISCONNECT_SUCCESS' - }) - expect(ModalController.close).toHaveBeenCalled() - }) - - it('handles disconnect failure', async () => { - vi.spyOn(ConnectionController, 'disconnect').mockRejectedValue(new Error()) - - const element: W3mAccountDefaultWidget = await fixture( - html`` - ) - const disconnectButton = HelpersUtil.querySelect(element, '[data-testid="disconnect-button"]') - await disconnectButton?.click() - - expect(EventsController.sendEvent).toHaveBeenCalledWith({ - type: 'track', - event: 'DISCONNECT_ERROR' - }) - expect(SnackController.showError).toHaveBeenCalledWith('Failed to disconnect') - }) - - it('navigates to explorer', async () => { - const element: W3mAccountDefaultWidget = await fixture( - html`` - ) - const explorerButton = HelpersUtil.querySelect(element, 'wui-button') - await explorerButton?.click() - - expect(CoreHelperUtil.openHref).toHaveBeenCalledWith('https://etherscan.io', '_blank') - }) - }) - - describe('State Updates', () => { - it('cleans up subscriptions on disconnect', async () => { - const element: W3mAccountDefaultWidget = await fixture( - html`` - ) - const unsubscribeSpy = vi.fn() - ;(element as any).unsubscribe = [unsubscribeSpy] - - element.disconnectedCallback() - expect(unsubscribeSpy).toHaveBeenCalled() - }) - }) -})