diff --git a/sdk/apps/modal-example/package.json b/sdk/apps/modal-example/package.json index cf589391..0f5dbf66 100644 --- a/sdk/apps/modal-example/package.json +++ b/sdk/apps/modal-example/package.json @@ -22,7 +22,7 @@ "@nightlylabs/nightly-connect-sui": "0.0.29", "@nightlylabs/wallet-selector-sui": "0.2.7", "@nightlylabs/nightly-connect-polkadot": "0.0.14", - "@nightlylabs/wallet-selector-polkadot": "0.1.19", + "@nightlylabs/wallet-selector-polkadot": "0.2.0", "@polkadot/extension-inject": "^0.46.5", "@polkadot/api": "^10.10.1", "@solana/web3.js": "^1.77.2", diff --git a/sdk/apps/modal-example/src/routes/aleph.tsx b/sdk/apps/modal-example/src/routes/aleph.tsx index aa506d34..ed5ea9e6 100644 --- a/sdk/apps/modal-example/src/routes/aleph.tsx +++ b/sdk/apps/modal-example/src/routes/aleph.tsx @@ -22,13 +22,14 @@ export default function Polkadot() { icon: 'https://docs.nightly.app/img/logo.png', additionalInfo: 'Courtesy of Nightly Connect team' }, - network: 'AlephZero' + network: 'AlephZero', + persistent: true }, - true, // change this to false to test disabling eager connect - document.getElementById('modalAnchor') + { initOnConnect: false, disableModal: false, disableEagerConnect: false } ) - adapter.canEagerConnect().then((canEagerConnect) => { + adapter.canEagerConnect().then((canEagerConnect: boolean) => { + console.log('canEagerConnect', canEagerConnect) setEager(canEagerConnect) }) setAdapter(adapter) diff --git a/sdk/packages/modal/src/components/nightly-desktop-main/nightly-desktop-main.css b/sdk/packages/modal/src/components/nightly-desktop-main/nightly-desktop-main.css index b65a94b6..51959aef 100644 --- a/sdk/packages/modal/src/components/nightly-desktop-main/nightly-desktop-main.css +++ b/sdk/packages/modal/src/components/nightly-desktop-main/nightly-desktop-main.css @@ -95,6 +95,52 @@ animation: fade_out_loader 250ms ease-out forwards; } +.nc_desktopQrTimeoutErrorOverlay { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: var(--nc-color-elements-2); + z-index: 5; + visibility: hidden; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.nc_desktopQrTimeoutError { + width: 120px; + filter: grayscale(100%) opacity(60%); +} + +.nc_desktopQrTimeoutErrorLabel { + margin-top: 15px; + margin-bottom: 5px; + font-size: 16px; + font-weight: 600; + line-height: 22px; + letter-spacing: 0.02em; + text-align: center; + color: var(--nc-color-elements-7); +} + +.nc_desktopQrTimeoutErrorLabelDescription { + color: var(--nc-color-elements-6); + font-size: 12px; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.02em; + text-align: center; + max-width: 40%; +} + +.nc_desktopQrTimeoutErrorOverlayFadeIn { + visibility: visible; + animation: fade_in_error 250ms ease-out forwards; +} + @media (max-width: 1080px) { .nc_desktopMainWrapper { max-width: 636px; @@ -111,6 +157,10 @@ width: 304px; height: 304px; } + + .nc_desktopQrTimeoutErrorLabelDescription { + max-width: 60%; + } } @keyframes fade_out_loader { @@ -122,3 +172,12 @@ display: none; } } + +@keyframes fade_in_error { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} diff --git a/sdk/packages/modal/src/components/nightly-desktop-main/nightly-desktop-main.stories.ts b/sdk/packages/modal/src/components/nightly-desktop-main/nightly-desktop-main.stories.ts index bf2a2683..53f1a457 100644 --- a/sdk/packages/modal/src/components/nightly-desktop-main/nightly-desktop-main.stories.ts +++ b/sdk/packages/modal/src/components/nightly-desktop-main/nightly-desktop-main.stories.ts @@ -14,14 +14,12 @@ import NightlyIcon from '../../static/svg/NightlyIcon.svg' import ChainIcon from '../../static/svg/ChainIcon.svg' import { useArgs } from '@storybook/client-api' - const meta = { title: 'nightly-desktop-main', parameters: { layout: 'centered' }, render: (args) => { - return html`` - } } satisfies Meta @@ -100,17 +97,17 @@ export const Loading: Story = (args: NightlyModalArgs) => { if (!args.sessionId) setTimeout(() => { - updateArgs({ sessionId: "1234" }) + updateArgs({ sessionId: '1234' }) }, 2000) return html`` + .selectorItems=${args.selectorItems} + .onWalletClick=${args.onWalletClick} + .chainIcon=${args.chainIcon} + .chainName=${args.chainName} + .sessionId=${args.sessionId ?? sessionId} + .relay=${args.relay} + >` } Loading.args = { @@ -150,4 +147,62 @@ Loading.args = { chainIcon: ChainIcon, chainName: 'Solana', relay: 'https://nc2.nightly.app' -} \ No newline at end of file +} + +export const Error: Story = (args: NightlyModalArgs) => { + const [{ timeoutError }, updateArgs] = useArgs() + + if (!args.sessionId) + setTimeout(() => { + updateArgs({ timeoutError: true }) + }, 5000) + + return html`` +} + +Error.args = { + selectorItems: [ + { name: 'Phantom', icon: Phantom, status: 'recent' }, + { name: 'Nightly Wallet', icon: NightlyIcon, status: 'recent' }, + { name: 'MetaMask', icon: MetaMask, status: '' }, + { name: 'Glow', icon: Glow, status: '' }, + { name: 'ZenGO', icon: ZenGO, status: 'detected' }, + { name: 'Trust', icon: Trust, status: '' }, + { name: 'Binance', icon: Binance, status: '' }, + { name: 'Sollet', icon: Sollet, status: '' }, + { name: 'Phantom', icon: Phantom, status: '' }, + { name: 'MetaMask', icon: MetaMask, status: 'recent' }, + { name: 'Coinbase', icon: Coinbase, status: '' }, + { name: 'ZenGO', icon: ZenGO, status: '' }, + { name: 'Trust', icon: Trust, status: 'detected' }, + { name: 'Binance', icon: Binance, status: '' }, + { name: 'Phantom', icon: Phantom, status: 'recent' }, + { name: 'Nightly Wallet', icon: NightlyIcon, status: 'recent' }, + { name: 'MetaMask', icon: MetaMask, status: '' }, + { name: 'Glow', icon: Glow, status: '' }, + { name: 'ZenGO', icon: ZenGO, status: 'detected' }, + { name: 'Trust', icon: Trust, status: '' }, + { name: 'Binance', icon: Binance, status: '' }, + { name: 'Sollet', icon: Sollet, status: '' }, + { name: 'Phantom', icon: Phantom, status: '' }, + { name: 'MetaMask', icon: MetaMask, status: 'recent' }, + { name: 'Coinbase', icon: Coinbase, status: '' }, + { name: 'ZenGO', icon: ZenGO, status: '' }, + { name: 'Trust', icon: Trust, status: 'detected' }, + { name: 'Binance', icon: Binance, status: '' } + ], + onWalletClick: (name: string) => { + console.log('Item clicked:', name) + }, + chainIcon: ChainIcon, + chainName: 'Solana', + relay: 'https://nc2.nightly.app' +} diff --git a/sdk/packages/modal/src/components/nightly-desktop-main/nightly-desktop-main.ts b/sdk/packages/modal/src/components/nightly-desktop-main/nightly-desktop-main.ts index b53666fc..551c5e35 100644 --- a/sdk/packages/modal/src/components/nightly-desktop-main/nightly-desktop-main.ts +++ b/sdk/packages/modal/src/components/nightly-desktop-main/nightly-desktop-main.ts @@ -32,12 +32,18 @@ export class NightlyDesktopMain extends LitElement { @property({ type: Object }) qrConfigOverride: Partial = {} + @property({ type: Boolean }) + timeoutError = false + @state() copyMessage = 'Copy' @state() qrSource: string | undefined = undefined + @state() + isSessionIdImmediatelyDefined: boolean = false + timeoutRef: number | undefined = undefined onCopy = () => { @@ -80,6 +86,7 @@ export class NightlyDesktopMain extends LitElement { super.connectedCallback() this.updateQrSource() + if (this.sessionId) this.isSessionIdImmediatelyDefined = true } protected updated(_changedProperties: PropertyValueMap | Map): void { @@ -106,17 +113,33 @@ export class NightlyDesktopMain extends LitElement { -
Loading

Generating QR code...

+
`} + +
+ Timeout error +

QR code couldn’t be generated...

+

+ Make sure you have stable internet connection. +

= {} + @property({ type: Boolean }) + timeoutError = false + @state() qrSource: string | undefined = undefined + @state() + isSessionIdImmediatelyDefined: boolean = false + private updateQrSource = () => { if (this.sessionId) this.qrSource = svgToBase64( @@ -51,6 +57,7 @@ export class NightlyMobileQr extends LitElement { super.connectedCallback() this.updateQrSource() + if (this.sessionId) this.isSessionIdImmediatelyDefined = true } protected updated(_changedProperties: PropertyValueMap | Map): void { @@ -69,16 +76,33 @@ export class NightlyMobileQr extends LitElement { -
Loading

Generating QR code...

+
`} + +
+ + Timeout error +

QR code couldn’t be generated...

+

+ Make sure you have stable internet connection. +

` diff --git a/sdk/packages/modal/src/components/nightly-selector/nightly-selector.stories.ts b/sdk/packages/modal/src/components/nightly-selector/nightly-selector.stories.ts index 0907c131..5a5b2fdf 100644 --- a/sdk/packages/modal/src/components/nightly-selector/nightly-selector.stories.ts +++ b/sdk/packages/modal/src/components/nightly-selector/nightly-selector.stories.ts @@ -114,8 +114,7 @@ Default.args = { 'fsdhfdzfsdhgfzghggdfhbgchgbdfnvfbxhncvfjhzxdhgbhghfgfvzhfgjhgszdhgzxdfhgfzxdjfuhdfhgd', connecting: true, relay: 'https://nc2.nightly.app', - open: true, - + open: true } export const Loading: Story = (args: NightlyModalArgs) => { @@ -126,9 +125,10 @@ export const Loading: Story = (args: NightlyModalArgs) => { args.onClose() } - open && setTimeout(() => { - updateArgs({ sessionId: "1234" }) - }, 2000) + open && + setTimeout(() => { + updateArgs({ sessionId: '1234' }) + }, 2000) return open ? html` @@ -147,4 +147,35 @@ export const Loading: Story = (args: NightlyModalArgs) => { } let { sessionId: _, ...rest } = Default.args -Loading.args = { ...rest } \ No newline at end of file +Loading.args = { ...rest } + +export const Error: Story = (args: NightlyModalArgs) => { + const [{ open, timeoutError }, updateArgs] = useArgs() + + const handleClose = () => { + updateArgs({ open: false }) + args.onClose() + } + + if (!args.sessionId) + setTimeout(() => { + updateArgs({ timeoutError: true }) + }, 5000) + + return open + ? html` + + ` + : html`` +} + +Error.args = { ...rest } diff --git a/sdk/packages/modal/src/components/nightly-selector/nightly-selector.ts b/sdk/packages/modal/src/components/nightly-selector/nightly-selector.ts index 5c0f9b27..b69fdc42 100644 --- a/sdk/packages/modal/src/components/nightly-selector/nightly-selector.ts +++ b/sdk/packages/modal/src/components/nightly-selector/nightly-selector.ts @@ -47,6 +47,9 @@ export class NightlySelector extends LitElement { @property({ type: Object }) qrConfigOverride: Partial = {} + @property({ type: Boolean }) + timeoutError = false + // state @state() @@ -235,6 +238,7 @@ export class NightlySelector extends LitElement { .sessionId=${this.sessionId} .relay=${this.relay} .qrConfigOverride=${this.qrConfigOverride} + .timeoutError=${this.timeoutError} > ` } @@ -274,6 +278,7 @@ export class NightlySelector extends LitElement { .relay=${this.relay} .showAllWallets=${this.returnToMobileInit} .qrConfigOverride=${this.qrConfigOverride} + .timeoutError=${this.timeoutError} > ` } diff --git a/sdk/packages/selector-base/package.json b/sdk/packages/selector-base/package.json index 78473d37..034bfb63 100644 --- a/sdk/packages/selector-base/package.json +++ b/sdk/packages/selector-base/package.json @@ -1,6 +1,6 @@ { "name": "@nightlylabs/wallet-selector-base", - "version": "0.2.5", + "version": "0.3.0", "description": "", "type": "module", "exports": { diff --git a/sdk/packages/selector-base/src/modal.ts b/sdk/packages/selector-base/src/modal.ts index 36d3f3c3..636fce3a 100644 --- a/sdk/packages/selector-base/src/modal.ts +++ b/sdk/packages/selector-base/src/modal.ts @@ -45,6 +45,10 @@ export class NightlyConnectSelectorModal { } } + set sessionId(id: string) { + if (this._modal && id) this._modal.sessionId = id + } + createSelectorElement = ( variablesOverride?: object, stylesOverride?: string, @@ -67,10 +71,13 @@ export class NightlyConnectSelectorModal { } } - public openModal = (sessionId: string, onSelectListWallet: (name: string) => void) => { + public openModal = ( + sessionId: string | undefined, + onSelectListWallet: (name: string) => void + ) => { if (this._modal && this._open === false) { this._modal.onWalletClick = onSelectListWallet - this._modal.sessionId = sessionId + this._modal.sessionId = sessionId ?? '' this._anchor.appendChild(this._modal) this._open = true this.onOpen?.() diff --git a/sdk/packages/selector-base/src/persistence.ts b/sdk/packages/selector-base/src/persistence.ts index 4cbb047d..a563ac3c 100644 --- a/sdk/packages/selector-base/src/persistence.ts +++ b/sdk/packages/selector-base/src/persistence.ts @@ -1,8 +1,14 @@ import { getSessionIdLocalStorageKey } from '@nightlylabs/nightly-connect-base' import { ILocalStorage, getStorage } from 'isomorphic-localstorage' +import { ConnectionType } from './types' let _localStorage: ILocalStorage | null = null +type WalletInfo = { + walletName: string + walletType: ConnectionType +} + export const getLocalStorage = () => { if (_localStorage === null) { _localStorage = getStorage('./nightly-connect-session') @@ -12,53 +18,32 @@ export const getLocalStorage = () => { } // recent wallet from standard - -export const persistRecentStandardWalletForNetwork = (walletName: string, network: string) => { +export const NIGHTLY_CONNECT_RECENT_WALLET = 'NIGHTLY_CONNECT_RECENT_WALLET_' +export const persistRecentWalletForNetwork = (network: string, walletInfo: WalletInfo) => { const storage = getLocalStorage() - - storage.setItem('NIGHTLY_CONNECT_SELECTOR_RECENT_STANDARD_WALLET_' + network, walletName) + storage.setItem(NIGHTLY_CONNECT_RECENT_WALLET + network, JSON.stringify(walletInfo)) } -export const getRecentStandardWalletForNetwork = (network: string) => { +export const getRecentWalletForNetwork = (network: string) => { const storage = getLocalStorage() - - const item = storage.getItem('NIGHTLY_CONNECT_SELECTOR_RECENT_STANDARD_WALLET_' + network) - - return item + const item = storage.getItem(NIGHTLY_CONNECT_RECENT_WALLET + network) + if (!item) return null + try { + return JSON.parse(item) as WalletInfo + } catch (error) { + console.warn('Error parsing recent wallet from local storage', error) + return null + } } -export const clearRecentStandardWalletForNetwork = (network: string) => { +export const clearRecentWalletForNetwork = (network: string) => { const storage = getLocalStorage() - - storage.removeItem('NIGHTLY_CONNECT_SELECTOR_RECENT_STANDARD_WALLET_' + network) + storage.removeItem(NIGHTLY_CONNECT_RECENT_WALLET + network) } // clearing last nightly connect session id export const clearSessionIdForNetwork = (network: string) => { const storage = getLocalStorage() - storage.removeItem(getSessionIdLocalStorageKey(network)) } - -// info if any wallet from standard is connected - -export const persistStandardConnectForNetwork = (network: string) => { - const storage = getLocalStorage() - - storage.setItem('NIGHTLY_CONNECT_SELECTOR_IS_DESKTOP_CONNECTED_' + network, 'true') -} - -export const isStandardConnectedForNetwork = (network: string) => { - const storage = getLocalStorage() - - const item = storage.getItem('NIGHTLY_CONNECT_SELECTOR_IS_DESKTOP_CONNECTED_' + network) - - return item !== null -} - -export const persistStandardDisconnectForNetwork = (network: string) => { - const storage = getLocalStorage() - - storage.removeItem('NIGHTLY_CONNECT_SELECTOR_IS_DESKTOP_CONNECTED_' + network) -} diff --git a/sdk/packages/selector-base/src/types.ts b/sdk/packages/selector-base/src/types.ts index 67d731c7..0a14a253 100644 --- a/sdk/packages/selector-base/src/types.ts +++ b/sdk/packages/selector-base/src/types.ts @@ -32,3 +32,14 @@ export enum ConnectionType { Nightly = 'Nightly', WalletStandard = 'WalletStandard' } + +export interface ConnectionOptions { + disableModal?: boolean // default: false + initOnConnect?: boolean // default: false + disableEagerConnect?: boolean // default: false +} +export const defaultConnectionOptions: ConnectionOptions = { + disableModal: false, + initOnConnect: false, + disableEagerConnect: false +} diff --git a/sdk/packages/selector-polkadot/package.json b/sdk/packages/selector-polkadot/package.json index 96293bd0..030dba3f 100644 --- a/sdk/packages/selector-polkadot/package.json +++ b/sdk/packages/selector-polkadot/package.json @@ -1,6 +1,6 @@ { "name": "@nightlylabs/wallet-selector-polkadot", - "version": "0.1.19", + "version": "0.2.0", "description": "", "type": "module", "exports": { @@ -25,7 +25,7 @@ "license": "ISC", "dependencies": { "@nightlylabs/nightly-connect-polkadot": "^0.0.15", - "@nightlylabs/wallet-selector-base": "^0.2.5", + "@nightlylabs/wallet-selector-base": "^0.3.0", "@polkadot/extension-inject": "0.46.5", "@polkadot/api": "10.10.1", "@wallet-standard/core": "^1.0.3" diff --git a/sdk/packages/selector-polkadot/src/adapter.ts b/sdk/packages/selector-polkadot/src/adapter.ts index 7b734b81..72fdf76b 100644 --- a/sdk/packages/selector-polkadot/src/adapter.ts +++ b/sdk/packages/selector-polkadot/src/adapter.ts @@ -5,17 +5,17 @@ import { WalletMetadata } from '@nightlylabs/nightly-connect-polkadot' import { + ConnectionOptions, + ConnectionType, NightlyConnectSelectorModal, XMLOptions, - clearRecentStandardWalletForNetwork, + clearRecentWalletForNetwork, clearSessionIdForNetwork, - getRecentStandardWalletForNetwork, + defaultConnectionOptions, + getRecentWalletForNetwork, isMobileBrowser, - isStandardConnectedForNetwork, logoBase64, - persistRecentStandardWalletForNetwork, - persistStandardConnectForNetwork, - persistStandardDisconnectForNetwork, + persistRecentWalletForNetwork, sleep, triggerConnect } from '@nightlylabs/wallet-selector-base' @@ -34,14 +34,13 @@ export class NightlyConnectAdapter implements Injected { private _connecting: boolean private _connected: boolean + private _connectionOptions: ConnectionOptions = defaultConnectionOptions private _app: AppPolkadot | undefined - private _appSessionActive: boolean private _innerStandardAdapter: Injected | undefined private _modal: NightlyConnectSelectorModal | undefined private _appInitData: AppSelectorInitialize - private _useEagerConnect: boolean private _metadataWallets: WalletMetadata[] = [] private _walletsList: IPolkadotWalletListItem[] = [] @@ -50,20 +49,16 @@ export class NightlyConnectAdapter implements Injected { private _loading: boolean - private _initOnConnect: boolean - - constructor( - appInitData: AppSelectorInitialize, - useEagerConnect?: boolean, - initOnConnect = false - ) { + constructor(appInitData: AppSelectorInitialize, connectionOptions?: ConnectionOptions) { this._connecting = false this._connected = false this._appInitData = appInitData - this._useEagerConnect = !!useEagerConnect - this._appSessionActive = false this._loading = false - this._initOnConnect = initOnConnect + this._connectionOptions = { ...this._connectionOptions, ...connectionOptions } + // If not persistent, clear session id + if (!this._appInitData.persistent) { + clearSessionIdForNetwork(this._appInitData.network) + } } get accounts() { @@ -107,6 +102,9 @@ export class NightlyConnectAdapter implements Injected { get network() { return this._appInitData.network } + get walletsFromRegistry() { + return this._metadataWallets + } get walletsList() { return this._walletsList } @@ -141,7 +139,7 @@ export class NightlyConnectAdapter implements Injected { public static build = async ( appInitData: AppSelectorInitialize, - useEagerConnect?: boolean, + connectionOptions?: ConnectionOptions, anchorRef?: HTMLElement | null, uiOverrides?: { variablesOverride?: object @@ -149,26 +147,23 @@ export class NightlyConnectAdapter implements Injected { qrConfigOverride?: Partial } ) => { - if (!useEagerConnect) { - clearSessionIdForNetwork(appInitData.network) - } - - const adapter = new NightlyConnectAdapter(appInitData, useEagerConnect) + const adapter = new NightlyConnectAdapter(appInitData, connectionOptions) adapter.walletsList = getPolkadotWalletsList( [], - getRecentStandardWalletForNetwork(adapter.network) ?? undefined - ) - adapter._modal = new NightlyConnectSelectorModal( - adapter.walletsList, - appInitData.url ?? 'https://nc2.nightly.app', - networkToData(adapter.network), - anchorRef, - uiOverrides?.variablesOverride, - uiOverrides?.stylesOverride, - uiOverrides?.qrConfigOverride + getRecentWalletForNetwork(adapter.network)?.walletName ?? undefined ) - + if (!adapter._connectionOptions.disableModal) { + adapter._modal = new NightlyConnectSelectorModal( + adapter.walletsList, + appInitData.url ?? 'https://nc2.nightly.app', + networkToData(adapter.network), + anchorRef, + uiOverrides?.variablesOverride, + uiOverrides?.stylesOverride, + uiOverrides?.qrConfigOverride + ) + } const [app, metadataWallets] = await NightlyConnectAdapter.initApp(appInitData) adapter._app = app @@ -176,7 +171,7 @@ export class NightlyConnectAdapter implements Injected { adapter.walletsList = getPolkadotWalletsList( metadataWallets, - getRecentStandardWalletForNetwork(adapter.network) ?? undefined + getRecentWalletForNetwork(adapter.network)?.walletName ?? undefined ) return adapter @@ -184,7 +179,7 @@ export class NightlyConnectAdapter implements Injected { public static buildLazy = ( appInitData: AppSelectorInitialize, - useEagerConnect?: boolean, + connectionOptions?: ConnectionOptions, anchorRef?: HTMLElement | null, uiOverrides?: { variablesOverride?: object @@ -192,206 +187,183 @@ export class NightlyConnectAdapter implements Injected { qrConfigOverride?: Partial } ) => { - if (!useEagerConnect) { - clearSessionIdForNetwork(appInitData.network) - } - - const adapter = new NightlyConnectAdapter(appInitData, useEagerConnect) + const adapter = new NightlyConnectAdapter(appInitData, connectionOptions) adapter.walletsList = getPolkadotWalletsList( [], - getRecentStandardWalletForNetwork(adapter.network) ?? undefined + getRecentWalletForNetwork(adapter.network)?.walletName ?? undefined ) - adapter._modal = new NightlyConnectSelectorModal( - adapter.walletsList, - appInitData.url ?? 'https://nc2.nightly.app', - networkToData(adapter.network), - anchorRef, - uiOverrides?.variablesOverride, - uiOverrides?.stylesOverride, - uiOverrides?.qrConfigOverride - ) - - adapter._loading = true - - NightlyConnectAdapter.initApp(appInitData) - .then(([app, metadataWallets]) => { - adapter._app = app - adapter._metadataWallets = metadataWallets - adapter.walletsList = getPolkadotWalletsList( - metadataWallets, - getRecentStandardWalletForNetwork(adapter.network) ?? undefined - ) - - adapter._loading = false - }) - .catch(() => { - adapter._loading = false - throw new Error('Failed to initialize adapter') - }) + // Fetch wallets from registry + adapter.fetchWalletsFromRegistry().then((metadataWallets) => { + adapter._metadataWallets = metadataWallets + adapter.walletsList = getPolkadotWalletsList( + metadataWallets, + getRecentWalletForNetwork(adapter.network)?.walletName ?? undefined + ) + }) - return adapter - } - public static buildWithInitOnConnect = ( - appInitData: AppSelectorInitialize, - useEagerConnect?: boolean, - anchorRef?: HTMLElement | null, - uiOverrides?: { - variablesOverride?: object - stylesOverride?: string - qrConfigOverride?: Partial - } - ) => { - if (!useEagerConnect) { - clearSessionIdForNetwork(appInitData.network) + if (!adapter._connectionOptions.disableModal) { + adapter._modal = new NightlyConnectSelectorModal( + adapter.walletsList, + appInitData.url ?? 'https://nc2.nightly.app', + networkToData(adapter.network), + anchorRef, + uiOverrides?.variablesOverride, + uiOverrides?.stylesOverride, + uiOverrides?.qrConfigOverride + ) } - const adapter = new NightlyConnectAdapter(appInitData, useEagerConnect, true) - - adapter.walletsList = getPolkadotWalletsList( - [], - getRecentStandardWalletForNetwork(adapter.network) ?? undefined - ) - adapter._modal = new NightlyConnectSelectorModal( - adapter.walletsList, - appInitData.url ?? 'https://nc2.nightly.app', - networkToData(adapter.network), - anchorRef, - uiOverrides?.variablesOverride, - uiOverrides?.stylesOverride, - uiOverrides?.qrConfigOverride - ) - + // If init on connect is not enabled, we should initialize app + if (!adapter._connectionOptions.initOnConnect) { + adapter._loading = true + NightlyConnectAdapter.initApp(appInitData) + .then(([app, metadataWallets]) => { + adapter._app = app + adapter._metadataWallets = metadataWallets + adapter.walletsList = getPolkadotWalletsList( + metadataWallets, + getRecentWalletForNetwork(adapter.network)?.walletName ?? undefined + ) + + adapter._loading = false + }) + .catch(() => { + adapter._loading = false + throw new Error('Failed to initialize adapter') + }) + } return adapter } - // ensureLoaded = async () => {} + + // Checks if we can restore user session canEagerConnect = async () => { - if (!this._useEagerConnect) { + // If eager connect is disabled, we can't eager connect + if (this._connectionOptions.disableEagerConnect) { return false } + // Get recent wallet for network + const recentWallet = getRecentWalletForNetwork(this.network) + // If there is no recent wallet, we can't eager connect + if (recentWallet === null) return false - // utility for case if somebody wants to fire connect asap, but doesn't want to show modal if e. g. there was no user connected on the device yet - if (this._loading) { - for (let i = 0; i < 200; i++) { - await sleep(10) - - if (!this._loading) { - break - } - } - } - - if (this._loading) { - false - } - - if (this._app && this._app.hasBeenRestored() && this._app.accounts.activeAccounts.length > 0) { - return true - } - + // If we user wallet standard, we can eager connect if ( - getRecentStandardWalletForNetwork(this.network) !== null && - isStandardConnectedForNetwork(this.network) + recentWallet.walletName !== null && + recentWallet.walletType === ConnectionType.WalletStandard ) { return true } + // If we user nightly connect we need to make sure app is restored + if (recentWallet.walletType === ConnectionType.Nightly) { + if (this._connectionOptions.initOnConnect) { + return false + } + // Wait for app to be restored + if (this._loading) { + for (let i = 0; i < 2000; i++) { + await sleep(10) + if (!this._loading) { + break + } + } + } + // If app is restored and has active accounts, we can eager connect + if (this._loading) { + return false + } + if ( + this._app && + this._app.hasBeenRestored() && + this._app.accounts.activeAccounts.length > 0 + ) { + return true + } + } + return false } - eagerConnectDeeplink = () => { - if (isMobileBrowser() && this._app) { - const mobileWalletName = getRecentStandardWalletForNetwork(this.network) - const wallet = this.walletsList.find((w) => w.name === mobileWalletName) + connectToMobileWallet = (walletName: string) => { + try { + if (this._modal) { + this._modal.setStandardWalletConnectProgress(true) + } + + const wallet = this.walletsList.find((w) => w.name === walletName) + + if (!this._app) { + throw new Error('Wallet not ready') + } if (typeof wallet === 'undefined') { - return + throw new Error('Wallet not found') } if (wallet.deeplink === null) { - return + throw new Error('Deeplink not found') } + // If we have a native deeplink, we should use it if (wallet.deeplink.native !== null) { this._app.connectDeeplink({ walletName: wallet.name, url: wallet.deeplink.native }) + this._chosenMobileWalletName = walletName + + triggerConnect( + wallet.deeplink.native, + this._app.sessionId, + this._appInitData.url ?? 'https://nc2.nightly.app' + ) return } + // If we have a universal deeplink, we should use it if (wallet.deeplink.universal !== null) { this._app.connectDeeplink({ walletName: wallet.name, url: wallet.deeplink.universal }) - } - } - } - - connectToMobileWallet = (walletName: string) => { - if (this._modal) { - this._modal.setStandardWalletConnectProgress(true) - } - const wallet = this.walletsList.find((w) => w.name === walletName) + this._chosenMobileWalletName = walletName - if (!this._app || typeof wallet === 'undefined') { - return - } - - if (wallet.deeplink === null) { - return - } - - if (wallet.deeplink.native !== null) { - this._app.connectDeeplink({ - walletName: wallet.name, - url: wallet.deeplink.native - }) - - this._chosenMobileWalletName = walletName - - triggerConnect( - wallet.deeplink.native, - this._app.sessionId, - this._appInitData.url ?? 'https://nc2.nightly.app' - ) - - return - } - - if (wallet.deeplink.universal !== null) { - this._app.connectDeeplink({ - walletName: wallet.name, - url: wallet.deeplink.universal - }) - - this._chosenMobileWalletName = walletName - - triggerConnect( - wallet.deeplink.universal, - this._app.sessionId, - this._appInitData.url ?? 'https://nc2.nightly.app' - ) - return - } - - const redirectToAppBrowser = wallet.deeplink.redirectToAppBrowser - if (redirectToAppBrowser !== null && redirectToAppBrowser.indexOf('{{url}}') > -1) { - const url = redirectToAppBrowser.replace( - '{{url}}', - encodeURIComponent(window.location.toString()) - ) - - window.open(url, '_blank', 'noreferrer noopener') - - return + triggerConnect( + wallet.deeplink.universal, + this._app.sessionId, + this._appInitData.url ?? 'https://nc2.nightly.app' + ) + return + } + // Fallback to redirecting to app browser + // aka browser inside the app + if (!wallet.deeplink.redirectToAppBrowser) { + const redirectToAppBrowser = wallet.deeplink.redirectToAppBrowser + if (redirectToAppBrowser !== null && redirectToAppBrowser.indexOf('{{url}}') > -1) { + const url = redirectToAppBrowser.replace( + '{{url}}', + encodeURIComponent(window.location.toString()) + ) + + window.open(url, '_blank', 'noreferrer noopener') + + return + } + } + } catch (err) { + // clear recent wallet + clearRecentWalletForNetwork(this.network) + if (this._modal) { + this._modal.setStandardWalletConnectProgress(false) + } + throw err } } - - connectToStandardWallet = async (walletName: string, onSuccess: () => void) => { + // Generic connect to standard wallet + connectToStandardWallet = async (walletName: string) => { try { if (this._modal) { this._modal.setStandardWalletConnectProgress(true) @@ -410,9 +382,6 @@ export class NightlyConnectAdapter implements Injected { if ((await inject.accounts.get()).length <= 0) { throw new Error('No accounts found') } - - persistRecentStandardWalletForNetwork(walletName, this.network) - persistStandardConnectForNetwork(this.network) this._innerStandardAdapter = { ...inject, signer: { @@ -427,169 +396,189 @@ export class NightlyConnectAdapter implements Injected { : undefined } } + this._connected = true this._connecting = false + + persistRecentWalletForNetwork(this.network, { + walletName, + walletType: ConnectionType.WalletStandard + }) + this._modal?.closeModal() - onSuccess() - } catch { + } catch (err) { // clear recent wallet - persistStandardDisconnectForNetwork(this.network) + clearRecentWalletForNetwork(this.network) if (this._modal) { this._modal.setStandardWalletConnectProgress(false) } + throw err + } + } + connectToWallet = async (walletName: string) => { + if (isMobileBrowser() && !this.walletsList.find((w) => w.name === walletName)?.injectedWallet) { + return this.connectToMobileWallet(walletName) + } else { + return await this.connectToStandardWallet(walletName) } } - connect = async () => new Promise((resolve, reject) => { const innerConnect = async () => { try { - if (this.connected || this.connecting) { + if (this._connecting) { + reject("Can't connect while connecting") + return + } + if (this._connected) { resolve() return } - if (this._initOnConnect) { - this._connecting = true - - if (!this._app) { - try { - const [app, metadataWallets] = await NightlyConnectAdapter.initApp( - this._appInitData - ) - - this._app = app - this._metadataWallets = metadataWallets - - this.walletsList = getPolkadotWalletsList( - metadataWallets, - getRecentStandardWalletForNetwork(this.network) ?? undefined - ) - } catch (e) { - this._connecting = false - if (!this._app) { - throw new Error('Wallet not ready') - } - throw e - } + const recentWallet = getRecentWalletForNetwork(this.network) + if (!this._connectionOptions.disableEagerConnect && recentWallet !== null) { + // Eager connect standard if possible + if (recentWallet.walletType === ConnectionType.WalletStandard) { + await this.connectToStandardWallet(recentWallet.walletName) + resolve() + return } - } else { - if (this._loading) { - // we do it to ensure proper connect flow in case if adapter is lazily built, but e. g. polkadot wallets selector uses its own eager connect - for (let i = 0; i < 200; i++) { - await sleep(10) - - if (!this._loading) { - break + // Eager connect remote if possible + if (recentWallet.walletType === ConnectionType.Nightly) { + if (this._app?.hasBeenRestored() && this._app.accounts.activeAccounts.length > 0) { + // Try to eager connect if session is restored + try { + this._connected = true + this._connecting = false + resolve() + return + } catch (error) { + // If we fail because of whatever reason + // Reset session since it might be corrupted + const [app] = await NightlyConnectAdapter.initApp(this._appInitData) + this._app = app } } - - if (this._loading) { - throw new Error('Wallet not ready') - } - } - - if (!this._app) { - throw new Error('Wallet not ready') } - - this._connecting = true } - if (this._app.hasBeenRestored() && this._app.accounts.activeAccounts.length > 0) { - // Try to eager connect if session is restored - try { - this.eagerConnectDeeplink() - this._connected = true - this._connecting = false - this._appSessionActive = true - resolve() - return - } catch (error) { - // If we fail because of whatever reason - // Reset session since it might be corrupted - const [app] = await NightlyConnectAdapter.initApp(this._appInitData) - this._app = app - } + if (this._connectionOptions.disableModal) { + reject('Modal is disabled') + return } - - const recentName = getRecentStandardWalletForNetwork(this.network) - if ( - this._useEagerConnect && - recentName !== null && - isStandardConnectedForNetwork(this.network) - ) { - await this.connectToStandardWallet(recentName, resolve) - - if (this._connected) { - return - } + if (this._connectionOptions.initOnConnect) { + this._loading = true + NightlyConnectAdapter.initApp(this._appInitData) + .then(([app, metadataWallets]) => { + this._app = app + this._metadataWallets = metadataWallets + this.walletsList = getPolkadotWalletsList( + metadataWallets, + getRecentWalletForNetwork(this.network)?.walletName ?? undefined + ) + this._loading = false + }) + .catch(() => { + this._loading = false + throw new Error('Failed to initialize adapter') + }) } - this._app.on('userConnected', () => { - try { - if (this._chosenMobileWalletName) { - persistRecentStandardWalletForNetwork(this._chosenMobileWalletName, this.network) - } else { - clearRecentStandardWalletForNetwork(this.network) - } - if (!this._app || this._app.accounts.activeAccounts.length <= 0) { - this._connecting = false - // If user does not pass any accounts, we should disconnect - this.disconnect() - } - this._connected = true - this._connecting = false - this._appSessionActive = true - this._modal?.closeModal() - resolve() - } catch { - this.disconnect() - } - }) + // Interval that checks if app has connected + let loadingInterval: NodeJS.Timeout + // opening modal and waiting for sessionId if (this._modal) { + this._connecting = true this._modal.onClose = () => { + clearInterval(loadingInterval) if (this._connecting) { this._connecting = false const error = new Error('Connection cancelled') reject(error) } } - this._modal.openModal(this._app.sessionId, (walletName: string) => { + this._modal.openModal(this._app?.sessionId ?? undefined, async (walletName: string) => { + // If we are on mobile and wallet is not injected, we should connect to mobile wallet if ( isMobileBrowser() && !this.walletsList.find((w) => w.name === walletName)?.injectedWallet ) { this.connectToMobileWallet(walletName) } else { - this.connectToStandardWallet(walletName, resolve) + await this.connectToStandardWallet(walletName) + resolve() } }) + + // loop until app is connected or we timeout + let checks = 0 + loadingInterval = setInterval(async (): Promise => { + checks++ + if (this._app) { + // Clear interval if app is connected + clearInterval(loadingInterval) + if (this._modal) this._modal.sessionId = this._app.sessionId + this._app.on('userConnected', () => { + try { + persistRecentWalletForNetwork(this.network, { + walletName: this._chosenMobileWalletName || '', + walletType: ConnectionType.Nightly + }) + + if (!this._app || this._app.accounts.activeAccounts.length <= 0) { + this._connecting = false + this._connected = false + // If user does not pass any accounts, we should disconnect + this.disconnect() + return + } + this._connected = true + this._connecting = false + this._modal?.closeModal() + resolve() + } catch { + this.disconnect() + } + }) + return + } + + // timeout after 5 seconds + if (checks > 500) { + clearInterval(loadingInterval) + // reject(new Error('Connecting takes too long')) + // TODO we need to have a way to show error on modal + } + }, 10) } // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { this._connecting = false reject(error) + } finally { + this._connecting = false } } innerConnect() }) - + fetchWalletsFromRegistry = async () => { + return AppPolkadot.getWalletsMetadata( + `${this._appInitData.url ?? 'https://nc2.nightly.app'}/get_wallets_metadata` + ) + } disconnect = async () => { try { // Some apps might use disconnect to reset state / recreate session clearSessionIdForNetwork(this.network) - this._appSessionActive = false + clearRecentWalletForNetwork(this.network) + this._innerStandardAdapter = undefined this._app = await AppPolkadot.build(this._appInitData) - if (this._innerStandardAdapter) { - this._innerStandardAdapter = undefined - persistStandardDisconnectForNetwork(this.network) - } + // Update recent wallet this.walletsList = getPolkadotWalletsList( this._metadataWallets, - getRecentStandardWalletForNetwork(this.network) ?? undefined + getRecentWalletForNetwork(this.network)?.walletName ?? undefined ) if (this._modal) { this._modal.walletsList = this.walletsList diff --git a/sdk/packages/selector-polkadot/src/detection.ts b/sdk/packages/selector-polkadot/src/detection.ts index 45394ba5..829a04aa 100644 --- a/sdk/packages/selector-polkadot/src/detection.ts +++ b/sdk/packages/selector-polkadot/src/detection.ts @@ -22,11 +22,11 @@ declare global { export const getPolkadotWallets = (): PolkadotWalletInjected[] => { if (window && window.injectedWeb3) { return Object.entries(window.injectedWeb3).map(([key, value]) => ({ - ...value, - name: value.name ?? key, // value.name might be undefined - slug: key, - icon: value.icon ?? appToIcon[key] ?? 'https://registry.nightly.app/networks/polkadot.png' // TODO add default icon - })) + ...value, + name: value.name ?? key, // value.name might be undefined + slug: key, + icon: value.icon ?? appToIcon[key] ?? 'https://registry.nightly.app/networks/polkadot.png' // TODO add default icon + })) } else { return [] } @@ -52,14 +52,10 @@ export const getPolkadotWalletsList = (presetList: WalletMetadata[], recentWalle recent: recentWalletName === wallet.name } }) - console.log('windowWallets', windowWallets) - console.log('walletsData', walletsData) for (const wallet of windowWallets) { // Check if wallet is already in the list // by namespace if (walletsData[wallet.slug.toLocaleLowerCase()]) { - console.log('a', walletsData[wallet.slug.toLocaleLowerCase()]) - console.log(wallet) walletsData[wallet.slug.toLocaleLowerCase()] = { ...(walletsData?.[wallet.slug.toLocaleLowerCase()] ?? { name: wallet.name, @@ -78,8 +74,6 @@ export const getPolkadotWalletsList = (presetList: WalletMetadata[], recentWalle // Check if wallet is already in the list // by name if (walletsData[wallet.name.toLocaleLowerCase()]) { - console.log('b', walletsData[wallet.name.toLocaleLowerCase()]) - console.log(wallet) walletsData[wallet.name.toLocaleLowerCase()] = { ...(walletsData?.[wallet.name.toLocaleLowerCase()] ?? { name: wallet.name, @@ -94,8 +88,6 @@ export const getPolkadotWalletsList = (presetList: WalletMetadata[], recentWalle } continue } - console.log({ wallet }) - walletsData[wallet.name.toLocaleLowerCase()] = { slug: wallet.name, name: wallet.name, diff --git a/sdk/pnpm-lock.yaml b/sdk/pnpm-lock.yaml index 41dde4ba..55b2d5b7 100644 --- a/sdk/pnpm-lock.yaml +++ b/sdk/pnpm-lock.yaml @@ -141,7 +141,7 @@ importers: specifier: 0.0.29 version: link:../sui '@nightlylabs/wallet-selector-polkadot': - specifier: 0.1.19 + specifier: 0.2.0 version: link:../../packages/selector-polkadot '@nightlylabs/wallet-selector-solana': specifier: 0.2.7 @@ -632,7 +632,7 @@ importers: specifier: ^0.0.15 version: link:../../apps/polkadot '@nightlylabs/wallet-selector-base': - specifier: ^0.2.5 + specifier: ^0.3.0 version: link:../selector-base '@polkadot/api': specifier: 10.10.1 @@ -676,7 +676,7 @@ importers: version: link:../../apps/solana '@nightlylabs/wallet-selector-base': specifier: ^0.2.4 - version: link:../selector-base + version: 0.2.4 '@solana/wallet-adapter-base': specifier: ^0.9.22 version: 0.9.22(@solana/web3.js@1.77.2)