From f67f226cc1d29032b54e9f371a63d51122389c6f Mon Sep 17 00:00:00 2001 From: David Totraev Date: Wed, 20 Nov 2024 12:41:03 +0500 Subject: [PATCH 1/3] feat: widget API --- src/context/Chain.context.tsx | 63 +++++++++++ src/context/Inscriptions.context.tsx | 35 ++++++ src/core/Wallet.ts | 18 +++- src/core/WalletConnector.ts | 37 ++++--- src/core/types.ts | 15 ++- src/core/wallets/bbn/BBNProvider.ts | 11 ++ src/core/wallets/bbn/babylon.jpeg | Bin 0 -> 6052 bytes src/core/wallets/bbn/index.ts | 13 +++ src/core/wallets/btc/BTCProvider.ts | 4 +- src/core/wallets/btc/bitcoin.png | Bin 0 -> 1600 bytes src/core/wallets/btc/index.ts | 8 +- src/core/wallets/index.tsx | 4 + src/hocs/withAppState.tsx | 18 ++++ src/hooks/useChainConnector.ts | 7 ++ src/hooks/usePersistState.ts | 24 +++++ src/index.tsx | 6 ++ src/state/state.d.ts | 29 +++-- src/state/state.tsx | 101 ++++++++++-------- src/utils/wallet.ts | 9 ++ src/widgets/ChainButton/index.tsx | 17 ++- src/widgets/Chains/index.stories.ts | 49 +++++++++ src/widgets/Chains/index.tsx | 81 ++++++++++++++ src/widgets/ConnectedWallet/index.tsx | 36 +++++-- src/widgets/Inscriptions/index.tsx | 43 +++++--- src/widgets/SelectChain/index.stories.ts | 19 ---- src/widgets/SelectChain/index.tsx | 35 ------ src/widgets/SelectWallet/index.stories.tsx | 26 ----- src/widgets/SelectWallet/index.tsx | 38 ------- src/widgets/TermsOfService/index.tsx | 33 ++++-- src/widgets/WalletButton/index.stories.tsx | 1 + src/widgets/WalletButton/index.tsx | 4 +- .../WalletProvider/components/Screen.tsx | 34 ++++++ .../components/WalletDialog.tsx | 57 ++++++++++ src/widgets/WalletProvider/index.stories.tsx | 42 ++++++++ src/widgets/WalletProvider/index.tsx | 26 +++++ src/widgets/Wallets/index.stories.tsx | 57 ++++++++++ src/widgets/Wallets/index.tsx | 80 ++++++++++++++ src/widgets/Widget/index.stories.tsx | 16 --- src/widgets/Widget/index.tsx | 25 ----- 39 files changed, 855 insertions(+), 266 deletions(-) create mode 100644 src/context/Chain.context.tsx create mode 100644 src/context/Inscriptions.context.tsx create mode 100644 src/core/wallets/bbn/BBNProvider.ts create mode 100644 src/core/wallets/bbn/babylon.jpeg create mode 100644 src/core/wallets/bbn/index.ts create mode 100644 src/core/wallets/btc/bitcoin.png create mode 100644 src/core/wallets/index.tsx create mode 100644 src/hocs/withAppState.tsx create mode 100644 src/hooks/useChainConnector.ts create mode 100644 src/hooks/usePersistState.ts create mode 100644 src/utils/wallet.ts create mode 100644 src/widgets/Chains/index.stories.ts create mode 100644 src/widgets/Chains/index.tsx delete mode 100644 src/widgets/SelectChain/index.stories.ts delete mode 100644 src/widgets/SelectChain/index.tsx delete mode 100644 src/widgets/SelectWallet/index.stories.tsx delete mode 100644 src/widgets/SelectWallet/index.tsx create mode 100644 src/widgets/WalletProvider/components/Screen.tsx create mode 100644 src/widgets/WalletProvider/components/WalletDialog.tsx create mode 100644 src/widgets/WalletProvider/index.stories.tsx create mode 100644 src/widgets/WalletProvider/index.tsx create mode 100644 src/widgets/Wallets/index.stories.tsx create mode 100644 src/widgets/Wallets/index.tsx delete mode 100644 src/widgets/Widget/index.stories.tsx delete mode 100644 src/widgets/Widget/index.tsx diff --git a/src/context/Chain.context.tsx b/src/context/Chain.context.tsx new file mode 100644 index 0000000..5861bc1 --- /dev/null +++ b/src/context/Chain.context.tsx @@ -0,0 +1,63 @@ +import { createContext, PropsWithChildren, useEffect, useState, useCallback, useContext } from "react"; +import { useAppState } from "@/state/state"; +import { WalletConnector } from "@/core/WalletConnector"; +import type { NetworkConfig } from "@/core/types"; + +import metadata from "@/core/wallets"; +import { BTCProvider } from "@/core/wallets/btc/BTCProvider"; +import { BBNProvider } from "@/core/wallets/bbn/BBNProvider"; + +interface ProviderProps { + context: any; + config: NetworkConfig; +} + +export interface Connectors { + BTC: WalletConnector<"BTC", BTCProvider> | null; + BBN: WalletConnector<"BBN", BBNProvider> | null; +} + +export type SupportedChains = keyof Connectors; + +const defaultState: Connectors = { + BTC: null, + BBN: null, +}; + +const Context = createContext(defaultState); + +export function ChainProvider({ children, context, config }: PropsWithChildren) { + const [connectors, setConnectors] = useState(defaultState); + const { addChain, displayLoading, displayTermsOfService } = useAppState(); + + const init = useCallback(async () => { + displayLoading?.(); + + const metadataArr = Object.values(metadata); + const connectorArr = await Promise.all(metadataArr.map((data) => WalletConnector.create(data, context, config))); + + return connectorArr.reduce((acc, connector) => ({ ...acc, [connector.id]: connector }), {} as Connectors); + }, []); + + useEffect(() => { + if (!displayLoading || !addChain || !setConnectors || !displayTermsOfService) return; + + displayLoading(); + + init().then((connectors) => { + setConnectors(connectors); + + Object.values(connectors).forEach((connector) => { + addChain(connector); + }); + + displayTermsOfService(); + }); + }, [displayLoading, addChain, setConnectors, init, displayTermsOfService]); + + return {children}; +} + +export const useChainProviders = () => { + return useContext(Context); +}; diff --git a/src/context/Inscriptions.context.tsx b/src/context/Inscriptions.context.tsx new file mode 100644 index 0000000..2db3d72 --- /dev/null +++ b/src/context/Inscriptions.context.tsx @@ -0,0 +1,35 @@ +import { createContext, PropsWithChildren, useContext, useMemo } from "react"; + +import { usePersistState } from "@/hooks/usePersistState"; + +interface InscriptionContext { + lockInscriptions: boolean; + showAgain: boolean; + toggleLockInscriptions?: (value: boolean) => void; + toggleShowAgain?: (value: boolean) => void; +} + +const Context = createContext({ lockInscriptions: true, showAgain: true }); + +export function InscriptionProvider({ children, context }: PropsWithChildren<{ context: any }>) { + const [showAgain, toggleShowAgain] = usePersistState("bwc-inscription-modal-show-again", context.localStorage, true); + const [lockInscriptions, toggleLockInscriptions] = usePersistState( + "bwc-inscription-modal-lock", + context.localStorage, + true, + ); + + const inscriptionContext = useMemo( + () => ({ + showAgain, + lockInscriptions, + toggleLockInscriptions, + toggleShowAgain, + }), + [showAgain, lockInscriptions, toggleLockInscriptions, toggleShowAgain], + ); + + return {children}; +} + +export const useInscriptionProvider = () => useContext(Context); diff --git a/src/core/Wallet.ts b/src/core/Wallet.ts index 5e6029f..295c4df 100644 --- a/src/core/Wallet.ts +++ b/src/core/Wallet.ts @@ -1,4 +1,4 @@ -import { IWallet, Network, IProvider, type NetworkConfig, type WalletMetadata } from "@/core/types"; +import { IWallet, Network, IProvider, type NetworkConfig, type WalletMetadata, Account } from "@/core/types"; interface Options

{ id: string; @@ -20,6 +20,7 @@ export class Wallet

implements IWallet { readonly docs: string; readonly networkds: Network[]; readonly provider: P | null = null; + account: Account | null = null; static create = async

(metadata: WalletMetadata

, context: any, config: NetworkConfig) => { const { @@ -87,7 +88,22 @@ export class Wallet

implements IWallet { } await this.provider.connectWallet(); + const [address, publicKeyHex] = await Promise.all([this.provider.getAddress(), this.provider.getPublicKeyHex()]); + + this.account = { address, publicKeyHex }; return this; } + + clone() { + return new Wallet({ + id: this.id, + origin: this.origin, + name: this.name, + icon: this.icon, + docs: this.docs, + networks: this.networkds, + provider: this.provider, + }); + } } diff --git a/src/core/WalletConnector.ts b/src/core/WalletConnector.ts index d6e3f63..11750db 100644 --- a/src/core/WalletConnector.ts +++ b/src/core/WalletConnector.ts @@ -1,34 +1,47 @@ import { Wallet } from "@/core/Wallet"; -import type { NetworkConfig, IProvider, IChain, ConnectMetadata } from "@/core/types"; +import type { NetworkConfig, IProvider, IChain, ChainMetadata } from "@/core/types"; -export class WalletConnector

implements IChain { - connectedWallet: Wallet

| null = null; +export class WalletConnector implements IChain { + private _connectedWallet: Wallet

| null = null; - static async create

( - metadata: ConnectMetadata

, - config: NetworkConfig, + static async create( + metadata: ChainMetadata, context: any, - ): Promise> { + config: NetworkConfig, + ): Promise> { const wallets: Wallet

[] = []; for (const walletMetadata of metadata.wallets) { wallets.push(await Wallet.create(walletMetadata, context, config)); } - return new WalletConnector(metadata.chain, metadata.icon, wallets); + return new WalletConnector(metadata.chain, metadata.name, metadata.icon, wallets); } constructor( - public readonly chain: string, + public readonly id: N, + public readonly name: string, public readonly icon: string, public readonly wallets: Wallet

[], ) {} - async connect(name: string) { - const wallet = this.wallets.find((wallet) => wallet.name.toLowerCase() === name.toLowerCase()); + get connectedWallet() { + return this._connectedWallet; + } + + async connect(walletId: string) { + const wallet = this.wallets.find((wallet) => wallet.id === walletId); - this.connectedWallet = (await wallet?.connect()) ?? null; + this._connectedWallet = (await wallet?.connect()) ?? null; return this.connectedWallet; } + + disconnect() { + this._connectedWallet = null; + } + + clone() { + return new WalletConnector(this.id, this.name, this.icon, this.wallets); + } } diff --git a/src/core/types.ts b/src/core/types.ts index 2a9f2b2..3e8442e 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -52,6 +52,8 @@ export interface NetworkConfig { export interface IProvider { connectWallet: () => Promise; + getAddress: () => Promise; + getPublicKeyHex: () => Promise; } export interface IWallet { @@ -61,14 +63,20 @@ export interface IWallet { docs: string; installed: boolean; provider: IProvider | null; + account: Account | null; } export interface IChain { - chain: string; + id: string; + name: string; icon: string; wallets: IWallet[]; } +export interface Account { + address: string; + publicKeyHex: string; +} export interface WalletMetadata

{ id: string; wallet?: string | ((context: any, config: NetworkConfig) => any); @@ -79,8 +87,9 @@ export interface WalletMetadata

{ createProvider: (wallet: any, config: NetworkConfig) => P; } -export interface ConnectMetadata

{ - chain: string; +export interface ChainMetadata { + chain: N; + name: string; icon: string; wallets: WalletMetadata

[]; } diff --git a/src/core/wallets/bbn/BBNProvider.ts b/src/core/wallets/bbn/BBNProvider.ts new file mode 100644 index 0000000..5dfcb5f --- /dev/null +++ b/src/core/wallets/bbn/BBNProvider.ts @@ -0,0 +1,11 @@ +import { IProvider } from "@/core/types"; + +export abstract class BBNProvider implements IProvider { + async connectWallet() { + return this; + } + + abstract getAddress(): Promise; + + abstract getPublicKeyHex(): Promise; +} diff --git a/src/core/wallets/bbn/babylon.jpeg b/src/core/wallets/bbn/babylon.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..5a39f343b2f1163e08c083c11a0a0d6b77000b17 GIT binary patch literal 6052 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 = { + chain: "BBN", + name: "Babylon Chain", + icon, + wallets: [], +}; + +export default metadata; diff --git a/src/core/wallets/btc/BTCProvider.ts b/src/core/wallets/btc/BTCProvider.ts index 98e7965..8e038f1 100644 --- a/src/core/wallets/btc/BTCProvider.ts +++ b/src/core/wallets/btc/BTCProvider.ts @@ -1,11 +1,11 @@ -import type { Fees, InscriptionIdentifier, Network, NetworkConfig, UTXO, Provider } from "../../types"; +import type { Fees, InscriptionIdentifier, Network, NetworkConfig, UTXO, IProvider } from "../../types"; import { createMempoolAPI, MempoolApi } from "../../utils/mempool"; /** * Abstract class representing a wallet provider. * Provides methods for connecting to a wallet, retrieving wallet information, signing transactions, and more. */ -export abstract class BTCProvider implements Provider { +export abstract class BTCProvider implements IProvider { protected mempool: MempoolApi; constructor(protected config: NetworkConfig) { diff --git a/src/core/wallets/btc/bitcoin.png b/src/core/wallets/btc/bitcoin.png new file mode 100644 index 0000000000000000000000000000000000000000..e7a67fb785bdec9dd629813b406f12684254238f GIT binary patch literal 1600 zcmV-G2EX}0~Efv?@?e6J}|D4^Hp55(s&KCM5d(N5rmv3hN`Ttpl z0uc*U)<}psjLEqGtD-Cu5M_{zONLGkv{PPK0>1#f$z0XigaW|aRF<29CGkjNH4#|N zB*wdGOCyCDW7{P6Hkglu!}`rMiNzv`dQxR2DKZu#$3_^wwK_zHC79UYf6$Wb4F*|) zGYL`@g1uzcwr$Q|Ef332ao{gZCDYN|_mjYK_tK7sxoZS}0gVckyn_-XJhZ^}JDqyyH@da#m< zV9c+FJk^Tc53YtwM2ak5C94bf*i+1`q^or&;@wE@ei@>@Ijf9~Er<8|_ib`1Sfpen zTw-k0NZ6cG3U2TpCT)W#0KPkSfKR++*m`{jx4_!_HYs`(nmB2$YAL#n7Dru0F0t(n$bXJ#^-UIhAx*JZi~_++r#r}ru7k8; z_CSzY$sM({_NweP*bg<;-fP~{?q71u;SBt86bM#6*V$pmUT2yVOYVB!k!RaQ-Toz; z%jp?Jf#8#8JJRA<4s#-s85t7VV64gzQui*``&Ya|NbV`y5JHU&AMY@^>I3A zvV5;dupXV(D!fE`8YK~A=TW>hVMdzp52^f9e?fNr?w%a2Jy>Gxz*zksN=3=plRABf z*4;F%HArs1p9u7Tm0g-Dh;>7B?#IYL_cdwt>{MoPv@t;IQLcvDZYRvN;}LPBVCS5~ zW9y4owK?LWoPJES(f=hI#j6h+>VBrSy(Tj|XUK2L_zHS`hfxBFv9*XUkA^548gaTS zk@75GzvYkllOs&u&2$gl!XWmUwH*a*y8i(-v@i = { +const metadata: ChainMetadata<"BTC", BTCProvider> = { chain: "BTC", - icon: "test", + name: "Bitcoin", + icon, wallets: [injectable, okx], }; diff --git a/src/core/wallets/index.tsx b/src/core/wallets/index.tsx new file mode 100644 index 0000000..830cae8 --- /dev/null +++ b/src/core/wallets/index.tsx @@ -0,0 +1,4 @@ +import btc from "./btc"; +import bbn from "./bbn"; + +export default { btc, bbn } as const; diff --git a/src/hocs/withAppState.tsx b/src/hocs/withAppState.tsx new file mode 100644 index 0000000..0b9e296 --- /dev/null +++ b/src/hocs/withAppState.tsx @@ -0,0 +1,18 @@ +/* eslint-disable @typescript-eslint/no-empty-object-type */ +import { ComponentType, memo, useMemo } from "react"; +import { useAppState } from "@/state/state"; +import type { Actions, State } from "@/state/state.d"; + +export const withAppState = + (stateMapper: (state: State & Actions) => IP) => + (Component: ComponentType

) => { + const Container = (props: OP) => { + const appState = useAppState(); + const outerProps = useMemo(() => stateMapper(appState), [appState, stateMapper]); + const PureComponent = memo(Component); + + return ; + }; + + return Container; + }; diff --git a/src/hooks/useChainConnector.ts b/src/hooks/useChainConnector.ts new file mode 100644 index 0000000..b0d0fcf --- /dev/null +++ b/src/hooks/useChainConnector.ts @@ -0,0 +1,7 @@ +import { type SupportedChains, useChainProviders } from "@/context/Chain.context"; + +export function useChainConnector(chainId: K) { + const connectors = useChainProviders(); + + return connectors?.[chainId] ?? null; +} diff --git a/src/hooks/usePersistState.ts b/src/hooks/usePersistState.ts new file mode 100644 index 0000000..bcab1a2 --- /dev/null +++ b/src/hooks/usePersistState.ts @@ -0,0 +1,24 @@ +"use client"; + +import { type SetStateAction, type Dispatch, useState, useEffect } from "react"; + +export function usePersistState(key: string, storage: Storage, initialState?: S): [S, Dispatch>] { + function getDefaultState() { + const defaultValue = typeof initialState === "function" ? (initialState as () => S)() : initialState; + const persistValue = storage.getItem(key); + const defaultState = persistValue ? (JSON.parse(persistValue) as S) : null; + + return (defaultState ?? defaultValue) as S; + } + + const [state, setState] = useState(getDefaultState); + + useEffect( + function updateLocalStorage() { + storage.setItem(key, JSON.stringify(state ?? "")); + }, + [key, storage, state], + ); + + return [state, setState]; +} diff --git a/src/index.tsx b/src/index.tsx index 3689fda..a495f17 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,11 @@ import "./index.css"; +export { useChainConnector } from "@/hooks/useChainConnector"; +export { useAppState } from "@/state/state"; +export { WalletProvider } from "@/widgets/WalletProvider"; +export * from "@/state/state.d"; + +// core-ui export * from "./components/Text"; export * from "./components/Heading"; export * from "./components/Button"; diff --git a/src/state/state.d.ts b/src/state/state.d.ts index e8a0498..2eb61fc 100644 --- a/src/state/state.d.ts +++ b/src/state/state.d.ts @@ -1,20 +1,33 @@ import type { IChain, IWalllet } from "@/core/types"; -type Step = "loading" | "acceptTermsOfService" | "selectChain" | "selectWallet" | "lockInscriptions"; +type Screen = { + type: T; + params?: Record; +}; + +type Screens = + | Screen<"LOADING"> + | Screen<"TERMS_OF_SERVICE"> + | Screen<"CHAINS"> + | Screen<"WALLETS"> + | Screen<"INSCRIPTIONS">; export interface State { visible: boolean; - loading: boolean; - step: Step; + screen: Screens; selectedWallets: Record; - visibleWallets: string; chains: Record; - displayTermsOfService?: () => void; +} + +export interface Actions { + open?: () => void; + close?: () => void; + displayLoading?: () => void; displayChains?: () => void; displayWallets?: (chain: string) => void; + displayInscriptions?: () => void; + displayTermsOfService?: () => void; selectWallet?: (chain: string, wallet: IWalllet) => void; + removeWallet?: (chain: string) => void; addChain?: (chain: IChain) => void; - setLoading?: (value: boolean) => void; - open?: () => void; - close?: () => void; } diff --git a/src/state/state.tsx b/src/state/state.tsx index d13d6cc..d5c7f95 100644 --- a/src/state/state.tsx +++ b/src/state/state.tsx @@ -1,73 +1,84 @@ -import { type PropsWithChildren, createContext, useCallback, useMemo, useState } from "react"; +import { type PropsWithChildren, createContext, useContext, useMemo, useState } from "react"; -import { type State } from "./state.d"; +import { Actions, type State } from "./state.d"; import { IChain, IWallet } from "@/core/types"; const defaultState: State = { - visible: true, - loading: true, - step: "selectChain", + visible: false, + screen: { type: "TERMS_OF_SERVICE" }, chains: {}, selectedWallets: {}, - visibleWallets: "", }; -const StateContext = createContext(defaultState); +const StateContext = createContext(defaultState); export function StateProvider({ children }: PropsWithChildren) { const [state, setState] = useState(defaultState); - const open = useCallback(() => { - setState((state) => ({ ...state, visible: true })); - }, []); + // useEffect(() => { + // console.log(state); + // }, [state]); - const close = useCallback(() => { - setState((state) => ({ ...state, visible: false })); - }, []); + const actions: Actions = useMemo( + () => ({ + open: () => { + setState(({ chains }) => ({ ...defaultState, chains, visible: true })); + }, - const displayTermsOfService = useCallback(() => { - setState((state) => ({ ...state, step: "acceptTermsOfService" })); - }, []); + close: () => { + setState((state) => ({ ...state, visible: false })); + }, - const displayChains = useCallback(() => { - setState((state) => ({ ...state, step: "selectChain", visibleWallets: "" })); - }, []); + displayLoading: () => { + setState((state) => ({ ...state, screen: { type: "LOADING" } })); + }, - const displayWallets = useCallback((chain: string) => { - setState((state) => ({ ...state, step: "selectWallet", visibleWallets: chain })); - }, []); + displayTermsOfService: () => { + setState((state) => ({ ...state, screen: { type: "TERMS_OF_SERVICE" } })); + }, - const selectWallet = useCallback((chain: string, wallet: IWallet) => { - setState((state) => ({ - ...state, - step: "lockInscriptions", - visibleWallets: "", - selectedWallets: { ...state.selectedWallets, [chain]: wallet }, - })); - }, []); + displayChains: () => { + setState((state) => ({ ...state, screen: { type: "CHAINS" } })); + }, + + displayWallets: (chain: string) => { + setState((state) => ({ ...state, screen: { type: "WALLETS", params: { chain } } })); + }, + + displayInscriptions: () => { + setState((state) => ({ ...state, screen: { type: "INSCRIPTIONS" } })); + }, - const addChain = useCallback((chainInfo: IChain) => { - setState((state) => ({ ...state, chains: { ...state.chains, [chainInfo.chain]: chainInfo } })); - }, []); + selectWallet: (chain: string, wallet: IWallet) => { + setState((state) => ({ + ...state, + selectedWallets: { ...state.selectedWallets, [chain]: wallet }, + })); + }, - const setLoading = useCallback((loading: boolean) => { - setState((state) => ({ ...state, loading })); - }, []); + removeWallet: (chain: string) => { + setState((state) => ({ + ...state, + selectedWallets: { ...state.selectedWallets, [chain]: null }, + })); + }, + + addChain: (chain: IChain) => { + setState((state) => ({ ...state, chains: { ...state.chains, [chain.id]: chain } })); + }, + }), + [], + ); const context = useMemo( () => ({ ...state, - displayTermsOfService, - displayChains, - displayWallets, - selectWallet, - addChain, - setLoading, - open, - close, + ...actions, }), - [state], + [state, actions], ); return {children}; } + +export const useAppState = () => useContext(StateContext); diff --git a/src/utils/wallet.ts b/src/utils/wallet.ts new file mode 100644 index 0000000..84a5d36 --- /dev/null +++ b/src/utils/wallet.ts @@ -0,0 +1,9 @@ +export const formatAddress = (str: string, symbols: number = 8) => { + if (str.length <= symbols) { + return str; + } else if (symbols === 0) { + return "..."; + } + + return `${str.slice(0, symbols / 2)}...${str.slice(-symbols / 2)}`; +}; diff --git a/src/widgets/ChainButton/index.tsx b/src/widgets/ChainButton/index.tsx index 02f19c1..19105be 100644 --- a/src/widgets/ChainButton/index.tsx +++ b/src/widgets/ChainButton/index.tsx @@ -9,22 +9,25 @@ interface ChainButtonProps extends PropsWithChildren { logo?: string | JSX.Element; title?: string | JSX.Element; alt?: string; + onClick?: () => void; } -export function ChainButton({ className, disabled, alt, logo, title, children }: ChainButtonProps) { +export function ChainButton({ className, disabled, alt, logo, title, children, onClick }: ChainButtonProps) { const avatar = typeof logo === "string" ? : {logo}; return ( -

+
{avatar} {title} @@ -45,7 +48,11 @@ export function ChainButton({ className, disabled, alt, logo, title, children }: )}
- {children &&
e.stopPropagation()}>{children}
} + {children && ( +
e.stopPropagation()}> + {children} +
+ )} ); } diff --git a/src/widgets/Chains/index.stories.ts b/src/widgets/Chains/index.stories.ts new file mode 100644 index 0000000..1654fc9 --- /dev/null +++ b/src/widgets/Chains/index.stories.ts @@ -0,0 +1,49 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { Chains } from "./index"; + +const meta: Meta = { + component: Chains, + tags: ["autodocs"], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + chains: [ + { + id: "BTC", + name: "Bitcoin", + icon: "/images/chains/bitcoin.png", + wallets: [ + { + id: "okx", + name: "OKX", + installed: true, + icon: "/images/wallets/okx.png", + docs: "", + provider: null, + account: null, + }, + ], + }, + { id: "BBN", name: "Babylon Chain", icon: "/images/chains/babylon.jpeg", wallets: [] }, + ], + selectedWallets: { + BTC: { + id: "okx", + name: "OKX", + installed: true, + icon: "/images/wallets/okx.png", + docs: "", + provider: null, + account: null, + }, + }, + className: "h-[600px]", + onSelectChain: console.log, + }, +}; diff --git a/src/widgets/Chains/index.tsx b/src/widgets/Chains/index.tsx new file mode 100644 index 0000000..49f8e62 --- /dev/null +++ b/src/widgets/Chains/index.tsx @@ -0,0 +1,81 @@ +import { twMerge } from "tailwind-merge"; +import { useMemo } from "react"; + +import { Button, DialogBody, DialogFooter, DialogHeader, Text } from "@/index"; + +import ConnectedWallet from "@/widgets/ConnectedWallet"; +import { ChainButton } from "@/widgets/ChainButton"; +import { withAppState } from "@/hocs/withAppState"; +import type { IChain, IWallet } from "@/core/types"; + +interface ChainsProps { + chains: IChain[]; + className?: string; + selectedWallets?: Record; + onClose?: () => void; + onSelectChain?: (chain: IChain) => void; +} + +export function Chains({ chains, selectedWallets = {}, className, onClose, onSelectChain }: ChainsProps) { + const countOfSelectedWallets = useMemo( + () => Object.values(selectedWallets).filter(Boolean).length, + [selectedWallets], + ); + const activeChains = useMemo(() => chains.filter((chain) => chain.wallets.length > 0), [chains]); + + return ( +
+ + Connect to both Bitcoin and Babylon Chain Wallets + + + + {activeChains.map((chain) => { + const selectedWallet = selectedWallets[chain.id]; + + return ( + void onSelectChain?.(chain)} + > + {selectedWallet && ( + + )} + + ); + })} + + + + + + +
+ ); +} + +interface OuterProps { + className?: string; + onClose?: () => void; +} + +export default withAppState((state) => ({ + chains: Object.values(state.chains), + selectedWallets: state.selectedWallets, + onSelectChain: (chain: IChain) => { + state.displayWallets?.(chain.id); + }, +}))(Chains); diff --git a/src/widgets/ConnectedWallet/index.tsx b/src/widgets/ConnectedWallet/index.tsx index 3a9e1b1..d614e40 100644 --- a/src/widgets/ConnectedWallet/index.tsx +++ b/src/widgets/ConnectedWallet/index.tsx @@ -2,32 +2,36 @@ import {} from "react"; import { twMerge } from "tailwind-merge"; import { Avatar, Text } from "@/index"; +import { withAppState } from "@/hocs/withAppState"; interface ConnectedWalletProps { className?: string; + chainId: string; logo: string; name: string; address: string; - onDisconnect?: () => void; + onDisconnect?: (chainId: string) => void; } -export function ConnectedWallet({ className, logo, name, address, onDisconnect }: ConnectedWalletProps) { +export function ConnectedWallet({ className, chainId, logo, name, address, onDisconnect }: ConnectedWalletProps) { return (
-
+
{name} - - {address} - + {Boolean(address) && ( + + {address} + + )}
- + toggleShowAgain(!value)} + /> +
); diff --git a/src/widgets/SelectChain/index.stories.ts b/src/widgets/SelectChain/index.stories.ts deleted file mode 100644 index 4022269..0000000 --- a/src/widgets/SelectChain/index.stories.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; - -import { SelectChain } from "./index"; - -const meta: Meta = { - component: SelectChain, - tags: ["autodocs"], -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - onClose: console.log, - className: "h-[600px]", - }, -}; diff --git a/src/widgets/SelectChain/index.tsx b/src/widgets/SelectChain/index.tsx deleted file mode 100644 index dd9915d..0000000 --- a/src/widgets/SelectChain/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { twMerge } from "tailwind-merge"; - -import { Button, DialogBody, DialogFooter, DialogHeader, Text } from "@/index"; -import { ChainButton } from "@/widgets/ChainButton"; -import { ConnectedWallet } from "@/widgets/ConnectedWallet"; - -interface WalletConnectProps { - className?: string; - onClose?: () => void; -} - -export function SelectChain({ className, onClose }: WalletConnectProps) { - return ( -
- - Connect to both Bitcoin and Babylon Chain Wallets - - - - - - - - - - - - - - -
- ); -} diff --git a/src/widgets/SelectWallet/index.stories.tsx b/src/widgets/SelectWallet/index.stories.tsx deleted file mode 100644 index 8f87875..0000000 --- a/src/widgets/SelectWallet/index.stories.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; - -import { Text } from "@/index"; -import { SelectWallet } from "./index"; -import { WalletButton } from "@/widgets/WalletButton"; - -const meta: Meta = { - component: SelectWallet, - tags: ["autodocs"], -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - className: "h-[600px]", - append: ( -
- More wallets with Tomo Connect - -
- ), - }, -}; diff --git a/src/widgets/SelectWallet/index.tsx b/src/widgets/SelectWallet/index.tsx deleted file mode 100644 index 73d8063..0000000 --- a/src/widgets/SelectWallet/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { twMerge } from "tailwind-merge"; - -import { Text, Button, DialogBody, DialogFooter, DialogHeader } from "@/index"; -import { WalletButton } from "@/widgets/WalletButton"; - -export interface Props { - className?: string; - onClose?: () => void; - append?: JSX.Element; -} - -export function SelectWallet({ className, append, onClose }: Props) { - return ( -
- - Connect a Bitcoin Wallet - - - -
- - - - - -
- - {append} -
- - - - -
- ); -} diff --git a/src/widgets/TermsOfService/index.tsx b/src/widgets/TermsOfService/index.tsx index 4c61a7d..e835eb1 100644 --- a/src/widgets/TermsOfService/index.tsx +++ b/src/widgets/TermsOfService/index.tsx @@ -1,14 +1,33 @@ +import { useCallback, useMemo, useState } from "react"; +import { twMerge } from "tailwind-merge"; import { Text, Checkbox, Button, DialogBody, DialogFooter, DialogHeader } from "@/index"; import { FieldControl } from "@/widgets/FieldControl"; -import { twMerge } from "tailwind-merge"; export interface Props { className?: string; onClose?: () => void; + onSubmit?: () => void; } -export function TermsOfService({ className, onClose }: Props) { +const defaultState = { + termsOfUse: false, + inscriptions: false, + staking: false, +} as const; + +export function TermsOfService({ className, onClose, onSubmit }: Props) { + const [state, setState] = useState(defaultState); + const valid = useMemo(() => Object.values(state).every((val) => val), [state]); + + const handleChange = useCallback( + (key: keyof typeof defaultState) => + (value: boolean = false) => { + setState((state) => ({ ...state, [key]: value })); + }, + [], + ); + return (
@@ -20,23 +39,25 @@ export function TermsOfService({ className, onClose }: Props) { label="I certify that I have read and accept the updated Terms of Use and Privacy Policy." className="mb-8" > - + - + - + - +
); diff --git a/src/widgets/WalletButton/index.stories.tsx b/src/widgets/WalletButton/index.stories.tsx index cdabfa1..a05b74b 100644 --- a/src/widgets/WalletButton/index.stories.tsx +++ b/src/widgets/WalletButton/index.stories.tsx @@ -15,5 +15,6 @@ export const Default: Story = { args: { name: "Binance Web3 Wallet", logo: "/images/wallets/binance.png", + label: "Installed", }, }; diff --git a/src/widgets/WalletButton/index.tsx b/src/widgets/WalletButton/index.tsx index f39d1b1..b650080 100644 --- a/src/widgets/WalletButton/index.tsx +++ b/src/widgets/WalletButton/index.tsx @@ -15,9 +15,9 @@ export function WalletButton({ className, disabled = false, name, logo, label, o return ( void; + onAccepTermsOfService?: () => void; + onToggleInscriptions?: (value: boolean, showAgain: boolean) => void; + onClose?: () => void; +} + +const SCREENS = { + TERMS_OF_SERVICE: ({ className, onClose, onAccepTermsOfService }: ScreenProps) => ( + + ), + CHAINS: ({ className, onClose }: ScreenProps) => , + WALLETS: ({ className, onClose, onSelectWallet }: ScreenProps) => ( + + ), + INSCRIPTIONS: ({ onToggleInscriptions }) => , +} as const; + +export function Screen(props: ScreenProps) { + const CurrentScreen = SCREENS[props.current.type] ?? SCREENS.CHAINS; + + return ; +} diff --git a/src/widgets/WalletProvider/components/WalletDialog.tsx b/src/widgets/WalletProvider/components/WalletDialog.tsx new file mode 100644 index 0000000..457f336 --- /dev/null +++ b/src/widgets/WalletProvider/components/WalletDialog.tsx @@ -0,0 +1,57 @@ +import { useCallback } from "react"; + +import { Dialog } from "@/index"; +import { useAppState } from "@/state/state"; +import { useChainProviders } from "@/context/Chain.context"; +import { useInscriptionProvider } from "@/context/Inscriptions.context"; +import type { IChain, IWallet } from "@/core/types"; + +import { Screen } from "./Screen"; + +export function WalletDialog() { + const { visible, screen, close, selectWallet, displayLoading, displayChains, displayInscriptions } = useAppState(); + const { showAgain, toggleShowAgain, toggleLockInscriptions } = useInscriptionProvider(); + const connectors = useChainProviders(); + + const handleSelectWallet = useCallback( + async (chain: IChain, wallet: IWallet) => { + displayLoading?.(); + + const connector = connectors[chain.id]; + const connectedWallet = await connector?.connect(wallet.id); + + if (connectedWallet) { + selectWallet?.(chain.id, connectedWallet); + } + + if (showAgain) { + displayInscriptions?.(); + } else { + displayChains?.(); + } + }, + [displayLoading, selectWallet, displayInscriptions, connectors, showAgain], + ); + + const handleToggleInscriptions = useCallback( + (lockInscriptions: boolean, showAgain: boolean) => { + toggleShowAgain?.(showAgain); + toggleLockInscriptions?.(lockInscriptions); + displayChains?.(); + }, + [toggleShowAgain, toggleLockInscriptions, displayChains], + ); + + return ( + + + + ); +} diff --git a/src/widgets/WalletProvider/index.stories.tsx b/src/widgets/WalletProvider/index.stories.tsx new file mode 100644 index 0000000..c25e27a --- /dev/null +++ b/src/widgets/WalletProvider/index.stories.tsx @@ -0,0 +1,42 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { WalletProvider } from "./index"; + +import { Button } from "@/components/Button"; +import { ScrollLocker } from "@/context/Dialog.context"; +import { useAppState } from "@/state/state"; +import { Network } from "@/core/types"; + +const meta: Meta = { + component: WalletProvider, + tags: ["autodocs"], +}; + +export default meta; + +type Story = StoryObj; + +const networkConfig = { + coinName: "Signet BTC", + coinSymbol: "sBTC", + networkName: "BTC signet", + mempoolApiUrl: "https://mempool.space/signet", + network: Network.SIGNET, +}; + +export const Default: Story = { + decorators: [ + (Story) => ( + + + + + + ), + ], + render: () => { + const { open } = useAppState(); + + return ; + }, +}; diff --git a/src/widgets/WalletProvider/index.tsx b/src/widgets/WalletProvider/index.tsx new file mode 100644 index 0000000..c80cb85 --- /dev/null +++ b/src/widgets/WalletProvider/index.tsx @@ -0,0 +1,26 @@ +import { type PropsWithChildren } from "react"; + +import { StateProvider } from "@/state/state"; +import { ChainProvider } from "@/context/Chain.context"; +import { InscriptionProvider } from "@/context/Inscriptions.context"; +import type { NetworkConfig } from "@/core/types"; + +import { WalletDialog } from "./components/WalletDialog"; + +interface WalletProviderProps { + context?: any; + config: NetworkConfig; +} + +export function WalletProvider({ children, config, context = window }: PropsWithChildren) { + return ( + + + + {children} + + + + + ); +} diff --git a/src/widgets/Wallets/index.stories.tsx b/src/widgets/Wallets/index.stories.tsx new file mode 100644 index 0000000..6033131 --- /dev/null +++ b/src/widgets/Wallets/index.stories.tsx @@ -0,0 +1,57 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { Text } from "@/index"; +import { Wallets } from "./index"; +import { WalletButton } from "@/widgets/WalletButton"; + +const meta: Meta = { + component: Wallets, + tags: ["autodocs"], +}; + +export default meta; + +type Story = StoryObj; + +const wallets = [ + { + id: "injectable", + name: "Binance (Browser)", + installed: true, + icon: "/images/wallets/binance.png", + docs: "", + provider: null, + account: null, + }, + { + id: "okx", + name: "OKX", + installed: true, + icon: "/images/wallets/okx.png", + docs: "", + provider: null, + account: null, + }, + { + id: "keystone", + name: "Keysone", + installed: true, + icon: "/images/wallets/keystone.svg", + docs: "", + provider: null, + account: null, + }, +]; + +export const Default: Story = { + args: { + className: "h-[600px]", + chain: { id: "BTC", name: "Bitcoin", icon: "/images/chains/bitcoin.png", wallets }, + append: ( +
+ More wallets with Tomo Connect + +
+ ), + }, +}; diff --git a/src/widgets/Wallets/index.tsx b/src/widgets/Wallets/index.tsx new file mode 100644 index 0000000..0aaa088 --- /dev/null +++ b/src/widgets/Wallets/index.tsx @@ -0,0 +1,80 @@ +import { twMerge } from "tailwind-merge"; +import { useMemo } from "react"; + +import { Text, Button, DialogBody, DialogFooter, DialogHeader } from "@/index"; + +import { WalletButton } from "@/widgets/WalletButton"; +import { withAppState } from "@/hocs/withAppState"; +import type { IChain, IWallet } from "@/core/types"; + +export interface WalletsProps { + chain: IChain; + className?: string; + append?: JSX.Element; + onClose?: () => void; + onSelectWallet?: (chain: IChain, wallet: IWallet) => void; + onBack?: () => void; +} + +export function Wallets({ chain, className, append, onClose, onBack, onSelectWallet }: WalletsProps) { + const countOfVisibleWallets = useMemo( + () => chain.wallets.filter((wallet) => wallet.id === "injectable" && !wallet.installed).length, + [chain], + ); + const injectableWallet = useMemo( + () => chain.wallets.find((wallet) => wallet.id === "injectable" && wallet.installed), + [chain], + ); + const wallets = useMemo(() => chain.wallets.filter((wallet) => wallet.id !== "injectable"), [chain]); + + return ( +
+ + Connect a {chain.name} Wallet + + + +
+ {injectableWallet && ( + onSelectWallet?.(chain, injectableWallet)} + /> + )} + + {wallets.map((wallet) => ( + onSelectWallet?.(chain, wallet)} + /> + ))} +
+ + {append} +
+ + + + +
+ ); +} + +interface OuterProps { + className?: string; + onClose?: () => void; + append?: JSX.Element; + onSelectWallet?: (chain: IChain, wallet: IWallet) => void; +} + +export default withAppState((state) => ({ + chain: state.chains?.[state.screen.params?.chain ?? ""], + onBack: () => void state.displayChains?.(), +}))(Wallets); diff --git a/src/widgets/Widget/index.stories.tsx b/src/widgets/Widget/index.stories.tsx deleted file mode 100644 index 9024572..0000000 --- a/src/widgets/Widget/index.stories.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; - -import { Widget } from "./index"; - -const meta: Meta = { - component: Widget, - tags: ["autodocs"], -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: {}, -}; diff --git a/src/widgets/Widget/index.tsx b/src/widgets/Widget/index.tsx deleted file mode 100644 index c0a9179..0000000 --- a/src/widgets/Widget/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Network } from "@/core/types"; -import { WalletConnector } from "@/core/WalletConnector"; -import btc from "@/core/wallets/btc"; -import { useEffect } from "react"; - -export function Widget() { - useEffect(() => { - WalletConnector.create( - btc, - { - coinName: "Signet BTC", - coinSymbol: "sBTC", - networkName: "BTC signet", - mempoolApiUrl: "https://mempool.space/signet", - network: Network.SIGNET, - }, - window.parent, - ) - .then((connector) => { - return connector.connect("okx"); - }) - .then(console.log); - }, []); - return ; -} From 865ffdfdd4e62ce28db18cbd000be4939908a6df Mon Sep 17 00:00:00 2001 From: David Totraev Date: Thu, 21 Nov 2024 17:04:14 +0500 Subject: [PATCH 2/3] feat: rename loader screen --- src/context/Chain.context.tsx | 10 +++++----- src/state/state.d.ts | 4 ++-- src/state/state.tsx | 8 ++------ src/widgets/WalletProvider/components/WalletDialog.tsx | 6 +++--- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/context/Chain.context.tsx b/src/context/Chain.context.tsx index 5861bc1..c89f9a6 100644 --- a/src/context/Chain.context.tsx +++ b/src/context/Chain.context.tsx @@ -28,10 +28,10 @@ const Context = createContext(defaultState); export function ChainProvider({ children, context, config }: PropsWithChildren) { const [connectors, setConnectors] = useState(defaultState); - const { addChain, displayLoading, displayTermsOfService } = useAppState(); + const { addChain, displayLoader, displayTermsOfService } = useAppState(); const init = useCallback(async () => { - displayLoading?.(); + displayLoader?.(); const metadataArr = Object.values(metadata); const connectorArr = await Promise.all(metadataArr.map((data) => WalletConnector.create(data, context, config))); @@ -40,9 +40,9 @@ export function ChainProvider({ children, context, config }: PropsWithChildren

{ - if (!displayLoading || !addChain || !setConnectors || !displayTermsOfService) return; + if (!displayLoader || !addChain || !setConnectors || !displayTermsOfService) return; - displayLoading(); + displayLoader(); init().then((connectors) => { setConnectors(connectors); @@ -53,7 +53,7 @@ export function ChainProvider({ children, context, config }: PropsWithChildren

{children}; } diff --git a/src/state/state.d.ts b/src/state/state.d.ts index 2eb61fc..85cba8a 100644 --- a/src/state/state.d.ts +++ b/src/state/state.d.ts @@ -6,7 +6,7 @@ type Screen = { }; type Screens = - | Screen<"LOADING"> + | Screen<"LOADER"> | Screen<"TERMS_OF_SERVICE"> | Screen<"CHAINS"> | Screen<"WALLETS"> @@ -22,7 +22,7 @@ export interface State { export interface Actions { open?: () => void; close?: () => void; - displayLoading?: () => void; + displayLoader?: () => void; displayChains?: () => void; displayWallets?: (chain: string) => void; displayInscriptions?: () => void; diff --git a/src/state/state.tsx b/src/state/state.tsx index d5c7f95..e0fa3a0 100644 --- a/src/state/state.tsx +++ b/src/state/state.tsx @@ -15,10 +15,6 @@ const StateContext = createContext(defaultState); export function StateProvider({ children }: PropsWithChildren) { const [state, setState] = useState(defaultState); - // useEffect(() => { - // console.log(state); - // }, [state]); - const actions: Actions = useMemo( () => ({ open: () => { @@ -29,8 +25,8 @@ export function StateProvider({ children }: PropsWithChildren) { setState((state) => ({ ...state, visible: false })); }, - displayLoading: () => { - setState((state) => ({ ...state, screen: { type: "LOADING" } })); + displayLoader: () => { + setState((state) => ({ ...state, screen: { type: "LOADER" } })); }, displayTermsOfService: () => { diff --git a/src/widgets/WalletProvider/components/WalletDialog.tsx b/src/widgets/WalletProvider/components/WalletDialog.tsx index 457f336..3158b82 100644 --- a/src/widgets/WalletProvider/components/WalletDialog.tsx +++ b/src/widgets/WalletProvider/components/WalletDialog.tsx @@ -9,13 +9,13 @@ import type { IChain, IWallet } from "@/core/types"; import { Screen } from "./Screen"; export function WalletDialog() { - const { visible, screen, close, selectWallet, displayLoading, displayChains, displayInscriptions } = useAppState(); + const { visible, screen, close, selectWallet, displayLoader, displayChains, displayInscriptions } = useAppState(); const { showAgain, toggleShowAgain, toggleLockInscriptions } = useInscriptionProvider(); const connectors = useChainProviders(); const handleSelectWallet = useCallback( async (chain: IChain, wallet: IWallet) => { - displayLoading?.(); + displayLoader?.(); const connector = connectors[chain.id]; const connectedWallet = await connector?.connect(wallet.id); @@ -30,7 +30,7 @@ export function WalletDialog() { displayChains?.(); } }, - [displayLoading, selectWallet, displayInscriptions, connectors, showAgain], + [displayLoader, selectWallet, displayInscriptions, connectors, showAgain], ); const handleToggleInscriptions = useCallback( From dfd0704e62b8d3fc3f4075d0dbdcdec07a7345c3 Mon Sep 17 00:00:00 2001 From: David Totraev Date: Thu, 21 Nov 2024 17:21:43 +0500 Subject: [PATCH 3/3] fix: review --- src/core/WalletConnector.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/WalletConnector.ts b/src/core/WalletConnector.ts index 11750db..dcbb929 100644 --- a/src/core/WalletConnector.ts +++ b/src/core/WalletConnector.ts @@ -32,7 +32,11 @@ export class WalletConnector implements I async connect(walletId: string) { const wallet = this.wallets.find((wallet) => wallet.id === walletId); - this._connectedWallet = (await wallet?.connect()) ?? null; + if (!wallet) { + throw new Error("Wallet not found"); + } + + this._connectedWallet = await wallet.connect(); return this.connectedWallet; }