diff --git a/apps/dashboard-for-dapps/src/routeTree.gen.ts b/apps/dashboard-for-dapps/src/routeTree.gen.ts
index fd01c2cb6e..97c9187969 100644
--- a/apps/dashboard-for-dapps/src/routeTree.gen.ts
+++ b/apps/dashboard-for-dapps/src/routeTree.gen.ts
@@ -11,10 +11,17 @@
// Import Routes
import { Route as rootRoute } from "./routes/__root";
+import { Route as CredentialsImport } from "./routes/credentials";
import { Route as IndexImport } from "./routes/index";
// Create/Update Routes
+const CredentialsRoute = CredentialsImport.update({
+ id: "/credentials",
+ path: "/credentials",
+ getParentRoute: () => rootRoute,
+} as any);
+
const IndexRoute = IndexImport.update({
id: "/",
path: "/",
@@ -32,6 +39,13 @@ declare module "@tanstack/react-router" {
preLoaderRoute: typeof IndexImport;
parentRoute: typeof rootRoute;
};
+ "/credentials": {
+ id: "/credentials";
+ path: "/credentials";
+ fullPath: "/credentials";
+ preLoaderRoute: typeof CredentialsImport;
+ parentRoute: typeof rootRoute;
+ };
}
}
@@ -39,32 +53,37 @@ declare module "@tanstack/react-router" {
export interface FileRoutesByFullPath {
"/": typeof IndexRoute;
+ "/credentials": typeof CredentialsRoute;
}
export interface FileRoutesByTo {
"/": typeof IndexRoute;
+ "/credentials": typeof CredentialsRoute;
}
export interface FileRoutesById {
__root__: typeof rootRoute;
"/": typeof IndexRoute;
+ "/credentials": typeof CredentialsRoute;
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath;
- fullPaths: "/";
+ fullPaths: "/" | "/credentials";
fileRoutesByTo: FileRoutesByTo;
- to: "/";
- id: "__root__" | "/";
+ to: "/" | "/credentials";
+ id: "__root__" | "/" | "/credentials";
fileRoutesById: FileRoutesById;
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute;
+ CredentialsRoute: typeof CredentialsRoute;
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
+ CredentialsRoute: CredentialsRoute,
};
export const routeTree = rootRoute
@@ -77,11 +96,15 @@ export const routeTree = rootRoute
"__root__": {
"filePath": "__root.tsx",
"children": [
- "/"
+ "/",
+ "/credentials"
]
},
"/": {
"filePath": "index.tsx"
+ },
+ "/credentials": {
+ "filePath": "credentials.tsx"
}
}
}
diff --git a/apps/dashboard-for-dapps/src/routes/credentials.tsx b/apps/dashboard-for-dapps/src/routes/credentials.tsx
new file mode 100644
index 0000000000..b91451444f
--- /dev/null
+++ b/apps/dashboard-for-dapps/src/routes/credentials.tsx
@@ -0,0 +1,408 @@
+import {
+ Center,
+ Container,
+ HStack,
+ Image,
+ List,
+ Spinner,
+ Stack,
+ Text,
+ chakra,
+} from "@chakra-ui/react";
+import type { idOSCredential } from "@idos-network/idos-sdk";
+import {
+ Button,
+ DrawerActionTrigger,
+ DrawerBackdrop,
+ DrawerBody,
+ DrawerCloseTrigger,
+ DrawerContent,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerRoot,
+ DrawerTitle,
+ RefreshButton,
+ SearchField,
+} from "@idos-network/ui-kit";
+import { DataListItem, DataListRoot, EmptyState } from "@idos-network/ui-kit";
+import { useQuery } from "@tanstack/react-query";
+import { createFileRoute, useNavigate } from "@tanstack/react-router";
+import { useDebounce, useToggle } from "@uidotdev/usehooks";
+import { matchSorter } from "match-sorter";
+import { useMemo, useState } from "react";
+
+import { SecretKeyPrompt } from "@/components/secret-key-prompt";
+import { useSecretKey } from "@/hooks";
+import { useIdOS } from "@/idOS.provider";
+import { changeCase, decrypt, openImageInNewTab } from "@/utils";
+
+export const Route = createFileRoute("/credentials")({
+ component: Credentials,
+ validateSearch: (search): { filter?: string } => {
+ return {
+ filter: (search.filter as string) ?? undefined,
+ };
+ },
+});
+
+export const useFetchAllCredentials = ({
+ enabled,
+ secretKey,
+}: { enabled: boolean; secretKey: string }) => {
+ const idOS = useIdOS();
+
+ return useQuery({
+ queryKey: ["credentials", secretKey],
+ queryFn: async () => {
+ const credentials = await idOS.data.listAllCredentials();
+ const promiseList = credentials.map(async (credential) => {
+ const fullCredential = (await idOS.data.get(
+ "credentials",
+ credential.id,
+ false,
+ )) as idOSCredential;
+
+ if (!fullCredential) return null;
+
+ try {
+ const decrypted = decrypt(
+ fullCredential.content,
+ fullCredential.encryption_public_key,
+ secretKey,
+ );
+
+ return {
+ ...fullCredential,
+ content: decrypted,
+ };
+ } catch (error) {
+ console.error(`Failed to decrypt/parse credential ${credential.id}:`, error);
+ return {
+ ...fullCredential,
+ content: null,
+ };
+ }
+ });
+
+ const stressPromiseList = Array.from({ length: 300 }, () => promiseList).flat();
+ const results = await Promise.all(stressPromiseList);
+ return results.filter((credential): credential is idOSCredential => credential !== null);
+ },
+ enabled,
+ });
+};
+
+function CredentialDetails({
+ credential,
+ open,
+ toggle,
+}: { credential: idOSCredential | null; open: boolean; toggle: (value?: boolean) => void }) {
+ if (!credential) return null;
+
+ const content = JSON.parse(credential.content);
+
+ const subject = Object.entries(content.credentialSubject).filter(
+ ([key]) => !["emails", "wallets"].includes(key) && !key.endsWith("_file"),
+ ) as [string, string][];
+
+ const emails = content.credentialSubject.emails;
+ const wallets = content.credentialSubject.wallets;
+ const files = (
+ Object.entries(content.credentialSubject).filter(([key]) => key.endsWith("_file")) as [
+ string,
+ string,
+ ][]
+ ).map(([key, value]) => [key, value]);
+
+ return (
+ {
+ toggle(false);
+ }}
+ >
+
+
+
+ Credential details
+
+
+
+
+ {subject.map(([key, value]) => (
+
+ ))}
+
+
+ {emails.map(({ address, verified }: { address: string; verified: boolean }) => (
+
+ {address}
+ {verified ? " (verified)" : ""}
+
+ ))}
+
+ }
+ />
+
+ {wallets.map(
+ ({
+ address,
+ currency,
+ }: { address: string; currency: string; verified: boolean }) => (
+
+ {address} ({currency})
+
+ ),
+ )}
+
+ }
+ />
+ {files.length > 0 ? (
+
+ {files.map(([key, value]) => (
+ openImageInNewTab(value)}
+ >
+
+
+
+
+ ))}
+
+ }
+ />
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function SearchResults({ results }: { results: idOSCredential[] }) {
+ const [credential, setCredential] = useState(null);
+ const [openSecretKeyPrompt, toggleSecretKeyPrompt] = useToggle();
+ const [openCredentialDetails, toggleCredentialDetails] = useToggle();
+ const [secretKey, setSecretKey] = useSecretKey();
+
+ if (!results.length) {
+ return ;
+ }
+
+ const handleOpenCredentialDetails = async (credential: idOSCredential) => {
+ setCredential(credential);
+
+ if (!secretKey) {
+ toggleSecretKeyPrompt();
+ return;
+ }
+
+ toggleCredentialDetails();
+ };
+
+ const onKeySubmit = async (secretKey: string) => {
+ setSecretKey(secretKey);
+ toggleCredentialDetails();
+ };
+
+ return (
+ <>
+ {results.map((credential) => {
+ const publicFields = Object.entries(JSON.parse(credential.public_notes)) as [
+ string,
+ string,
+ ][];
+
+ return (
+
+
+ {publicFields.map(([key, value]) => (
+
+ ))}
+
+
+
+ );
+ })}
+
+
+
+ >
+ );
+}
+
+function Credentials() {
+ const navigate = useNavigate({ from: Route.fullPath });
+ const { filter = "" } = Route.useSearch();
+ const debouncedSearchTerm = useDebounce(filter, 300);
+ const [secretKey] = useSecretKey();
+ const credentials = useFetchAllCredentials({ enabled: !!secretKey, secretKey });
+
+ const handleSearchChange = (e: React.ChangeEvent) => {
+ const search = e.target.value;
+ navigate({
+ search: {
+ filter: search,
+ },
+ });
+ };
+
+ const results = useMemo(() => {
+ if (!credentials.data) return [];
+ if (!debouncedSearchTerm) return credentials.data;
+
+ return matchSorter(credentials.data, debouncedSearchTerm, {
+ keys: ["public_notes", "content"],
+ threshold: matchSorter.rankings.CONTAINS,
+ });
+ }, [debouncedSearchTerm, credentials.data]);
+
+ return (
+
+
+ {credentials.isFetching ? (
+
+
+ Fetching credentials...
+
+ ) : (
+
+
+
+ navigate({
+ search: {},
+ })
+ }
+ />
+
+ credentials.refetch()}
+ />
+
+
+
+ )}
+
+
+ );
+}