Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(isle): view credential details #594

Merged
merged 8 commits into from
Mar 6, 2025
140 changes: 103 additions & 37 deletions apps/isle/src/features/permissions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { DeleteIcon } from "@/components/icons/delete";
import { ViewIcon } from "@/components/icons/view";
import { BreadcrumbLink, BreadcrumbRoot, Button } from "@/components/ui";
import { useIsleStore } from "@/store";
import type { idOSCredential } from "@idos-network/core";
import { RequestPermission } from "./request-permission";

interface GrantRevocationProps {
Expand Down Expand Up @@ -189,7 +190,7 @@ function GrantRevocation({ grant, onDismiss, onSuccess }: GrantRevocationProps)
);
}

function Breadcrumbs() {
function Breadcrumbs({ goHome }: { goHome: () => void }) {
return (
<BreadcrumbRoot
size="lg"
Expand All @@ -209,6 +210,7 @@ function Breadcrumbs() {
_dark: "neutral.50",
_light: "neutral.950",
}}
onClick={goHome}
>
Permissions
</BreadcrumbLink>
Expand All @@ -227,43 +229,87 @@ function Breadcrumbs() {
);
}

//@ts-ignore
function CredentialDetails() {
return (
<Stack gap="6">
<Breadcrumbs />
<Stack gap="3">
<Flex justifyContent="space-between">
<Flex alignItems="center" gap="2.5">
<Image />
<Text fontWeight="semibold" color={{ _dark: "neutral.50", _light: "neutral.950" }}>
Common
</Text>
function CredentialDetails({ goHome, onRevoke }: { goHome: () => void; onRevoke: () => void }) {
const node = useIsleStore((state) => state.node);
const [credential, setCredential] = useState<idOSCredential | null>(null);
const [status, setStatus] = useState<"idle" | "pending" | "success" | "error">("idle");

useEffect(() => {
if (!node) return;
node.on("update-view-credential-details-status", (data) => {
setStatus(data.status);
if (data.status === "success") {
setCredential(data.credential ?? null);
}
});
}, [node]);

if (status === "pending") {
return (
<Center flexDir="column" gap="6">
<Spinner size="xl" />
</Center>
);
}

if (status === "error") {
return (
<Center flexDir="column" gap="6">
<Heading fontSize="lg" fontWeight="semibold" textAlign="center">
Error while viewing credential details.
</Heading>
</Center>
);
}

if (status === "success") {
return (
<Stack gap="6" w="full" maxW="full" overflow="auto">
<Breadcrumbs goHome={goHome} />
<Stack gap="3">
<Flex justifyContent="space-between">
<Flex alignItems="center" gap="2.5">
<Image />
<Text fontWeight="semibold" color={{ _dark: "neutral.50", _light: "neutral.950" }}>
Common
</Text>
</Flex>
<AuthorizedIcon color="aquamarine.400" />
</Flex>
<AuthorizedIcon color="aquamarine.400" />
</Flex>
<CredentialContent />
<Flex maxW="324px" maxH="340px" overflow="auto">
<CredentialContent content={credential?.content} />
</Flex>
</Stack>
<Button onClick={onRevoke}>
Revoke Access
<Icon w={5} h={5} color="neutral.950">
<DeleteIcon />
</Icon>
</Button>
</Stack>
<Button>
Revoke Access
<Icon w={5} h={5} color="neutral.950">
<DeleteIcon />
</Icon>
</Button>
</Stack>
);
);
}
}

function CredentialContent() {
function CredentialContent({ content }: { content: string | undefined }) {
// @todo: decide which data to show
const parsedContent: Record<string, string> = content ? JSON.parse(content) : {};
return (
<Stack bg={{ _dark: "neutral.800", _light: "neutral.200" }} borderRadius="xl" gap="0">
{Object.entries({ name: "John" }).map(([key, value], index) => (
<Stack
bg={{ _dark: "neutral.800", _light: "neutral.200" }}
borderRadius="xl"
gap="0"
w="full"
maxW="full"
overflow="auto"
>
{Object.entries(parsedContent).map(([key, value], index) => (
<Flex
key={key}
py="3.5"
px="4"
gap="5"
borderColor={{ _dark: "neutral.700", _light: "neutral.300" }}
borderBottomWidth={index === Object.keys({}).length - 1 ? 0 : 1}
borderBottomWidth={index === Object.keys(parsedContent).length - 1 ? 0 : 1}
borderStyle="solid"
>
<Text
Expand All @@ -280,7 +326,7 @@ function CredentialContent() {
fontWeight="medium"
color={{ _dark: "neutral.50", _light: "neutral.950" }}
>
{value}
{JSON.stringify(value)}
</Text>
</Flex>
))}
Expand All @@ -301,12 +347,14 @@ interface AccessGrantWithGrantee {
dataId: string;
type: string;
grantee: GranteeInfo;
mode: "view" | "revoke";
lockedUntil: number;
originalCredentialId: string;
}

export function Permissions() {
const accessGrants = useIsleStore((state) => state.accessGrants);
const [grantToRevoke, setGrantToRevoke] = useState<AccessGrantWithGrantee | null>(null);
const [grant, setGrant] = useState<AccessGrantWithGrantee | null>(null);
const [permissionStatus, setPermissionStatus] = useState<
"idle" | "request-permission" | "pending" | "success" | "error"
>("idle");
Expand Down Expand Up @@ -339,12 +387,20 @@ export function Permissions() {
return Array.from(accessGrants?.entries() ?? []);
}, [accessGrants]);

if (grantToRevoke) {
if (grant?.mode === "view") {
return (
<CredentialDetails
onRevoke={() => setGrant({ ...grant, mode: "revoke" })}
goHome={() => setGrant(null)}
/>
);
}
if (grant?.mode === "revoke") {
return (
<GrantRevocation
grant={grantToRevoke}
onSuccess={() => setGrantToRevoke(null)}
onDismiss={() => setGrantToRevoke(null)}
grant={grant}
onSuccess={() => setGrant(null)}
onDismiss={() => setGrant(null)}
/>
);
}
Expand Down Expand Up @@ -468,7 +524,16 @@ export function Permissions() {
size="xs"
colorPalette="green"
rounded="full"
onClick={() => {}}
onClick={() => {
setGrant({
grantee,
...grant,
mode: "view",
});
node?.post("view-credential-details", {
id: grant.originalCredentialId,
});
}}
>
<ViewIcon w="5" h="5" color="neutral.400" />
</IconButton>
Expand All @@ -480,9 +545,10 @@ export function Permissions() {
colorPalette="green"
rounded="full"
onClick={() => {
setGrantToRevoke({
setGrant({
grantee,
...grant,
mode: "revoke",
});
}}
>
Expand Down
8 changes: 7 additions & 1 deletion apps/isle/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ interface NodeState {
theme?: IsleTheme;
accessGrants: Map<
{ granteePublicKey: string; meta: { name: string; logo: string; url: string } },
{ id: string; dataId: string; type: string; lockedUntil: number }[]
{
id: string;
dataId: string;
type: string;
originalCredentialId: string;
lockedUntil: number;
}[]
> | null;
initializeNode: () => () => void;
connectWallet: () => void;
Expand Down
23 changes: 23 additions & 0 deletions examples/consumer-and-issuer/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export default function Home() {

isleRef.current = createIsleController({
container: container.id,
enclaveOptions: {
container: "#idOS-enclave",
url: "https://localhost:5173/",
},
credentialRequirements: {
acceptedIssuers: [
{
Expand Down Expand Up @@ -127,6 +131,25 @@ export default function Home() {
});
}
});

isle.on("view-credential-details", async ({ data }) => {
isle?.send("update-view-credential-details-status", {
status: "pending",
});

try {
const credential = await isle?.viewCredentialDetails(data.id);
isle?.send("update-view-credential-details-status", {
status: "success",
credential,
});
} catch (error) {
isle?.send("update-view-credential-details-status", {
status: "error",
error: error as Error,
});
}
});
}, [address, signMessageAsync]);

useEffect(() => {
Expand Down
1 change: 1 addition & 0 deletions packages/@controllers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"version": "0.0.1",
"dependencies": {
"@idos-network/core": "workspace:*",
"@idos-network/idos-sdk": "workspace:*",
"@sanity/comlink": "^3.0.1",
"@wagmi/core": "^2.16.4",
"ethers": "catalog:",
Expand Down
Loading
Loading