diff --git a/src/api/neighborhoods/provider.tsx b/src/api/neighborhoods/provider.tsx index db6ea09..e5d0fe8 100644 --- a/src/api/neighborhoods/provider.tsx +++ b/src/api/neighborhoods/provider.tsx @@ -12,39 +12,78 @@ import { Tokens, } from "@liftedinit/many-js"; import { useAccountsStore } from "features/accounts"; -import { createContext, ReactNode, useMemo } from "react"; +import { + createContext, + ReactNode, + useContext, + useEffect, + useMemo, + useState, +} from "react"; import { useNeighborhoodStore } from "./store"; -export const NeighborhoodContext = createContext( - undefined -); +interface INeighborhoodContext { + query?: Network; + command?: Network; + services: Set; +} + +export const NeighborhoodContext = createContext({ + services: new Set(), +}); export function NeighborhoodProvider({ children }: { children: ReactNode }) { - const activeNeighborhood = useNeighborhoodStore( - (s) => s.neighborhoods[s.activeId] - ); - const activeAccount = useAccountsStore((s) => s.byId.get(s.activeId))!; - - const neighborhood = useMemo(() => { - const identity = activeAccount?.identity ?? new AnonymousIdentity(); - const network = new Network(activeNeighborhood.url, identity); - network.apply([ - Account, - Base, - Blockchain, - Compute, - Events, - IdStore, - KvStore, - Ledger, - Tokens, - ]); - return network; - }, [activeAccount, activeNeighborhood]); + const { url } = useNeighborhoodStore((s) => s.neighborhoods[s.activeId]); + const account = useAccountsStore((s) => s.byId.get(s.activeId))!; + const [services, setServices] = useState>(new Set()); + + const context = useMemo(() => { + const anonymous = new AnonymousIdentity(); + const identity = account?.identity ?? anonymous; + + const query = new Network(url, anonymous); + const command = new Network(url, identity); + [query, command].forEach((network) => + network.apply([ + Account, + Base, + Blockchain, + Compute, + Events, + IdStore, + KvStore, + Ledger, + Tokens, + ]) + ); + return { query, command, services }; + }, [account, url, services]); + + useEffect(() => { + async function updateServices() { + if (!context.query || !context.query.base) { + return; + } + const updated = new Set(); + try { + const { endpoints } = await context.query.base.endpoints(); + endpoints + .map((endpoint: string) => endpoint.split(".")[0]) + .forEach((service: string) => updated.add(service)); + } catch (error) { + console.error(`Couldn't update services: ${(error as Error).message}`); + } + setServices(updated); + } + updateServices(); + // eslint-disable-next-line + }, [url]); return ( - + {children} ); } + +export const useNeighborhoodContext = () => useContext(NeighborhoodContext); diff --git a/src/features/accounts/api/create-account.ts b/src/features/accounts/api/create-account.ts index bf44617..c211b37 100644 --- a/src/features/accounts/api/create-account.ts +++ b/src/features/accounts/api/create-account.ts @@ -3,8 +3,7 @@ import { AccountMultisigArgument, CreateAccountResponse, } from "@liftedinit/many-js"; -import { NeighborhoodContext } from "api/neighborhoods"; -import { useContext } from "react"; +import { useNeighborhoodContext } from "api/neighborhoods"; import { useMutation } from "react-query"; import { accountLedgerFeature, accountMultisigFeature } from "../types"; @@ -20,7 +19,7 @@ export type CreateAccountFormData = { }; export function useCreateAccount() { - const n = useContext(NeighborhoodContext); + const { command: n } = useNeighborhoodContext(); return useMutation( async (vars: CreateAccountFormData) => { const name = vars.accountSettings.name; diff --git a/src/features/accounts/api/get-account-info.ts b/src/features/accounts/api/get-account-info.ts index 6e98eef..22d7bce 100644 --- a/src/features/accounts/api/get-account-info.ts +++ b/src/features/accounts/api/get-account-info.ts @@ -3,13 +3,12 @@ import { AccountRole, GetAccountInfoResponse, } from "@liftedinit/many-js"; -import { NeighborhoodContext } from "api/neighborhoods"; -import { useContext } from "react"; +import { useNeighborhoodContext } from "api/neighborhoods"; import { useQuery } from "react-query"; import { useAccountsStore } from "../stores"; export function useGetAccountInfo(accountAddress?: string) { - const n = useContext(NeighborhoodContext); + const { query: n } = useNeighborhoodContext(); const activeIdentity = useAccountsStore((s) => s.byId.get(s.activeId)); const address = activeIdentity?.address; diff --git a/src/features/accounts/api/get-webauthn-credential.ts b/src/features/accounts/api/get-webauthn-credential.ts index e12c9b6..69c6d2e 100644 --- a/src/features/accounts/api/get-webauthn-credential.ts +++ b/src/features/accounts/api/get-webauthn-credential.ts @@ -1,5 +1,4 @@ -import { NeighborhoodContext } from "api/neighborhoods"; -import { useContext } from "react"; +import { useNeighborhoodContext } from "api/neighborhoods"; import { useMutation } from "react-query"; import { RecoverOptions } from "../types"; @@ -7,13 +6,13 @@ import { RecoverOptions } from "../types"; * get webauthn account data from k-v store during import/recovery */ export function useGetWebauthnCredential() { - const queryNetwork = useContext(NeighborhoodContext); + const { query } = useNeighborhoodContext(); return useMutation( async ({ getFrom, value }: { getFrom: RecoverOptions; value: string }) => { const getFn = getFrom === RecoverOptions.phrase - ? queryNetwork?.idStore.getFromRecallPhrase - : queryNetwork?.idStore.getFromAddress; + ? query?.idStore.getFromRecallPhrase + : query?.idStore.getFromAddress; const res = await getFn(value); return res; } diff --git a/src/features/accounts/api/save-webauthn-credential.ts b/src/features/accounts/api/save-webauthn-credential.ts index 9b90796..df7702c 100644 --- a/src/features/accounts/api/save-webauthn-credential.ts +++ b/src/features/accounts/api/save-webauthn-credential.ts @@ -1,10 +1,9 @@ import { IdStore, Network, WebAuthnIdentity } from "@liftedinit/many-js"; -import { NeighborhoodContext } from "api/neighborhoods"; -import { useContext } from "react"; +import { useNeighborhoodContext } from "api/neighborhoods"; import { useMutation } from "react-query"; export function useSaveWebauthnCredential() { - const network = useContext(NeighborhoodContext); + const { command } = useNeighborhoodContext(); return useMutation< { phrase: string }, Error, @@ -15,7 +14,7 @@ export function useSaveWebauthnCredential() { identity: WebAuthnIdentity; } >(async ({ address, credentialId, cosePublicKey, identity }) => { - const n = new Network(network?.url ?? "", identity); + const n = new Network(command?.url ?? "", identity); n.apply([IdStore]); const res = await n?.idStore.store(address, credentialId, cosePublicKey); return res; diff --git a/src/features/accounts/components/accounts-menu/accounts-menu.tsx b/src/features/accounts/components/accounts-menu/accounts-menu.tsx index 6341c2c..14cddf4 100644 --- a/src/features/accounts/components/accounts-menu/accounts-menu.tsx +++ b/src/features/accounts/components/accounts-menu/accounts-menu.tsx @@ -1,37 +1,39 @@ -import React from "react"; import { AnonymousIdentity, ANON_IDENTITY, WebAuthnIdentity, } from "@liftedinit/many-js"; -import { useAccountsStore } from "features/accounts"; import { + AddressText, Box, Button, + ChevronDownIcon, Circle, + EditIcon, Flex, HStack, Icon, IconButton, Menu, MenuButton, - MenuList, + MenuDivider, MenuItem, + MenuList, MenuOptionGroup, - MenuDivider, SimpleGrid, Text, + UsbIcon, useDisclosure, - VStack, - AddressText, - ChevronDownIcon, - EditIcon, UserIcon, - UsbIcon, + useToast, + VStack, } from "@liftedinit/ui"; +import { useNeighborhoodContext } from "api/neighborhoods"; +import { useAccountsStore } from "features/accounts"; +import { useEffect, useState } from "react"; +import { Account, AccountId } from "../../types"; import { AddAccountModal } from "./add-account-modal"; import { EditAccountModal } from "./edit-account-modal"; -import { Account, AccountId } from "../../types"; export type AccountItemWithIdDisplayStrings = [ AccountId, @@ -66,7 +68,25 @@ export function AccountsMenu() { }) ); - const [editAccount, setEditAccount] = React.useState< + const toast = useToast(); + const { services } = useNeighborhoodContext(); + useEffect(() => { + (async () => { + const isWebAuthnIdentity = + activeAccount?.identity instanceof WebAuthnIdentity; + if (isWebAuthnIdentity && !services.has("idstore")) { + setActiveId(0); // reset to Anonymous + toast({ + status: "warning", + title: "Unsupported Identity", + description: + "Selected Neighborhood does not support Hardware Authenticators", + }); + } + })(); + }, [activeAccount, services, setActiveId, toast]); + + const [editAccount, setEditAccount] = useState< [number, Account] | undefined >(); diff --git a/src/features/accounts/components/accounts-menu/add-account-modal.tsx b/src/features/accounts/components/accounts-menu/add-account-modal.tsx index b504bc9..b3a6b02 100644 --- a/src/features/accounts/components/accounts-menu/add-account-modal.tsx +++ b/src/features/accounts/components/accounts-menu/add-account-modal.tsx @@ -1,21 +1,22 @@ -import React from "react"; import { Box, Button, + ChevronRightIcon, Flex, + Modal, ScaleFade, Tab, - Tabs, TabList, + Tabs, VStack, - ChevronRightIcon, - Modal, } from "@liftedinit/ui"; -import { SeedWords } from "./seed-words"; +import { useNeighborhoodContext } from "api/neighborhoods"; +import { Dispatch, ReactNode, useEffect, useState } from "react"; +import { SocialLogin } from "../social-login"; import { CreateAccount } from "./create-account"; -import { PemFile } from "./pem-file"; import { HardwareAuthenticator } from "./hardware-authenticator"; -import { SocialLogin } from "../social-login"; +import { PemFile } from "./pem-file"; +import { SeedWords } from "./seed-words"; export enum AddAccountMethodTypes { createSeed, @@ -29,7 +30,7 @@ export enum AddAccountMethodTypes { export const toastTitle = "Add Account"; export type AddMethodState = AddAccountMethodTypes | ""; export type AddAccountMethodProps = { - setAddMethod: React.Dispatch; + setAddMethod: Dispatch; onSuccess: () => void; }; @@ -40,9 +41,8 @@ export function AddAccountModal({ isOpen: boolean; onClose: () => void; }) { - const [addMethod, setAddMethod] = React.useState(""); - const [showDefaultFooter, setShowDefaultFooter] = - React.useState(true); + const [addMethod, setAddMethod] = useState(""); + const [showDefaultFooter, setShowDefaultFooter] = useState(true); function onSuccess() { onClose(); @@ -50,7 +50,7 @@ export function AddAccountModal({ const hasAddMethod = typeof addMethod === "number"; - React.useEffect(() => { + useEffect(() => { setAddMethod(""); }, [isOpen]); @@ -86,13 +86,13 @@ export function AddAccountModal({ )} {(addMethod === AddAccountMethodTypes.importAuthenticator || addMethod === AddAccountMethodTypes.createAuthenticator) && ( - - )} + + )} {showDefaultFooter && ( @@ -120,7 +120,7 @@ function AddAccountMethods({ onAddMethodClick: (method: AddAccountMethodTypes) => void; onSuccess: () => void; }) { - const [activeTab, setActiveTab] = React.useState(TabNames.create); + const [activeTab, setActiveTab] = useState(TabNames.create); const tabs = ["Create New", "Import"]; return ( <> @@ -164,6 +164,7 @@ const createCards = [ label: "Hardware Authenticator", title: "create new with hardware authenticator", onClickArg: AddAccountMethodTypes.createAuthenticator, + requires: "idstore", }, ]; @@ -172,18 +173,21 @@ function CreateAccountOptions({ }: { onAddMethodClick: (method: AddAccountMethodTypes) => void; }) { + const { services } = useNeighborhoodContext(); return ( - {createCards.map((c, idx) => { - return ( - onAddMethodClick(c.onClickArg)} - /> - ); - })} + {createCards + .filter((c) => !c.requires || services.has(c.requires)) + .map((c, idx) => { + return ( + onAddMethodClick(c.onClickArg)} + /> + ); + })} ); } @@ -203,6 +207,7 @@ const importCards = [ label: "Hardware Authenticator", title: "import with hardware authenticator", onClickArg: AddAccountMethodTypes.importAuthenticator, + requires: "idstore", }, ]; function ImportAcountOptions({ @@ -210,18 +215,22 @@ function ImportAcountOptions({ }: { onAddMethodClick: (method: AddAccountMethodTypes) => void; }) { + const { services } = useNeighborhoodContext(); + return ( - {importCards.map((c, idx) => { - return ( - onAddMethodClick(c.onClickArg)} - /> - ); - })} + {importCards + .filter((c) => !c.requires || services.has(c.requires)) + .map((c, idx) => { + return ( + onAddMethodClick(c.onClickArg)} + /> + ); + })} ); } @@ -231,7 +240,7 @@ function AddAccountCard({ title, onClick, }: { - label: string | React.ReactNode; + label: string | ReactNode; title?: string; onClick: () => void; }) { diff --git a/src/pages/services/blocks/blocks.tsx b/src/pages/services/blocks/blocks.tsx index 1be1614..3e3f7ec 100644 --- a/src/pages/services/blocks/blocks.tsx +++ b/src/pages/services/blocks/blocks.tsx @@ -6,14 +6,13 @@ import { Box, Heading, } from "@liftedinit/ui"; -import { NeighborhoodContext } from "api/neighborhoods"; +import { useNeighborhoodContext } from "api/neighborhoods"; import { useBlockchainInfoQuery } from "api/services"; -import { useContext } from "react"; import { replacer } from "shared"; import { Breadcrumbs } from "../breadcrumbs"; export function Blocks() { - const neighborhood = useContext(NeighborhoodContext); + const { query: neighborhood } = useNeighborhoodContext(); const { data } = useBlockchainInfoQuery(neighborhood); return ( diff --git a/src/pages/services/compute/close-deployment-dialog.tsx b/src/pages/services/compute/close-deployment-dialog.tsx index aeee563..a059c09 100644 --- a/src/pages/services/compute/close-deployment-dialog.tsx +++ b/src/pages/services/compute/close-deployment-dialog.tsx @@ -8,9 +8,9 @@ import { useDisclosure, useToast, } from "@liftedinit/ui"; -import { NeighborhoodContext } from "api/neighborhoods"; +import { useNeighborhoodContext } from "api/neighborhoods"; import { useCloseDeployment } from "api/services"; -import { useContext, useRef } from "react"; +import { useRef } from "react"; export function CloseDeploymentDialog({ dseq, @@ -19,7 +19,7 @@ export function CloseDeploymentDialog({ dseq: number; children: (onOpen: () => void) => void; }) { - const neighborhood = useContext(NeighborhoodContext); + const { command: neighborhood } = useNeighborhoodContext(); const toast = useToast(); const { mutate: doCloseDeployment, diff --git a/src/pages/services/compute/compute.tsx b/src/pages/services/compute/compute.tsx index 04ea796..dac5f02 100644 --- a/src/pages/services/compute/compute.tsx +++ b/src/pages/services/compute/compute.tsx @@ -9,16 +9,15 @@ import { useDisclosure, } from "@liftedinit/ui"; import { useAccountsStore } from "features/accounts"; +import { useNeighborhoodContext } from "../../../api/neighborhoods"; +import { useListDeployments } from "../../../api/services"; import { Breadcrumbs } from "../breadcrumbs"; -import { DeploymentTable } from "./deployment-table"; import { CreateDeploymentModal } from "./create-deployment-modal"; -import { useContext } from "react"; -import { NeighborhoodContext } from "../../../api/neighborhoods"; -import { useListDeployments } from "../../../api/services"; +import { DeploymentTable } from "./deployment-table"; export function Compute() { const account = useAccountsStore((s) => s.byId.get(s.activeId)); - const neighborhood = useContext(NeighborhoodContext); + const { query: neighborhood } = useNeighborhoodContext(); // const [keyvalue, setKeyvalue] = useState({ key: "", value: "" }); diff --git a/src/pages/services/compute/create-deployment-modal.tsx b/src/pages/services/compute/create-deployment-modal.tsx index 1b23eba..e0a6a52 100644 --- a/src/pages/services/compute/create-deployment-modal.tsx +++ b/src/pages/services/compute/create-deployment-modal.tsx @@ -13,9 +13,8 @@ import { Select, useToast, } from "@liftedinit/ui"; -import { NeighborhoodContext } from "api/neighborhoods"; +import { useNeighborhoodContext } from "api/neighborhoods"; import { useCreateDeployment } from "api/services"; -import { useContext } from "react"; import { Controller, SubmitHandler, useForm } from "react-hook-form"; const Regions = ["us-east", "us-west"]; @@ -53,7 +52,7 @@ export function CreateDeploymentModal({ isOpen: boolean; onClose: () => void; }) { - const neighborhood = useContext(NeighborhoodContext); + const { command: neighborhood } = useNeighborhoodContext(); const { mutate: doCreateDeployment, error, diff --git a/src/pages/services/data/data.tsx b/src/pages/services/data/data.tsx index 89381a2..1bdf48c 100644 --- a/src/pages/services/data/data.tsx +++ b/src/pages/services/data/data.tsx @@ -1,26 +1,26 @@ -import {ANON_IDENTITY} from "@liftedinit/many-js"; -import {Box, Button, Flex, Heading, useDisclosure} from "@liftedinit/ui"; -import {NeighborhoodContext} from "api/neighborhoods"; +import { ANON_IDENTITY } from "@liftedinit/many-js"; +import { Box, Button, Flex, Heading, useDisclosure } from "@liftedinit/ui"; +import { useNeighborhoodContext } from "api/neighborhoods"; import { combineData, useGetValues, useListKeys, useQueryValues, } from "api/services"; -import {useAccountsStore} from "features/accounts"; -import {useContext, useState} from "react"; -import {Breadcrumbs} from "../breadcrumbs"; -import {DataTable} from "./data-table"; -import {PutValueModal} from "./put-value-modal"; +import { useAccountsStore } from "features/accounts"; +import { useState } from "react"; +import { Breadcrumbs } from "../breadcrumbs"; +import { DataTable } from "./data-table"; +import { PutValueModal } from "./put-value-modal"; export function Data() { const account = useAccountsStore((s) => s.byId.get(s.activeId)); - const neighborhood = useContext(NeighborhoodContext); - const {isOpen, onOpen, onClose} = useDisclosure(); - const [keyvalue, setKeyvalue] = useState({key: "", value: ""}); + const { query: neighborhood } = useNeighborhoodContext(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const [keyvalue, setKeyvalue] = useState({ key: "", value: "" }); const list = useListKeys(neighborhood, account?.address.toString()); - const all_keys = list.flatMap(item => item.data?.keys || []); + const all_keys = list.flatMap((item) => item.data?.keys || []); const values = useGetValues(neighborhood, all_keys); const queries = useQueryValues(neighborhood, all_keys); const data = combineData([...values, ...queries]); @@ -28,7 +28,7 @@ export function Data() { return ( Data - +