unify state management for extension accounts
This commit is contained in:
+156
-60
@@ -1,5 +1,6 @@
|
||||
import { state } from "@react-rxjs/core"
|
||||
import { state, withDefault } from "@react-rxjs/core"
|
||||
import { combineKeys, createKeyedSignal } from "@react-rxjs/utils"
|
||||
import { Binary, getSs58AddressInfo } from "polkadot-api"
|
||||
import {
|
||||
connectInjectedExtension,
|
||||
getInjectedExtensions,
|
||||
@@ -7,31 +8,37 @@ import {
|
||||
InjectedPolkadotAccount,
|
||||
} from "polkadot-api/pjs-signer"
|
||||
import {
|
||||
catchError,
|
||||
concat,
|
||||
defer,
|
||||
EMPTY,
|
||||
exhaustMap,
|
||||
filter,
|
||||
interval,
|
||||
map,
|
||||
merge,
|
||||
mergeMap,
|
||||
NEVER,
|
||||
Observable,
|
||||
of,
|
||||
scan,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
timer,
|
||||
} from "rxjs"
|
||||
|
||||
const availableExtensions$ = state(
|
||||
defer(() =>
|
||||
timer(100, 500).pipe(
|
||||
export const availableExtensions$ = state(
|
||||
concat(
|
||||
timer(0, 100).pipe(
|
||||
map(getInjectedExtensions),
|
||||
startWith(getInjectedExtensions()),
|
||||
filter((v) => v.length > 0),
|
||||
take(1),
|
||||
),
|
||||
interval(2000).pipe(map(getInjectedExtensions)),
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
const [toggleExtension$, onTogleExtension] = createKeyedSignal<string>()
|
||||
export { onTogleExtension }
|
||||
const [toggleExtension$, onToggleExtension] = createKeyedSignal<string>()
|
||||
export { onToggleExtension }
|
||||
|
||||
export const enum ConnectStatus {
|
||||
Connecting,
|
||||
@@ -45,61 +52,150 @@ export type ExtensionState =
|
||||
| { type: ConnectStatus.Connecting }
|
||||
| { type: ConnectStatus.Connected; value: InjectedExtension }
|
||||
|
||||
const extension$ = state(
|
||||
(name: string): Observable<ExtensionState> => {
|
||||
const storageKey = `extension-${name}`
|
||||
const isInStorage = localStorage.hasOwnProperty(storageKey)
|
||||
const SELECTED_EXTENSIONS_KEY = "selected-extensions"
|
||||
const getPreselectedExtensions = () => {
|
||||
try {
|
||||
const res = JSON.parse(localStorage.getItem(SELECTED_EXTENSIONS_KEY)!)
|
||||
if (Array.isArray(res)) return res
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (_) {}
|
||||
return null
|
||||
}
|
||||
const extensionIsPreselected = (extension: string) =>
|
||||
getPreselectedExtensions()?.includes(extension) ?? false
|
||||
const setPreselectedExtension = (extension: string, selected: boolean) => {
|
||||
const preselectedExtensions = getPreselectedExtensions() ?? []
|
||||
const result = selected
|
||||
? [...new Set([...preselectedExtensions, extension])]
|
||||
: preselectedExtensions.filter((v) => v !== extension)
|
||||
localStorage.setItem(SELECTED_EXTENSIONS_KEY, JSON.stringify(result))
|
||||
}
|
||||
|
||||
const connect$ = defer(() =>
|
||||
connectInjectedExtension(name)
|
||||
.then((value) => ({
|
||||
type: ConnectStatus.Connected as const,
|
||||
value,
|
||||
}))
|
||||
.catch(() => ({ type: ConnectStatus.Disconnected as const })),
|
||||
).pipe(startWith({ type: ConnectStatus.Connecting as const }))
|
||||
const extension$ = (name: string) => {
|
||||
const connect$ = defer(() => connectInjectedExtension(name)).pipe(
|
||||
map((extension) => ({
|
||||
type: ConnectStatus.Connected as const,
|
||||
extension,
|
||||
})),
|
||||
catchError(() => of({ type: ConnectStatus.Disconnected as const })),
|
||||
startWith({ type: ConnectStatus.Connecting as const }),
|
||||
)
|
||||
|
||||
const init$ = isInStorage ? of() : EMPTY
|
||||
|
||||
let state: ExtensionState = { type: ConnectStatus.Disconnected }
|
||||
return merge(
|
||||
init$,
|
||||
toggleExtension$(name).pipe(
|
||||
exhaustMap(() =>
|
||||
state.type === ConnectStatus.Disconnected
|
||||
? connect$
|
||||
: of({ type: ConnectStatus.Disconnected as const }),
|
||||
),
|
||||
tap(({ type }) => {
|
||||
if (type === ConnectStatus.Disconnected)
|
||||
localStorage.removeItem(storageKey)
|
||||
else if (type === ConnectStatus.Connected)
|
||||
localStorage.setItem(storageKey, "")
|
||||
}),
|
||||
),
|
||||
const connectWithCleanup$ = defer(() => {
|
||||
let disconnected = false
|
||||
let extension: InjectedExtension | null = null
|
||||
return concat(connect$, NEVER).pipe(
|
||||
tap({
|
||||
next(value) {
|
||||
if (value.type === ConnectStatus.Connected) {
|
||||
if (disconnected) {
|
||||
value.extension.disconnect()
|
||||
} else {
|
||||
extension = value.extension
|
||||
}
|
||||
}
|
||||
},
|
||||
unsubscribe() {
|
||||
if (extension) {
|
||||
extension.disconnect()
|
||||
} else {
|
||||
disconnected = true
|
||||
}
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
{ type: ConnectStatus.Disconnected as const },
|
||||
)
|
||||
})
|
||||
|
||||
const extensionAccounts$ = state((name: string) =>
|
||||
extension$(name).pipe(
|
||||
mergeMap((x) => {
|
||||
if (x.type !== ConnectStatus.Connected) return EMPTY
|
||||
return new Observable<InjectedPolkadotAccount[]>((observer) =>
|
||||
x.value.subscribe((accounts) => {
|
||||
observer.next(accounts)
|
||||
}),
|
||||
)
|
||||
const initialSelected = extensionIsPreselected(name)
|
||||
return toggleExtension$(name).pipe(
|
||||
scan((acc) => !acc, initialSelected),
|
||||
startWith(initialSelected),
|
||||
switchMap((selected) =>
|
||||
selected
|
||||
? connectWithCleanup$
|
||||
: of({
|
||||
type: ConnectStatus.Disconnected as const,
|
||||
}),
|
||||
),
|
||||
tap((v) => {
|
||||
if (v.type === ConnectStatus.Connected) {
|
||||
setPreselectedExtension(name, true)
|
||||
} else if (v.type === ConnectStatus.Disconnected) {
|
||||
setPreselectedExtension(name, false)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export const extensions$ = state(combineKeys(availableExtensions$, extension$))
|
||||
|
||||
export const selectedExtensions$ = extensions$.pipeState(
|
||||
map(
|
||||
(extensions) =>
|
||||
new Map(
|
||||
[...extensions.entries()]
|
||||
.filter(([, v]) => v.type !== ConnectStatus.Disconnected)
|
||||
.map(([k, v]) => [
|
||||
k,
|
||||
v.type === ConnectStatus.Connected ? v.extension : null,
|
||||
]),
|
||||
),
|
||||
),
|
||||
withDefault(new Map<string, InjectedExtension | null>()),
|
||||
)
|
||||
|
||||
export const extensions$ = combineKeys(availableExtensions$, extension$)
|
||||
// extensions$.subscribe()
|
||||
|
||||
export const accountsByExntesion$ = combineKeys(
|
||||
availableExtensions$,
|
||||
extensionAccounts$,
|
||||
export const extensionAccounts$ = state(
|
||||
(name: string) =>
|
||||
extension$(name).pipe(
|
||||
switchMap((x) => {
|
||||
if (x.type !== ConnectStatus.Connected) return of([])
|
||||
return new Observable<InjectedPolkadotAccount[]>((observer) =>
|
||||
x.extension.subscribe((accounts) => {
|
||||
observer.next(accounts)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
export const accountsByExtension$ = state(
|
||||
combineKeys(availableExtensions$, extensionAccounts$),
|
||||
new Map<string, InjectedPolkadotAccount[]>(),
|
||||
)
|
||||
|
||||
export const getPublicKey = (address: string) => {
|
||||
const info = getSs58AddressInfo(address)
|
||||
return info.isValid ? info.publicKey : null
|
||||
}
|
||||
export const getAccountMapKey = (address: string) => {
|
||||
const pk = getPublicKey(address)
|
||||
return pk ? Binary.fromBytes(pk).asHex() : address
|
||||
}
|
||||
|
||||
export const accounts$ = state(
|
||||
accountsByExtension$.pipe(
|
||||
map(
|
||||
(extensionAccounts) =>
|
||||
new Map(
|
||||
Array.from(extensionAccounts.entries()).flatMap(
|
||||
([extension, accounts]) =>
|
||||
accounts.map((account) => [
|
||||
getAccountMapKey(account.address),
|
||||
{
|
||||
...account,
|
||||
extension,
|
||||
},
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
new Map<string, InjectedPolkadotAccount & { extension: string }>(),
|
||||
)
|
||||
|
||||
export const accountDetail$ = state(
|
||||
(account: string) =>
|
||||
accounts$.pipe(map((v) => v.get(getAccountMapKey(account)) ?? null)),
|
||||
null,
|
||||
)
|
||||
// accountsByExntesion$.subscribe()
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
accountDetail$,
|
||||
accounts$,
|
||||
getAccountMapKey,
|
||||
getPublicKey,
|
||||
} from "@/accounts.state"
|
||||
import { PolkadotIdenticon } from "@/components/PolkadotIdenticon"
|
||||
import { useSynchronizeInput } from "@/components/useSynchroniseInput"
|
||||
import { EditAccountId, NOTIN } from "@codec-components"
|
||||
@@ -15,12 +21,6 @@ import {
|
||||
import { useStateObservable } from "@react-rxjs/core"
|
||||
import { FC, useState } from "react"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import {
|
||||
accountDetail$,
|
||||
accounts$,
|
||||
getAccountMapKey,
|
||||
getPublicKey,
|
||||
} from "../common/accounts.state"
|
||||
|
||||
export const CAccountId: EditAccountId = ({ value, onValueChanged }) => {
|
||||
const [localInput, setLocalInput] = useSynchronizeInput(
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { Binary, getSs58AddressInfo } from "@polkadot-api/substrate-bindings"
|
||||
import { state } from "@react-rxjs/core"
|
||||
import { combineKeys } from "@react-rxjs/utils"
|
||||
import {
|
||||
connectInjectedExtension,
|
||||
getInjectedExtensions,
|
||||
InjectedPolkadotAccount,
|
||||
} from "polkadot-api/pjs-signer"
|
||||
import {
|
||||
concat,
|
||||
filter,
|
||||
finalize,
|
||||
from,
|
||||
fromEventPattern,
|
||||
interval,
|
||||
map,
|
||||
retry,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
timer,
|
||||
} from "rxjs"
|
||||
|
||||
export const getPublicKey = (address: string) => {
|
||||
const info = getSs58AddressInfo(address)
|
||||
return info.isValid ? info.publicKey : null
|
||||
}
|
||||
export const getAccountMapKey = (address: string) => {
|
||||
const pk = getPublicKey(address)
|
||||
return pk ? Binary.fromBytes(pk).asHex() : address
|
||||
}
|
||||
|
||||
const extensions$ = state(
|
||||
concat(
|
||||
timer(0, 100).pipe(
|
||||
map(getInjectedExtensions),
|
||||
filter((v) => v.length > 0),
|
||||
take(1),
|
||||
),
|
||||
interval(2000).pipe(map(getInjectedExtensions)),
|
||||
),
|
||||
[],
|
||||
)
|
||||
const extensionAccounts$ = combineKeys(extensions$, (extension) =>
|
||||
from(connectInjectedExtension(extension)).pipe(
|
||||
switchMap((extension) =>
|
||||
fromEventPattern<InjectedPolkadotAccount[]>(
|
||||
extension.subscribe,
|
||||
(_, fn) => fn(),
|
||||
).pipe(
|
||||
startWith(extension.getAccounts()),
|
||||
finalize(extension.disconnect),
|
||||
),
|
||||
),
|
||||
retry({
|
||||
delay: 100,
|
||||
}),
|
||||
),
|
||||
)
|
||||
export const accounts$ = state(
|
||||
extensionAccounts$.pipe(
|
||||
map(
|
||||
(extensionAccounts) =>
|
||||
new Map(
|
||||
Array.from(extensionAccounts.entries()).flatMap(
|
||||
([extension, accounts]) =>
|
||||
accounts.map((account) => [
|
||||
getAccountMapKey(account.address),
|
||||
{
|
||||
...account,
|
||||
extension,
|
||||
},
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
new Map<string, InjectedPolkadotAccount & { extension: string }>(),
|
||||
)
|
||||
|
||||
export const accountDetail$ = state(
|
||||
(account: string) =>
|
||||
accounts$.pipe(map((v) => v.get(getAccountMapKey(account)) ?? null)),
|
||||
null,
|
||||
)
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
accountDetail$,
|
||||
getPublicKey,
|
||||
} from "@/codec-components/common/accounts.state"
|
||||
import { accountDetail$, getPublicKey } from "@/accounts.state"
|
||||
import { PolkadotIdenticon } from "@/components/PolkadotIdenticon"
|
||||
import { useStateObservable } from "@react-rxjs/core"
|
||||
import { SS58String } from "polkadot-api"
|
||||
|
||||
@@ -74,6 +74,7 @@ const SelectContent = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"bg-black",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
|
||||
@@ -28,7 +28,8 @@ export const TabsTrigger: React.FC<DivProps & { active?: boolean }> = ({
|
||||
{...props}
|
||||
className={cn(
|
||||
"cursor-pointer inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
active ? "bg-background text-foreground shadow-sm" : "",
|
||||
"border border-polkadot-500",
|
||||
active ? "bg-polkadot-500 text-foreground shadow-sm" : "",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import {
|
||||
InjectedExtension,
|
||||
InjectedPolkadotAccount,
|
||||
} from "polkadot-api/pjs-signer"
|
||||
import React, {
|
||||
PropsWithChildren,
|
||||
useEffect,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
} from "react"
|
||||
import { useSelectedExtensions } from "./extensionCtx"
|
||||
accountsByExtension$,
|
||||
extensionAccounts$,
|
||||
selectedExtensions$,
|
||||
} from "@/accounts.state"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -18,15 +12,16 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { SelectedAccountCtx } from "./accountCtx"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { createSignal } from "@react-rxjs/utils"
|
||||
import { InjectedExtension } from "polkadot-api/pjs-signer"
|
||||
import React from "react"
|
||||
import { combineLatest, distinctUntilChanged, map, merge, of, tap } from "rxjs"
|
||||
|
||||
export const Accounts: React.FC<{ extension: InjectedExtension }> = ({
|
||||
const Accounts: React.FC<{ extension: InjectedExtension }> = ({
|
||||
extension,
|
||||
}) => {
|
||||
const accounts = useSyncExternalStore(
|
||||
extension.subscribe,
|
||||
extension.getAccounts,
|
||||
)
|
||||
const accounts = useStateObservable(extensionAccounts$(extension.name))
|
||||
|
||||
return (
|
||||
<SelectGroup>
|
||||
@@ -43,58 +38,51 @@ export const Accounts: React.FC<{ extension: InjectedExtension }> = ({
|
||||
)
|
||||
}
|
||||
|
||||
const SignerCtx: React.FC<PropsWithChildren<{ account: string | null }>> = ({
|
||||
account,
|
||||
children,
|
||||
}) => {
|
||||
const extensions = useSelectedExtensions()
|
||||
const [injectedPolkadotAccount, setInjectedPolkadotAccount] =
|
||||
useState<InjectedPolkadotAccount | null>(null)
|
||||
const [valueSelected$, selectValue] = createSignal<string>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!account) {
|
||||
setInjectedPolkadotAccount(null)
|
||||
return
|
||||
}
|
||||
const LS_KEY = "selected-signer"
|
||||
const selectedValue$ = state(
|
||||
merge(
|
||||
of(localStorage.getItem(LS_KEY)),
|
||||
valueSelected$.pipe(tap((v) => localStorage.setItem(LS_KEY, v))),
|
||||
),
|
||||
null,
|
||||
)
|
||||
|
||||
const separator = account.indexOf("-")
|
||||
const address = account.slice(0, separator)
|
||||
const extensionName = account.slice(separator + 1)
|
||||
export const selectedAccount$ = state(
|
||||
combineLatest([selectedValue$, accountsByExtension$]).pipe(
|
||||
map(([selectedAccount, accountsByExtension]) => {
|
||||
if (!selectedAccount) return null
|
||||
const [address, ...rest] = selectedAccount.split("-")
|
||||
const signer = rest.join("-")
|
||||
|
||||
setInjectedPolkadotAccount(
|
||||
extensions
|
||||
.find((x) => x.name === extensionName)
|
||||
?.getAccounts()
|
||||
.find((account) => account.address === address) ?? null,
|
||||
)
|
||||
}, [extensions, account])
|
||||
const accounts = accountsByExtension.get(signer)
|
||||
if (!accounts) return null
|
||||
return accounts.find((account) => account.address === address) ?? null
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
),
|
||||
null,
|
||||
)
|
||||
|
||||
export const AccountProvider: React.FC = () => {
|
||||
const value = useStateObservable(selectedValue$)
|
||||
const extensions = useStateObservable(selectedExtensions$)
|
||||
|
||||
const activeExtensions = [...extensions.values()].filter((v) => !!v)
|
||||
|
||||
if (!activeExtensions.length) return null
|
||||
|
||||
return (
|
||||
injectedPolkadotAccount && (
|
||||
<SelectedAccountCtx.Provider value={injectedPolkadotAccount}>
|
||||
{children}
|
||||
</SelectedAccountCtx.Provider>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export const AccountProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const [selectedAccount, setSelectedAcount] = useState<string | null>(null)
|
||||
const extensions = useSelectedExtensions()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select onValueChange={setSelectedAcount}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an account" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{extensions.map((extension) => (
|
||||
<Accounts key={extension.name} {...{ extension }} />
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<SignerCtx account={selectedAccount}>{children}</SignerCtx>
|
||||
</>
|
||||
<Select value={value ?? ""} onValueChange={selectValue}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an account" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{activeExtensions.map((extension) => (
|
||||
<Accounts key={extension.name} extension={extension} />
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,85 +1,27 @@
|
||||
import {
|
||||
InjectedExtension,
|
||||
connectInjectedExtension,
|
||||
} from "polkadot-api/pjs-signer"
|
||||
import { PropsWithChildren, useSyncExternalStore } from "react"
|
||||
import { extensionCtx, useAvailableExtensions } from "./extensionCtx"
|
||||
import { TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
availableExtensions$,
|
||||
onToggleExtension,
|
||||
selectedExtensions$,
|
||||
} from "@/accounts.state"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { useStateObservable } from "@react-rxjs/core"
|
||||
|
||||
const getExtensionsStore = () => {
|
||||
let connectedExtensions = new Map<string, InjectedExtension>()
|
||||
const getSnapshot = () => connectedExtensions
|
||||
|
||||
const listeners = new Set<() => void>()
|
||||
const update = () => {
|
||||
connectedExtensions = new Map(connectedExtensions)
|
||||
listeners.forEach((cb) => {
|
||||
cb()
|
||||
})
|
||||
}
|
||||
const subscribe = (cb: () => void) => {
|
||||
listeners.add(cb)
|
||||
return () => {
|
||||
listeners.delete(cb)
|
||||
}
|
||||
}
|
||||
|
||||
let isRunning = false
|
||||
const onToggleExtension = (name: string) => {
|
||||
if (isRunning) return
|
||||
|
||||
if (connectedExtensions.has(name)) {
|
||||
connectedExtensions.delete(name)
|
||||
return update()
|
||||
}
|
||||
|
||||
isRunning = true
|
||||
connectInjectedExtension(name)
|
||||
.then(
|
||||
(extension) => {
|
||||
connectedExtensions.set(name, extension)
|
||||
update()
|
||||
},
|
||||
() => {},
|
||||
)
|
||||
.finally(() => {
|
||||
isRunning = false
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
getSnapshot,
|
||||
onToggleExtension,
|
||||
}
|
||||
}
|
||||
|
||||
const extensionsStore = getExtensionsStore()
|
||||
extensionsStore.subscribe(Function.prototype as any)
|
||||
|
||||
export const ExtensionProvider: React.FC<PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const availableExtensions = useAvailableExtensions()
|
||||
const selectedExtensions = useSyncExternalStore(
|
||||
extensionsStore.subscribe,
|
||||
extensionsStore.getSnapshot,
|
||||
)
|
||||
export const ExtensionProvider: React.FC = () => {
|
||||
const availableExtensions = useStateObservable(availableExtensions$)
|
||||
const selectedExtensions = useStateObservable(selectedExtensions$)
|
||||
|
||||
if (availableExtensions.length === 0)
|
||||
return <div>No extension provider detected</div>
|
||||
|
||||
return (
|
||||
<>
|
||||
<Label>Click on the extension name to (dis)connect it:</Label>
|
||||
<Label>Click on the extension name to toggle it:</Label>
|
||||
<TabsList>
|
||||
{availableExtensions.map((extensionName) => (
|
||||
<TabsTrigger
|
||||
className="mx-1"
|
||||
onClick={() => {
|
||||
extensionsStore.onToggleExtension(extensionName)
|
||||
}}
|
||||
onClick={() => onToggleExtension(extensionName)}
|
||||
active={selectedExtensions.has(extensionName)}
|
||||
key={extensionName}
|
||||
>
|
||||
@@ -87,9 +29,6 @@ export const ExtensionProvider: React.FC<PropsWithChildren> = ({
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
<extensionCtx.Provider value={[...selectedExtensions.values()]}>
|
||||
{selectedExtensions.size ? children : null}
|
||||
</extensionCtx.Provider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { unsafeApi$ } from "@/chain.state"
|
||||
import { ActionButton } from "@/components/ActionButton"
|
||||
import {
|
||||
Dialog,
|
||||
@@ -5,23 +6,25 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { onNexTx } from "@/pages/Transactions"
|
||||
import { toHex } from "@polkadot-api/utils"
|
||||
import { useStateObservable } from "@react-rxjs/core"
|
||||
import { Binary, HexString } from "polkadot-api"
|
||||
import React, { createContext, useContext, useState } from "react"
|
||||
import { ExtensionProvider } from "./ExtensionProvider"
|
||||
import { AccountProvider } from "./AccountProvider"
|
||||
import { useSelectedAccount } from "./accountCtx"
|
||||
import { unsafeApi$ } from "@/chain.state"
|
||||
import { noop } from "rxjs"
|
||||
import { toHex } from "@polkadot-api/utils"
|
||||
import { onNexTx } from "@/pages/Transactions"
|
||||
import { AccountProvider, selectedAccount$ } from "./AccountProvider"
|
||||
import { ExtensionProvider } from "./ExtensionProvider"
|
||||
|
||||
const CallDataCtx = createContext("")
|
||||
const OnCloseCtx = createContext(noop)
|
||||
|
||||
const SignAndSubmit = () => {
|
||||
const account = useSelectedAccount()
|
||||
const account = useStateObservable(selectedAccount$)
|
||||
const callData = useContext(CallDataCtx)
|
||||
const close = useContext(OnCloseCtx)
|
||||
|
||||
if (!account) return null
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
onClick={() => {
|
||||
@@ -51,11 +54,11 @@ const SignAndSubmit = () => {
|
||||
|
||||
const SelectAccountFromExtension = () => {
|
||||
return (
|
||||
<ExtensionProvider>
|
||||
<AccountProvider>
|
||||
<SignAndSubmit />
|
||||
</AccountProvider>
|
||||
</ExtensionProvider>
|
||||
<>
|
||||
<ExtensionProvider />
|
||||
<AccountProvider />
|
||||
<SignAndSubmit />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { type InjectedPolkadotAccount } from "polkadot-api/pjs-signer"
|
||||
import { createContext, useContext } from "react"
|
||||
|
||||
export const SelectedAccountCtx = createContext<InjectedPolkadotAccount | null>(
|
||||
null,
|
||||
)
|
||||
export const useSelectedAccount = () => useContext(SelectedAccountCtx)!
|
||||
@@ -1,33 +0,0 @@
|
||||
import {
|
||||
InjectedExtension,
|
||||
getInjectedExtensions,
|
||||
} from "polkadot-api/pjs-signer"
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from "react"
|
||||
|
||||
const getJoinedInjectedExtensions = () => getInjectedExtensions().join(",")
|
||||
|
||||
export const useAvailableExtensions = (): string[] => {
|
||||
const [extensions, setExtensions] = useState(getJoinedInjectedExtensions)
|
||||
|
||||
useEffect(() => {
|
||||
let token: any
|
||||
const updateExtensions = () => {
|
||||
const jointedExtensions = getJoinedInjectedExtensions()
|
||||
setExtensions(jointedExtensions)
|
||||
token = setTimeout(updateExtensions, jointedExtensions ? 2_000 : 100)
|
||||
}
|
||||
updateExtensions()
|
||||
|
||||
return () => {
|
||||
clearTimeout(token)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return useMemo(
|
||||
() => (extensions.length ? extensions.split(",") : []),
|
||||
[extensions],
|
||||
)
|
||||
}
|
||||
|
||||
export const extensionCtx = createContext<InjectedExtension[]>([])
|
||||
export const useSelectedExtensions = () => useContext(extensionCtx)
|
||||
Reference in New Issue
Block a user