import type { Observable } from "rxjs" import { BehaviorSubject, concat, defer, NEVER, of } from "rxjs" import { catchError, distinctUntilChanged, first, map, shareReplay, switchMap, tap } from "rxjs/operators" import type { ConnectionProvider } from "./provider" import type { ConnectionState } from "./connection-state" import { getStateConnecting, getStateDisconnected, STATE_INITIALIZING } from "./connection-state" export type ProviderOption = { provider: ConnectionProvider option: Option } export interface IConnector { /** * Get all available connection options (Metamask, Fortmatic, Blocto, Temple etc) */ getOptions(): Promise[]> /** * Connect using specific option */ connect(option: ProviderOption): void /** * Subscribe to this observable to get current connection state */ connection: Observable> } /** * This component is used to save/load last connected provider */ export interface IConnectorStateProvider { getValue(): Promise setValue(value: string | undefined): Promise } export class DefaultConnectionStateProvider implements IConnectorStateProvider { constructor(private readonly key: string) { } async getValue(): Promise { const value = localStorage.getItem(this.key) return value !== null ? value : undefined } async setValue(value: string | undefined): Promise { if (value === undefined) { localStorage.removeItem(this.key) } else { localStorage.setItem(this.key, value) } } } export class Connector implements IConnector { static pageUnloading: boolean | undefined private readonly provider = new BehaviorSubject | undefined>(undefined) public connection: Observable> constructor( private readonly providers: ConnectionProvider[], private readonly stateProvider?: IConnectorStateProvider, ) { Connector.initPageUnloadProtection() this.add = this.add.bind(this) this.connect = this.connect.bind(this) this.connection = concat( of(STATE_INITIALIZING), defer(() => this.checkAutoConnect()), this.provider.pipe( distinctUntilChanged(), switchMap(provider => { if (provider) { return concat(provider.getConnection(), NEVER).pipe( catchError(error => concat(of(getStateDisconnected({ error })), NEVER)), ) } else { return concat(of(getStateDisconnected()), NEVER) } }), ), ).pipe( distinctUntilChanged((c1, c2) => { if (Connector.pageUnloading) return true if (c1 === c2) return true if (c1.status === "connected" && c2.status === "connected") { return c1.connection === c2.connection } else if (c1.status === "connecting" && c2.status === "connecting") { return c1.providerId === c2.providerId } return c1.status === c2.status }), shareReplay(1), map(conn => { if (conn.status === "connected") { return { ...conn, disconnect: async () => { if (conn.disconnect !== undefined) { try { await conn.disconnect() } catch (e) { console.warn("caught on disconnect", e) } } this.provider.next(undefined) }, } } else { return conn } }), tap(async conn => { if (conn.status === "disconnected" && !Connector.pageUnloading) { this.provider.next(undefined) const current = await this.stateProvider?.getValue() if (current !== undefined) { this.stateProvider?.setValue(undefined) } } }), ) } /** * Add flag when page unload to avoid disconnect events from connectors */ static initPageUnloadProtection() { if (Connector.pageUnloading === undefined && typeof window !== "undefined") { window.addEventListener("beforeunload", function () { Connector.pageUnloading = true }) Connector.pageUnloading = false } } /** * Push {@link provider} to connectors list * @param provider connection provider */ add(provider: ConnectionProvider