import { createStore } from 'zustand/vanilla' import AuthcClient from '../AuthcClient' import type { AuthcClientOptions, LogoutOptions, RedirectLoginOptions } from '..' import type { AuthcWalletOptions as CustomWalletOptions } from '../wallet' import { AuthcWalletLogin } from '../wallet' interface AuthcWalletOptions extends CustomWalletOptions { } const debouncePromise = (fn: (...args: any[]) => Promise, delay: number) => { let timer: any return (...args: any[]) => { clearTimeout(timer) return new Promise((resolve, reject) => { timer = setTimeout(() => { fn(...args).then(resolve).catch(reject) }, delay) }) } } export interface AuthcOptions extends AuthcClientOptions { /** * unlock wallet on demand * @default false */ unlockOnDemand?: boolean walletOptions: AuthcWalletOptions events?: { afterLogout?: () => void } } export class Authc extends AuthcClient { wallet: AuthcWalletLogin private loginPromise: Promise private opts: AuthcOptions store = createStore<{ status: 'login' | 'loginFailed' | 'createWallet' | 'reconstruct' | 'viewAccount' | 'reconstructFailed' | '' loading: boolean }>(() => ({ status: '', loading: false, })) get events() { return { afterLogout: () => {}, ...this.opts.events, } } constructor(_options: AuthcOptions) { const options = { unlockOnDemand: false, ..._options, } super(options) this.wallet = new AuthcWalletLogin({ ...this.options.walletOptions, unlockOnDemand: options.unlockOnDemand, authc: this, }) this.wallet.addMessageListener('lifecycle', ({ name }) => { if (name === 'reconstruct') { if (this.opts.unlockOnDemand) this.wallet.replace('/forward') else this.wallet.replace('/reconstruct') } this.store.setState({ status: name, loading: name === 'login', }) }) Reflect.deleteProperty(options, 'walletOptions') this.opts = options if (!this.inLoginIframe()) { if (this.wallet.client().getAccount().isConnected) { this.walletHandler() } else { const isLogin = (async () => { try { await this.checkSession() return await this.isAuthenticated() } catch (err) { console.log('🚀 ~ file: authc.ts:57 ~ Authc ~ err:', err) return false } })() isLogin.then(async (res) => { if (res) { const record = await this.getClientCache() // authc login if (record.loginMode === false) { setTimeout(() => { this.connectWallet() }, 2500) } } }) } this.onConnectWalletByPostMessage() } } onConnectWalletByPostMessage() { this.wallet.onConnectWalletByPostMessage((...args) => { const iframeWindow = window.document.querySelector('.authc-popup > iframe')?.contentWindow // eslint-disable-next-line prefer-spread return iframeWindow?.postMessage.apply(iframeWindow, args as any) }) } private closeWatchHandler = () => {} walletHandler() { this.closeWatchHandler() const isLogin = async () => { try { await this.loginPromise await this.checkSession() return await this.isAuthenticated() } catch { return false } } const watchAccount = async (account: any) => { if (account.isConnected && await isLogin()) { // 钱包连接成功,但是钱包地址和缓存的地址不一致,应该登出 const cache = await this.getClientCache() if (account.address.toLocaleUpperCase() !== cache.walletAddress?.toLocaleUpperCase()) { console.warn('Current wallet address is not equal to the cached address, will logout', account.address, cache.walletAddress) return this.logout() } } const record = await this.getClientCache() if (account.status === 'disconnected' && await isLogin()) { if (this.wallet.walletName && account.isCustomWallet) { return this.wallet.connectWalletCustom().catch(() => { // 重连失败,应该登出 if (!this.opts.unlockOnDemand || record?.loginMode === true) { return this.logout() } else { // TODO: should to reconnect return this.logout() } }) } else if (!this.opts.unlockOnDemand || record.loginMode === true) { return this.logout() } } else if (account.isConnected && !await isLogin()) { return this.wallet.disconnect() } } this.closeWatchHandler = this.wallet.watchAccount(debouncePromise(watchAccount, 100)) } async loginWithWallet( options: RedirectLoginOptions = {}, ) { this.loginPromise = new Promise(async (resolve, reject) => { const res = await this.loginWithIframe(options).catch((err) => { this.wallet.disconnect() reject(err) throw err }) const account = this.wallet.client().getAccount() await this.updateClientCache({ /** * true: metamask, walletconnect, etc... * false: custom wallet */ loginMode: account.isConnected, }) if (!account.isConnected) { this.connectWallet().catch((err) => { this.logout() return reject(err) }) } else { await this.updateClientCache({ walletAddress: account.address, }) this.walletHandler() } resolve(res) }) return this.loginPromise } async connectWallet() { const res = await this.wallet.connectWalletCustom() await this.updateClientCache({ walletAddress: res.address, }) this.walletHandler() return res } toUnlockWallet() { if (this.store.getState().status === 'reconstruct') this.wallet.replace('/reconstruct') } async logout(options?: LogoutOptions) { if ((await this.wallet.client().getAccount()).status === 'connected') return this.wallet.disconnect() this.closeWatchHandler() const logoutPromise = new Promise(async (resolve, reject) => { try { resolve(await super.logout({ silent: true, ...options, })) } catch (err) { reject(err) } }) return logoutPromise.finally(() => { this.events.afterLogout?.() }) } }