From e522f82f24ba9384a6147ac1db601ac4446224a5 Mon Sep 17 00:00:00 2001 From: Enes Date: Fri, 9 Aug 2024 16:48:46 +0300 Subject: [PATCH] refactor: improvements to siwe flow and modal animations (#2672) --- .../core/src/controllers/ModalController.ts | 14 ++- .../scaffold-ui/src/modal/w3m-modal/index.ts | 38 +++++--- .../scaffold-ui/src/modal/w3m-modal/styles.ts | 84 ++++++++++++----- .../scaffold-ui/src/modal/w3m-router/index.ts | 50 +++++------ .../src/modal/w3m-router/styles.ts | 66 +++++++++++++- .../src/partials/w3m-header/index.ts | 89 ++++++++++++------- .../src/partials/w3m-header/styles.ts | 63 +++++++++++++ .../scaffold-ui/src/utils/ConstantsUtil.ts | 11 ++- .../w3m-approve-transaction-view/index.ts | 31 +++---- .../views/w3m-connecting-siwe-view/index.ts | 5 ++ packages/ui/src/utils/ThemeUtil.ts | 48 ++++++++++ packages/wallet/src/W3mFrame.ts | 2 +- 12 files changed, 383 insertions(+), 118 deletions(-) diff --git a/packages/core/src/controllers/ModalController.ts b/packages/core/src/controllers/ModalController.ts index 6cb02f7c77..16920577cb 100644 --- a/packages/core/src/controllers/ModalController.ts +++ b/packages/core/src/controllers/ModalController.ts @@ -11,6 +11,7 @@ import { RouterController } from './RouterController.js' export interface ModalControllerState { loading: boolean open: boolean + shake: boolean } export interface ModalControllerArguments { @@ -24,7 +25,8 @@ type StateKey = keyof ModalControllerState // -- State --------------------------------------------- // const state = proxy({ loading: false, - open: false + open: false, + shake: false }) // -- Controller ---------------------------------------- // @@ -72,5 +74,15 @@ export const ModalController = { setLoading(loading: ModalControllerState['loading']) { state.loading = loading PublicStateController.set({ loading }) + }, + + shake() { + if (state.shake) { + return + } + state.shake = true + setTimeout(() => { + state.shake = false + }, 500) } } diff --git a/packages/scaffold-ui/src/modal/w3m-modal/index.ts b/packages/scaffold-ui/src/modal/w3m-modal/index.ts index f85fed77c5..c335904486 100644 --- a/packages/scaffold-ui/src/modal/w3m-modal/index.ts +++ b/packages/scaffold-ui/src/modal/w3m-modal/index.ts @@ -1,7 +1,6 @@ import { AccountController, ApiController, - ConnectionController, CoreHelperUtil, EventsController, ModalController, @@ -39,19 +38,24 @@ export class W3mModal extends LitElement { @state() private loading = ModalController.state.loading + @state() private shake = ModalController.state.shake + public constructor() { super() this.initializeTheming() ApiController.prefetch() this.unsubscribe.push( - ModalController.subscribeKey('open', val => (val ? this.onOpen() : this.onClose())), - ModalController.subscribeKey('loading', val => { - this.loading = val - this.onNewAddress(AccountController.state.caipAddress) - }), - AccountController.subscribeKey('isConnected', val => (this.connected = val)), - AccountController.subscribeKey('caipAddress', val => this.onNewAddress(val)), - OptionsController.subscribeKey('isSiweEnabled', val => (this.isSiweEnabled = val)) + ...[ + ModalController.subscribeKey('open', val => (val ? this.onOpen() : this.onClose())), + ModalController.subscribeKey('shake', val => (this.shake = val)), + ModalController.subscribeKey('loading', val => { + this.loading = val + this.onNewAddress(AccountController.state.caipAddress) + }), + AccountController.subscribeKey('isConnected', val => (this.connected = val)), + AccountController.subscribeKey('caipAddress', val => this.onNewAddress(val)), + OptionsController.subscribeKey('isSiweEnabled', val => (this.isSiweEnabled = val)) + ] ) EventsController.sendEvent({ type: 'track', event: 'MODAL_LOADED' }) } @@ -66,7 +70,7 @@ export class W3mModal extends LitElement { return this.open ? html` - + @@ -85,14 +89,20 @@ export class W3mModal extends LitElement { } private async handleClose() { + const isSiweSignScreen = RouterController.state.view === 'ConnectingSiwe' + const isApproveSignScreen = RouterController.state.view === 'ApproveTransaction' + if (this.isSiweEnabled) { const { SIWEController } = await import('@web3modal/siwe') - - if (SIWEController.state.status !== 'success' && this.connected) { - await ConnectionController.disconnect() + const isUnauthenticated = SIWEController.state.status !== 'success' + if (isUnauthenticated && (isSiweSignScreen || isApproveSignScreen)) { + ModalController.shake() + } else { + ModalController.close() } + } else { + ModalController.close() } - ModalController.close() } private initializeTheming() { diff --git a/packages/scaffold-ui/src/modal/w3m-modal/styles.ts b/packages/scaffold-ui/src/modal/w3m-modal/styles.ts index e3b34e50b2..9964cdeca4 100644 --- a/packages/scaffold-ui/src/modal/w3m-modal/styles.ts +++ b/packages/scaffold-ui/src/modal/w3m-modal/styles.ts @@ -22,35 +22,21 @@ export default css` opacity: 1; } - @keyframes zoom-in { - 0% { - transform: scale(0.95) translateY(0); - } - 100% { - transform: scale(1) translateY(0); - } - } - - @keyframes slide-in { - 0% { - transform: scale(1) translateY(50px); - } - 100% { - transform: scale(1) translateY(0); - } - } - wui-card { max-width: var(--w3m-modal-width); width: 100%; position: relative; - animation-duration: 0.2s; - animation-name: zoom-in; + animation: zoom-in 0.2s var(--wui-ease-out-power-2); animation-fill-mode: backwards; - animation-timing-function: var(--wui-ease-out-power-2); outline: none; } + wui-card[shake='true'] { + animation: + zoom-in 0.2s var(--wui-ease-out-power-2), + w3m-shake 0.5s var(--wui-ease-out-power-2); + } + wui-flex { overflow-x: hidden; overflow-y: auto; @@ -81,7 +67,61 @@ export default css` border-bottom-left-radius: 0; border-bottom-right-radius: 0; border-bottom: none; - animation-name: slide-in; + animation: slide-in 0.2s var(--wui-ease-out-power-2); + } + + wui-card[shake='true'] { + animation: + slide-in 0.2s var(--wui-ease-out-power-2), + w3m-shake 0.5s var(--wui-ease-out-power-2); + } + } + + @keyframes zoom-in { + 0% { + transform: scale(0.95) translateY(0); + } + 100% { + transform: scale(1) translateY(0); + } + } + + @keyframes slide-in { + 0% { + transform: scale(1) translateY(50px); + } + 100% { + transform: scale(1) translateY(0); + } + } + + @keyframes w3m-shake { + 0% { + transform: scale(1) rotate(0deg); + } + 20% { + transform: scale(1) rotate(-1deg); + } + 40% { + transform: scale(1) rotate(1.5deg); + } + 60% { + transform: scale(1) rotate(-1.5deg); + } + 80% { + transform: scale(1) rotate(1deg); + } + 100% { + transform: scale(1) rotate(0deg); + } + } + + @keyframes w3m-view-height { + from { + height: var(--prev-height); + } + to { + height: var(--new-height); } } ` diff --git a/packages/scaffold-ui/src/modal/w3m-router/index.ts b/packages/scaffold-ui/src/modal/w3m-router/index.ts index af9c6e9864..05348f11c3 100644 --- a/packages/scaffold-ui/src/modal/w3m-router/index.ts +++ b/packages/scaffold-ui/src/modal/w3m-router/index.ts @@ -4,6 +4,7 @@ import { customElement } from '@web3modal/ui' import { LitElement, html } from 'lit' import { state } from 'lit/decorators.js' import styles from './styles.js' +import { ConstantsUtil } from '../../utils/ConstantsUtil.js' @customElement('w3m-router') export class W3mRouter extends LitElement { @@ -21,23 +22,26 @@ export class W3mRouter extends LitElement { // -- State & Properties -------------------------------- // @state() private view = RouterController.state.view + @state() private viewDirection = '' + public constructor() { super() this.unsubscribe.push(RouterController.subscribeKey('view', val => this.onViewChange(val))) } public override firstUpdated() { - this.resizeObserver = new ResizeObserver(async ([content]) => { + this.resizeObserver = new ResizeObserver(([content]) => { const height = `${content?.contentRect.height}px` if (this.prevHeight !== '0px') { - await this.animate([{ height: this.prevHeight }, { height }], { - duration: 150, - easing: 'ease', - fill: 'forwards' - }).finished + this.style.setProperty('--prev-height', this.prevHeight) + this.style.setProperty('--new-height', height) + this.style.animation = 'w3m-view-height 150ms forwards ease' this.style.height = 'auto' } - this.prevHeight = height + setTimeout(() => { + this.prevHeight = height + this.style.animation = 'unset' + }, ConstantsUtil.ANIMATION_DURATIONS.ModalHeight) }) this.resizeObserver.observe(this.getWrapper()) } @@ -49,7 +53,9 @@ export class W3mRouter extends LitElement { // -- Render -------------------------------------------- // public override render() { - return html`
${this.viewTemplate()}
` + return html`
+ ${this.viewTemplate()} +
` } // -- Private ------------------------------------------- // @@ -154,33 +160,21 @@ export class W3mRouter extends LitElement { } } - private async onViewChange(newView: RouterControllerState['view']) { + private onViewChange(newView: RouterControllerState['view']) { TooltipController.hide() + let direction = ConstantsUtil.VIEW_DIRECTION.Next const { history } = RouterController.state - let xOut = -10 - let xIn = 10 if (history.length < this.prevHistoryLength) { - xOut = 10 - xIn = -10 + direction = ConstantsUtil.VIEW_DIRECTION.Prev } this.prevHistoryLength = history.length - await this.animate( - [ - { opacity: 1, transform: 'translateX(0px)' }, - { opacity: 0, transform: `translateX(${xOut}px)` } - ], - { duration: 150, easing: 'ease', fill: 'forwards' } - ).finished - this.view = newView - await this.animate( - [ - { opacity: 0, transform: `translateX(${xIn}px)` }, - { opacity: 1, transform: 'translateX(0px)' } - ], - { duration: 150, easing: 'ease', fill: 'forwards', delay: 50 } - ).finished + this.viewDirection = direction + + setTimeout(() => { + this.view = newView + }, ConstantsUtil.ANIMATION_DURATIONS.ViewTransition) } private getWrapper() { diff --git a/packages/scaffold-ui/src/modal/w3m-router/styles.ts b/packages/scaffold-ui/src/modal/w3m-router/styles.ts index 101ac74d0f..0c76faf25b 100644 --- a/packages/scaffold-ui/src/modal/w3m-router/styles.ts +++ b/packages/scaffold-ui/src/modal/w3m-router/styles.ts @@ -2,7 +2,71 @@ import { css } from 'lit' export default css` :host { + --prev-height: 0px; + --new-height: 0px; display: block; - will-change: transform, opacity; + } + + div.w3m-router-container { + transform: translateY(0); + opacity: 1; + } + + div.w3m-router-container[view-direction='prev'] { + animation: + slide-left-out 150ms forwards ease, + slide-left-in 150ms forwards ease; + animation-delay: 0ms, 200ms; + } + + div.w3m-router-container[view-direction='next'] { + animation: + slide-right-out 150ms forwards ease, + slide-right-in 150ms forwards ease; + animation-delay: 0ms, 200ms; + } + + @keyframes slide-left-out { + from { + transform: translateX(0px); + opacity: 1; + } + to { + transform: translateX(10px); + opacity: 0; + } + } + + @keyframes slide-left-in { + from { + transform: translateX(-10px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + + @keyframes slide-right-out { + from { + transform: translateX(0px); + opacity: 1; + } + to { + transform: translateX(-10px); + opacity: 0; + } + } + + @keyframes slide-right-in { + from { + transform: translateX(10px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } } ` diff --git a/packages/scaffold-ui/src/partials/w3m-header/index.ts b/packages/scaffold-ui/src/partials/w3m-header/index.ts index 6be834ba38..29ac306f35 100644 --- a/packages/scaffold-ui/src/partials/w3m-header/index.ts +++ b/packages/scaffold-ui/src/partials/w3m-header/index.ts @@ -1,4 +1,3 @@ -import type { RouterControllerState } from '@web3modal/core' import { AccountController, ConnectionController, @@ -12,6 +11,7 @@ import { customElement } from '@web3modal/ui' import { LitElement, html } from 'lit' import { state } from 'lit/decorators.js' import styles from './styles.js' +import { ConstantsUtil } from '../../utils/ConstantsUtil.js' // -- Constants ----------------------------------------- // const BETA_SCREENS = ['Swap', 'SwapSelectToken', 'SwapPreview'] @@ -95,11 +95,25 @@ export class W3mHeader extends LitElement { @state() private showBack = false + @state() private isSiweEnabled = OptionsController.state.isSiweEnabled + + @state() private prevHistoryLength = 1 + + @state() private view = RouterController.state.view + + @state() private viewDirection = '' + + @state() private headerText = headings()[RouterController.state.view] + public constructor() { super() this.unsubscribe.push( RouterController.subscribeKey('view', val => { - this.onViewChange(val) + setTimeout(() => { + this.view = val + this.headerText = headings()[val] + }, ConstantsUtil.ANIMATION_DURATIONS.HeaderText) + this.onViewChange() this.onHistoryChange() }), ConnectionController.subscribeKey('buffering', val => (this.buffering = val)) @@ -114,13 +128,7 @@ export class W3mHeader extends LitElement { public override render() { return html` - ${this.dynamicButtonTemplate()} ${this.titleTemplate()} - + ${this.dynamicButtonTemplate()} ${this.titleTemplate()} ${this.closeButtonTemplate()} ` } @@ -134,21 +142,46 @@ export class W3mHeader extends LitElement { } private async onClose() { - if (OptionsController.state.isSiweEnabled) { + if (this.isSiweEnabled) { const { SIWEController } = await import('@web3modal/siwe') - if (SIWEController.state.status !== 'success') { - await ConnectionController.disconnect() + if (SIWEController.state.status === 'success') { + ModalController.close() + } else { + RouterController.popTransactionStack(true) } + } else { + ModalController.close() } - ModalController.close() + } + + private closeButtonTemplate() { + const isSiweSignScreen = RouterController.state.view === 'ConnectingSiwe' + + if (this.isSiweEnabled && isSiweSignScreen) { + return html`
` + } + + return html` + + ` } private titleTemplate() { - const isBeta = BETA_SCREENS.includes(RouterController.state.view) + const isBeta = BETA_SCREENS.includes(this.view) return html` - - ${this.heading} + + ${this.headerText} ${isBeta ? html`Beta` : null} ` @@ -185,26 +218,18 @@ export class W3mHeader extends LitElement { return ['l', '2l', 'l', '2l'] as const } - return ['l', '2l', '0', '2l'] as const + return ['0', '2l', '0', '2l'] as const } - private async onViewChange(view: RouterControllerState['view']) { - const headingEl = this.shadowRoot?.querySelector('wui-flex.w3m-header-title') + private onViewChange() { + const { history } = RouterController.state - if (headingEl) { - const preset = headings()[view] - await headingEl.animate([{ opacity: 1 }, { opacity: 0 }], { - duration: 200, - fill: 'forwards', - easing: 'ease' - }).finished - this.heading = preset - headingEl.animate([{ opacity: 0 }, { opacity: 1 }], { - duration: 200, - fill: 'forwards', - easing: 'ease' - }) + let direction = ConstantsUtil.VIEW_DIRECTION.Next + if (history.length < this.prevHistoryLength) { + direction = ConstantsUtil.VIEW_DIRECTION.Prev } + this.prevHistoryLength = history.length + this.viewDirection = direction } private async onHistoryChange() { diff --git a/packages/scaffold-ui/src/partials/w3m-header/styles.ts b/packages/scaffold-ui/src/partials/w3m-header/styles.ts index f95c496dec..466e437ee7 100644 --- a/packages/scaffold-ui/src/partials/w3m-header/styles.ts +++ b/packages/scaffold-ui/src/partials/w3m-header/styles.ts @@ -9,8 +9,71 @@ export default css` text-transform: capitalize; } + wui-flex.w3m-header-title { + transform: translateY(0); + opacity: 1; + } + + wui-flex.w3m-header-title[view-direction='prev'] { + animation: + slide-down-out 120ms forwards var(--wui-ease-out-power-2), + slide-down-in 120ms forwards var(--wui-ease-out-power-2); + animation-delay: 0ms, 200ms; + } + + wui-flex.w3m-header-title[view-direction='next'] { + animation: + slide-up-out 120ms forwards var(--wui-ease-out-power-2), + slide-up-in 120ms forwards var(--wui-ease-out-power-2); + animation-delay: 0ms, 200ms; + } + wui-icon-link[data-hidden='true'] { opacity: 0 !important; pointer-events: none; } + + @keyframes slide-up-out { + from { + transform: translateY(0px); + opacity: 1; + } + to { + transform: translateY(3px); + opacity: 0; + } + } + + @keyframes slide-up-in { + from { + transform: translateY(-3px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } + } + + @keyframes slide-down-out { + from { + transform: translateY(0px); + opacity: 1; + } + to { + transform: translateY(-3px); + opacity: 0; + } + } + + @keyframes slide-down-in { + from { + transform: translateY(3px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } + } ` diff --git a/packages/scaffold-ui/src/utils/ConstantsUtil.ts b/packages/scaffold-ui/src/utils/ConstantsUtil.ts index be015325c5..7e6ba337ba 100644 --- a/packages/scaffold-ui/src/utils/ConstantsUtil.ts +++ b/packages/scaffold-ui/src/utils/ConstantsUtil.ts @@ -1,5 +1,14 @@ export const ConstantsUtil = { ACCOUNT_TABS: [{ label: 'Tokens' }, { label: 'NFTs' }, { label: 'Activity' }], SECURE_SITE_ORIGIN: - process.env['NEXT_PUBLIC_SECURE_SITE_ORIGIN'] || 'https://secure.walletconnect.org' + process.env['NEXT_PUBLIC_SECURE_SITE_ORIGIN'] || 'https://secure.walletconnect.org', + VIEW_DIRECTION: { + Next: 'next', + Prev: 'prev' + }, + ANIMATION_DURATIONS: { + HeaderText: 120, + ModalHeight: 150, + ViewTransition: 150 + } } diff --git a/packages/scaffold-ui/src/views/w3m-approve-transaction-view/index.ts b/packages/scaffold-ui/src/views/w3m-approve-transaction-view/index.ts index 60a6899d8d..538781d074 100644 --- a/packages/scaffold-ui/src/views/w3m-approve-transaction-view/index.ts +++ b/packages/scaffold-ui/src/views/w3m-approve-transaction-view/index.ts @@ -31,7 +31,6 @@ export class W3mApproveTransactionView extends LitElement { public constructor() { super() - this.unsubscribe.push( ...[ ModalController.subscribeKey('open', isOpen => { @@ -39,6 +38,13 @@ export class W3mApproveTransactionView extends LitElement { this.onHideIframe() RouterController.popTransactionStack() } + }), + ModalController.subscribeKey('shake', val => { + if (val) { + this.iframe.style.animation = `w3m-shake 500ms var(--wui-ease-out-power-2)` + } else { + this.iframe.style.animation = 'none' + } }) ] ) @@ -71,38 +77,27 @@ export class W3mApproveTransactionView extends LitElement { this.iframe.style.bottom = 'unset' } this.ready = true + this.onShowIframe() }) this.bodyObserver.observe(window.document.body) } // -- Render -------------------------------------------- // public override render() { - if (this.ready) { - this.onShowIframe() - } - return html`
` } // -- Private ------------------------------------------- // private onShowIframe() { const isMobile = window.innerWidth <= 430 - this.iframe.animate( - [ - { opacity: 0, transform: isMobile ? 'translateY(50px)' : 'scale(.95)' }, - { opacity: 1, transform: isMobile ? 'translateY(0)' : 'scale(1)' } - ], - { duration: 200, easing: 'ease', fill: 'forwards' } - ) + this.iframe.style.animation = isMobile + ? 'w3m-iframe-zoom-in-mobile 200ms var(--wui-ease-out-power-2)' + : 'w3m-iframe-zoom-in 200ms var(--wui-ease-out-power-2)' } - private async onHideIframe() { + private onHideIframe() { this.iframe.style.display = 'none' - await this.iframe.animate([{ opacity: 1 }, { opacity: 0 }], { - duration: 200, - easing: 'ease', - fill: 'forwards' - }).finished + this.iframe.style.animation = 'w3m-iframe-fade-out 200ms var(--wui-ease-out-power-2)' } private async syncTheme() { diff --git a/packages/siwe/scaffold/views/w3m-connecting-siwe-view/index.ts b/packages/siwe/scaffold/views/w3m-connecting-siwe-view/index.ts index ce8f225cfb..5d2fe97fc2 100644 --- a/packages/siwe/scaffold/views/w3m-connecting-siwe-view/index.ts +++ b/packages/siwe/scaffold/views/w3m-connecting-siwe-view/index.ts @@ -21,6 +21,8 @@ export class W3mConnectingSiweView extends LitElement { @state() private isSigning = false + @state() private isCancelling = false + // -- Render -------------------------------------------- // public override render() { this.onRender() @@ -54,6 +56,7 @@ export class W3mConnectingSiweView extends LitElement { borderRadius="xs" fullWidth variant="neutral" + ?loading=${this.isCancelling} @click=${this.onCancel.bind(this)} data-testid="w3m-connecting-siwe-cancel" > @@ -135,6 +138,7 @@ export class W3mConnectingSiweView extends LitElement { } private async onCancel() { + this.isCancelling = true const isConnected = AccountController.state.isConnected if (isConnected) { await ConnectionController.disconnect() @@ -142,6 +146,7 @@ export class W3mConnectingSiweView extends LitElement { } else { RouterController.push('Connect') } + this.isCancelling = false EventsController.sendEvent({ event: 'CLICK_CANCEL_SIWE', type: 'track', diff --git a/packages/ui/src/utils/ThemeUtil.ts b/packages/ui/src/utils/ThemeUtil.ts index 76b81a46da..c0f9fdb619 100644 --- a/packages/ui/src/utils/ThemeUtil.ts +++ b/packages/ui/src/utils/ThemeUtil.ts @@ -44,6 +44,54 @@ function createRootStyles(themeVariables?: ThemeVariables) { return { core: css` @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); + @keyframes w3m-shake { + 0% { + transform: scale(1) rotate(0deg); + } + 20% { + transform: scale(1) rotate(-1deg); + } + 40% { + transform: scale(1) rotate(1.5deg); + } + 60% { + transform: scale(1) rotate(-1.5deg); + } + 80% { + transform: scale(1) rotate(1deg); + } + 100% { + transform: scale(1) rotate(0deg); + } + } + @keyframes w3m-iframe-fade-out { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } + } + @keyframes w3m-iframe-zoom-in { + 0% { + transform: translateY(50px); + opacity: 0; + } + 100% { + transform: translateY(0px); + opacity: 1; + } + } + @keyframes w3m-iframe-zoom-in-mobile { + 0% { + transform: scale(0.95); + opacity: 0; + } + 100% { + transform: scale(1); + opacity: 1; + } + } :root { --w3m-modal-width: 360px; --w3m-color-mix-strength: ${unsafeCSS( diff --git a/packages/wallet/src/W3mFrame.ts b/packages/wallet/src/W3mFrame.ts index d08a4dbead..0d724e6eaf 100644 --- a/packages/wallet/src/W3mFrame.ts +++ b/packages/wallet/src/W3mFrame.ts @@ -39,7 +39,7 @@ export class W3mFrame { iframe.style.position = 'fixed' iframe.style.zIndex = '999999' iframe.style.display = 'none' - iframe.style.opacity = '0' + iframe.style.animationDelay = '0s, 50ms' iframe.style.borderBottomLeftRadius = `clamp(0px, var(--wui-border-radius-l), 44px)` iframe.style.borderBottomRightRadius = `clamp(0px, var(--wui-border-radius-l), 44px)` document.body.appendChild(iframe)