From 26a9ff9cb55d7c9f96c2c600da91606247fb4389 Mon Sep 17 00:00:00 2001 From: Enes Date: Fri, 6 Dec 2024 19:57:16 +0300 Subject: [PATCH] refactor: connect method and wallet features for customization and sorting features (#3400) --- .changeset/old-laws-brake.md | 21 + packages/appkit/src/client.ts | 42 +- .../core/src/controllers/ModalController.ts | 15 +- .../core/src/controllers/OptionsController.ts | 57 ++- packages/core/src/utils/ConstantsUtil.ts | 21 +- packages/core/src/utils/TypeUtil.ts | 46 +- .../scaffold-ui/src/modal/w3m-modal/index.ts | 41 +- .../scaffold-ui/src/modal/w3m-modal/styles.ts | 12 + .../w3m-account-default-widget/index.ts | 47 +- .../index.ts | 117 +++-- .../partials/w3m-email-login-widget/index.ts | 58 +-- .../partials/w3m-social-login-list/index.ts | 16 +- .../partials/w3m-social-login-widget/index.ts | 55 ++- .../w3m-social-login-widget/styles.ts | 3 - .../src/partials/w3m-wallet-guide/index.ts | 2 +- .../src/views/w3m-connect-view/index.ts | 195 ++++++-- .../src/views/w3m-connect-view/styles.ts | 5 + .../test/views/w3m-connect-view.test.ts | 182 +++++-- pnpm-lock.yaml | 447 +++++++++++++++--- 19 files changed, 1082 insertions(+), 300 deletions(-) create mode 100644 .changeset/old-laws-brake.md diff --git a/.changeset/old-laws-brake.md b/.changeset/old-laws-brake.md new file mode 100644 index 0000000000..e502f71d81 --- /dev/null +++ b/.changeset/old-laws-brake.md @@ -0,0 +1,21 @@ +--- +'@reown/appkit-scaffold-ui': patch +'@reown/appkit': patch +'@reown/appkit-core': patch +'@reown/appkit-adapter-ethers': patch +'@reown/appkit-adapter-ethers5': patch +'@reown/appkit-adapter-solana': patch +'@reown/appkit-adapter-wagmi': patch +'@reown/appkit-utils': patch +'@reown/appkit-cdn': patch +'appkit-cli': patch +'@reown/appkit-common': patch +'@reown/appkit-experimental': patch +'@reown/appkit-polyfills': patch +'@reown/appkit-siwe': patch +'@reown/appkit-siwx': patch +'@reown/appkit-ui': patch +'@reown/appkit-wallet': patch +--- + +Refactors some ui rendering logics and enables setting extra configurations via modal functions diff --git a/packages/appkit/src/client.ts b/packages/appkit/src/client.ts index 6169224d99..4f65bda36e 100644 --- a/packages/appkit/src/client.ts +++ b/packages/appkit/src/client.ts @@ -20,8 +20,13 @@ import { type EstimateGasTransactionArgs, type AccountControllerState, type AdapterNetworkState, + type Features, SIWXUtil, - type ConnectionStatus + type ConnectionStatus, + type OptionsControllerState, + type WalletFeature, + type ConnectMethod, + type SocialProvider } from '@reown/appkit-core' import { AccountController, @@ -294,6 +299,14 @@ export class AppKit { setColorTheme(ThemeController.state.themeMode) } + public setTermsConditionsUrl(termsConditionsUrl: string) { + OptionsController.setTermsConditionsUrl(termsConditionsUrl) + } + + public setPrivacyPolicyUrl(privacyPolicyUrl: string) { + OptionsController.setPrivacyPolicyUrl(privacyPolicyUrl) + } + public setThemeVariables(themeVariables: ThemeControllerState['themeVariables']) { ThemeController.setThemeVariables(themeVariables) setThemeVariables(ThemeController.state.themeVariables) @@ -597,6 +610,32 @@ export class AppKit { } } + public updateFeatures(newFeatures: Partial) { + OptionsController.setFeatures(newFeatures) + } + + public updateOptions(newOptions: Partial) { + const currentOptions = OptionsController.state || {} + const updatedOptions = { ...currentOptions, ...newOptions } + OptionsController.setOptions(updatedOptions) + } + + public setConnectMethodsOrder(connectMethodsOrder: ConnectMethod[]) { + OptionsController.setConnectMethodsOrder(connectMethodsOrder) + } + + public setWalletFeaturesOrder(walletFeaturesOrder: WalletFeature[]) { + OptionsController.setWalletFeaturesOrder(walletFeaturesOrder) + } + + public setCollapseWallets(collapseWallets: boolean) { + OptionsController.setCollapseWallets(collapseWallets) + } + + public setSocialsOrder(socialsOrder: SocialProvider[]) { + OptionsController.setSocialsOrder(socialsOrder) + } + public async disconnect() { await this.connectionControllerClient?.disconnect() } @@ -612,6 +651,7 @@ export class AppKit { OptionsController.setDebug(options.debug) OptionsController.setProjectId(options.projectId) OptionsController.setSdkVersion(options.sdkVersion) + OptionsController.setEnableEmbedded(options.enableEmbedded) if (!options.projectId) { AlertController.open(ErrorUtil.ALERT_ERRORS.PROJECT_ID_NOT_CONFIGURED, 'error') diff --git a/packages/core/src/controllers/ModalController.ts b/packages/core/src/controllers/ModalController.ts index a29cf732b1..d5cc3f0e2d 100644 --- a/packages/core/src/controllers/ModalController.ts +++ b/packages/core/src/controllers/ModalController.ts @@ -7,6 +7,7 @@ import type { RouterControllerState } from './RouterController.js' import { RouterController } from './RouterController.js' import { ChainController } from './ChainController.js' import { CoreHelperUtil } from '../utils/CoreHelperUtil.js' +import { OptionsController } from './OptionsController.js' // -- Types --------------------------------------------- // export interface ModalControllerState { @@ -67,9 +68,21 @@ export const ModalController = { }, close() { + const isEmbeddedEnabled = OptionsController.state.enableEmbedded const connected = Boolean(ChainController.state.activeCaipAddress) + state.open = false - PublicStateController.set({ open: false }) + + if (isEmbeddedEnabled) { + if (connected) { + RouterController.replace('Account') + } else { + RouterController.push('Connect') + } + } else { + PublicStateController.set({ open: false }) + } + EventsController.sendEvent({ type: 'track', event: 'MODAL_CLOSE', diff --git a/packages/core/src/controllers/OptionsController.ts b/packages/core/src/controllers/OptionsController.ts index 21a98becea..c5acfbd71a 100644 --- a/packages/core/src/controllers/OptionsController.ts +++ b/packages/core/src/controllers/OptionsController.ts @@ -1,12 +1,15 @@ import { subscribeKey as subKey } from 'valtio/vanilla/utils' import { proxy } from 'valtio/vanilla' import type { + ConnectMethod, CustomWallet, Features, Metadata, ProjectId, SdkVersion, - Tokens + SocialProvider, + Tokens, + WalletFeature } from '../utils/TypeUtil.js' import { ConstantsUtil } from '../utils/ConstantsUtil.js' import type { SIWXConfig } from '../utils/SIWXUtil.js' @@ -117,6 +120,11 @@ export interface OptionsControllerStatePublic { * @default undefined */ siwx?: SIWXConfig + /** + * Renders the AppKit to DOM instead of the default modal. + * @default false + */ + enableEmbedded?: boolean } export interface OptionsControllerStateInternal { @@ -155,15 +163,14 @@ export const OptionsController = { return } - Object.entries(features).forEach(([key, value]) => { - if (!state.features) { - state.features = ConstantsUtil.DEFAULT_FEATURES - } + if (!state.features) { + state.features = ConstantsUtil.DEFAULT_FEATURES + + return + } - if (key in state.features) { - ;(state.features as Record)[key as keyof Features] = value - } - }) + const newFeatures = { ...state.features, ...features } + state.features = newFeatures }, setProjectId(projectId: OptionsControllerState['projectId']) { @@ -244,5 +251,37 @@ export const OptionsController = { setSIWX(siwx: OptionsControllerState['siwx']) { state.siwx = siwx + }, + + setConnectMethodsOrder(connectMethodsOrder: ConnectMethod[]) { + state.features = { + ...state.features, + connectMethodsOrder + } + }, + + setWalletFeaturesOrder(walletFeaturesOrder: WalletFeature[]) { + state.features = { + ...state.features, + walletFeaturesOrder + } + }, + + setSocialsOrder(socialsOrder: SocialProvider[]) { + state.features = { + ...state.features, + socials: socialsOrder + } + }, + + setCollapseWallets(collapseWallets: boolean) { + state.features = { + ...state.features, + collapseWallets + } + }, + + setEnableEmbedded(enableEmbedded: OptionsControllerState['enableEmbedded']) { + state.enableEmbedded = enableEmbedded } } diff --git a/packages/core/src/utils/ConstantsUtil.ts b/packages/core/src/utils/ConstantsUtil.ts index a63d86790c..e5d094eafe 100644 --- a/packages/core/src/utils/ConstantsUtil.ts +++ b/packages/core/src/utils/ConstantsUtil.ts @@ -1,4 +1,4 @@ -import type { Features } from './TypeUtil.js' +import type { Features, SocialProvider } from './TypeUtil.js' import type { ChainNamespace } from '@reown/appkit-common' const SECURE_SITE = 'https://secure.walletconnect.org' @@ -217,13 +217,26 @@ export const ConstantsUtil = { DEFAULT_FEATURES: { swaps: true, onramp: true, + receive: true, + send: true, email: true, emailShowWallets: true, - socials: ['google', 'x', 'discord', 'farcaster', 'github', 'apple', 'facebook'], + socials: [ + 'google', + 'x', + 'discord', + 'farcaster', + 'github', + 'apple', + 'facebook' + ] as SocialProvider[], history: true, analytics: true, allWallets: true, legalCheckbox: false, - smartSessions: false - } as Features + smartSessions: false, + collapseWallets: false, + connectMethodsOrder: ['email', 'social', 'wallet'], + walletFeaturesOrder: ['onramp', 'swaps', 'receive', 'send'] + } satisfies Features } diff --git a/packages/core/src/utils/TypeUtil.ts b/packages/core/src/utils/TypeUtil.ts index 97676d74a0..992c9bd33d 100644 --- a/packages/core/src/utils/TypeUtil.ts +++ b/packages/core/src/utils/TypeUtil.ts @@ -982,14 +982,9 @@ export type CombinedProvider = W3mFrameProvider & Provider export type CoinbasePaySDKChainNameValues = keyof typeof ConstantsUtil.WC_COINBASE_PAY_SDK_CHAIN_NAME_MAP -export type FeaturesSocials = - | 'google' - | 'x' - | 'discord' - | 'farcaster' - | 'github' - | 'apple' - | 'facebook' +export type WalletFeature = 'swaps' | 'send' | 'receive' | 'onramp' + +export type ConnectMethod = 'email' | 'social' | 'wallet' export type Features = { /** @@ -1002,6 +997,17 @@ export type Features = { * @type {boolean} */ onramp?: boolean + /** + * @description Enable or disable the receive feature. Enabled by default. + * This feature is only visible when connected with email/social. It's not possible to configure when connected with wallet, which is enabled by default. + * @type {boolean} + */ + receive?: boolean + /** + * @description Enable or disable the send feature. Enabled by default. + * @type {boolean} + */ + send?: boolean /** * @description Enable or disable the email feature. Enabled by default. * @type {boolean} @@ -1009,14 +1015,15 @@ export type Features = { email?: boolean /** * @description Show or hide the regular wallet options when email is enabled. Enabled by default. + * @deprecated - This property will be removed in the next major release. Please use `features.collapseWallets` instead. * @type {boolean} */ emailShowWallets?: boolean /** * @description Enable or disable the socials feature. Enabled by default. - * @type {FeaturesSocials[]} + * @type {SocialProvider[]} */ - socials?: FeaturesSocials[] | false + socials?: SocialProvider[] | false /** * @description Enable or disable the history feature. Enabled by default. * @type {boolean} @@ -1042,6 +1049,25 @@ export type Features = { * @default false */ legalCheckbox?: boolean + /** + * @description The order of the connect methods. This is experimental and subject to change. + * @default ['email', 'social', 'wallet'] + * @type {('email' | 'social' | 'wallet')[]} + */ + connectMethodsOrder?: ConnectMethod[] + /** + * @ + * @description The order of the wallet features. This is experimental and subject to change. + * @default ['receive' | 'onramp' | 'swaps' | 'send'] + * @type {('receive' | 'onramp' | 'swaps' | 'send')[]} + */ + walletFeaturesOrder?: WalletFeature[] + /** + * @description Enable or disable the collapse wallets as a single "Continue with wallet" button for simple UI in connect page. + * This can be activated when only have another connect method like email or social activated along with wallets. + * @default false + */ + collapseWallets?: boolean } export type FeaturesKeys = keyof Features diff --git a/packages/scaffold-ui/src/modal/w3m-modal/index.ts b/packages/scaffold-ui/src/modal/w3m-modal/index.ts index 127a3bc682..858a5196ad 100644 --- a/packages/scaffold-ui/src/modal/w3m-modal/index.ts +++ b/packages/scaffold-ui/src/modal/w3m-modal/index.ts @@ -4,6 +4,7 @@ import { CoreHelperUtil, EventsController, ModalController, + OptionsController, RouterController, SIWXUtil, SnackController, @@ -11,7 +12,7 @@ import { } from '@reown/appkit-core' import { UiHelperUtil, customElement, initializeTheming } from '@reown/appkit-ui' import { LitElement, html } from 'lit' -import { state } from 'lit/decorators.js' +import { property, state } from 'lit/decorators.js' import styles from './styles.js' import { type CaipAddress, type CaipNetwork } from '@reown/appkit-common' @@ -28,6 +29,8 @@ export class W3mModal extends LitElement { private abortController?: AbortController = undefined // -- State & Properties -------------------------------- // + @property() private enableEmbedded = OptionsController.state.enableEmbedded + @state() private open = ModalController.state.open @state() private caipAddress = ChainController.state.activeCaipAddress @@ -45,7 +48,8 @@ export class W3mModal extends LitElement { ModalController.subscribeKey('open', val => (val ? this.onOpen() : this.onClose())), ModalController.subscribeKey('shake', val => (this.shake = val)), ChainController.subscribeKey('activeCaipNetwork', val => this.onNewNetwork(val)), - ChainController.subscribeKey('activeCaipAddress', val => this.onNewAddress(val)) + ChainController.subscribeKey('activeCaipAddress', val => this.onNewAddress(val)), + OptionsController.subscribeKey('enableEmbedded', val => (this.enableEmbedded = val)) ] ) EventsController.sendEvent({ type: 'track', event: 'MODAL_LOADED' }) @@ -58,21 +62,15 @@ export class W3mModal extends LitElement { // -- Render -------------------------------------------- // public override render() { + if (this.enableEmbedded) { + return html`${this.contentTemplate()} + ` + } + return this.open ? html` - - - - - - + ${this.contentTemplate()} ` @@ -80,6 +78,21 @@ export class W3mModal extends LitElement { } // -- Private ------------------------------------------- // + private contentTemplate() { + return html` + + + + + ` + } + private async onOverlayClick(event: PointerEvent) { if (event.target === event.currentTarget) { await this.handleClose() diff --git a/packages/scaffold-ui/src/modal/w3m-modal/styles.ts b/packages/scaffold-ui/src/modal/w3m-modal/styles.ts index 9964cdeca4..9b9f04ef2c 100644 --- a/packages/scaffold-ui/src/modal/w3m-modal/styles.ts +++ b/packages/scaffold-ui/src/modal/w3m-modal/styles.ts @@ -22,6 +22,14 @@ export default css` opacity: 1; } + :host(.embedded) { + position: relative; + pointer-events: unset; + background: none; + width: 100%; + opacity: 1; + } + wui-card { max-width: var(--w3m-modal-width); width: 100%; @@ -31,6 +39,10 @@ export default css` outline: none; } + :host(.embedded) wui-card { + max-width: 400px; + } + wui-card[shake='true'] { animation: zoom-in 0.2s var(--wui-ease-out-power-2), 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 index c88d8e8423..fa063de786 100644 --- a/packages/scaffold-ui/src/partials/w3m-account-default-widget/index.ts +++ b/packages/scaffold-ui/src/partials/w3m-account-default-widget/index.ts @@ -11,7 +11,8 @@ import { ConstantsUtil as CommonConstantsUtil, OptionsController, ChainController, - type AccountType + type AccountType, + ConstantsUtil as CoreConstantsUtil } from '@reown/appkit-core' import { customElement, UiHelperUtil } from '@reown/appkit-ui' import { LitElement, html } from 'lit' @@ -113,7 +114,7 @@ export class W3mAccountDefaultWidget extends LitElement { ${this.authCardTemplate()} - ${this.onrampTemplate()} ${this.swapsTemplate()} ${this.activityTemplate()} + ${this.orderedFeaturesTemplate()} ${this.activityTemplate()} { + switch (feature) { + case 'onramp': + return this.onrampTemplate() + case 'swaps': + return this.swapsTemplate() + case 'send': + return this.sendTemplate() + default: + return null + } + }) + } + private activityTemplate() { const isSolana = ChainController.state.activeChain === ConstantsUtil.CHAIN.SOLANA @@ -186,6 +205,26 @@ export class W3mAccountDefaultWidget extends LitElement { ` } + 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 type = StorageUtil.getConnectedConnector() const authConnector = ConnectorController.getAuthConnector() @@ -217,6 +256,10 @@ export class W3mAccountDefaultWidget extends LitElement { RouterController.push('Swap') } + private handleClickSend() { + RouterController.push('WalletSend') + } + private explorerBtnTemplate() { const addressExplorerUrl = AccountController.state.addressExplorerUrl 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 index 076e806a3a..304428ef55 100644 --- 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 @@ -15,6 +15,7 @@ import { state } from 'lit/decorators.js' import { ifDefined } from 'lit/directives/if-defined.js' import styles from './styles.js' import { ConstantsUtil } from '../../utils/ConstantsUtil.js' +import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common' import { W3mFrameRpcConstants } from '@reown/appkit-wallet' const TABS = 3 @@ -98,35 +99,11 @@ export class W3mAccountWalletFeaturesWidget extends LitElement { networkSrc=${ifDefined(networkImage)} icon="chevronBottom" avatarSrc=${ifDefined(this.profileImage ? this.profileImage : undefined)} - profileName=${this.profileName} + profileName=${ifDefined(this.profileName ?? undefined)} data-testid="w3m-profile-button" > - ${this.tokenBalanceTemplate()} - - - - - ${this.swapsTemplate()} - - - - - - - - + + ${this.tokenBalanceTemplate()} ${this.orderedWalletFeatures()} !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) { + if (!swaps || !isEvm) { return null } @@ -160,6 +183,44 @@ export class W3mAccountWalletFeaturesWidget extends LitElement { ` } + 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(), 10000) } diff --git a/packages/scaffold-ui/src/partials/w3m-email-login-widget/index.ts b/packages/scaffold-ui/src/partials/w3m-email-login-widget/index.ts index 1b792ef979..18c2dc9a8e 100644 --- a/packages/scaffold-ui/src/partials/w3m-email-login-widget/index.ts +++ b/packages/scaffold-ui/src/partials/w3m-email-login-widget/index.ts @@ -1,10 +1,4 @@ -import { - ChainController, - ConnectorController, - CoreHelperUtil, - OptionsController, - type WalletGuideType -} from '@reown/appkit-core' +import { ChainController, ConnectorController, CoreHelperUtil } from '@reown/appkit-core' import { customElement } from '@reown/appkit-ui' import { LitElement, html } from 'lit' import { property, state } from 'lit/decorators.js' @@ -27,28 +21,12 @@ export class W3mEmailLoginWidget extends LitElement { // -- State & Properties -------------------------------- // @property() public tabIdx?: number - @state() private connectors = ConnectorController.state.connectors - - @state() private authConnector = this.connectors.find(c => c.type === 'AUTH') - @state() private email = '' @state() private loading = false @state() private error = '' - @property() private walletGuide: WalletGuideType = 'get-started' - - public constructor() { - super() - this.unsubscribe.push( - ConnectorController.subscribeKey('connectors', val => { - this.connectors = val - this.authConnector = val.find(c => c.type === 'AUTH') - }) - ) - } - public override disconnectedCallback() { this.unsubscribe.forEach(unsubscribe => unsubscribe()) } @@ -63,12 +41,6 @@ export class W3mEmailLoginWidget extends LitElement { // -- Render -------------------------------------------- // public override render() { - const email = OptionsController.state.features?.email - - if (!this.authConnector || !email) { - return null - } - return html`