diff --git a/generator/nextjs/template/package-lock.json b/generator/nextjs/template/package-lock.json index 81c2f94..b9f621c 100644 --- a/generator/nextjs/template/package-lock.json +++ b/generator/nextjs/template/package-lock.json @@ -15,6 +15,7 @@ "@affinidi-tdk/iota-browser": "^1.0.0", "@affinidi-tdk/iota-client": "^1.4.0", "@affinidi-tdk/iota-core": "^1.0.0", + "@tanstack/react-query": "^5.49.2", "next": "^14.2.4", "next-auth": "^4.24.3", "qrcode.react": "^3.1.0", @@ -2524,6 +2525,30 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.49.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.49.1.tgz", + "integrity": "sha512-JnC9ndmD1KKS01Rt/ovRUB1tmwO7zkyXAyIxN9mznuJrcNtOrkmOnQqdJF2ib9oHzc2VxHomnEG7xyfo54Npkw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.49.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.49.2.tgz", + "integrity": "sha512-6rfwXDK9BvmHISbNFuGd+wY3P44lyW7lWiA9vIFGT/T0P9aHD1VkjTvcM4SDAIbAQ9ygEZZoLt7dlU1o3NjMVA==", + "dependencies": { + "@tanstack/query-core": "5.49.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", diff --git a/generator/nextjs/template/package.json b/generator/nextjs/template/package.json index 2eaee0f..415bf5d 100644 --- a/generator/nextjs/template/package.json +++ b/generator/nextjs/template/package.json @@ -15,6 +15,7 @@ "@affinidi-tdk/iota-browser": "^1.0.0", "@affinidi-tdk/iota-client": "^1.4.0", "@affinidi-tdk/iota-core": "^1.0.0", + "@tanstack/react-query": "^5.49.2", "next": "^14.2.4", "next-auth": "^4.24.3", "qrcode.react": "^3.1.0", diff --git a/generator/nextjs/template/src/components/iota/IotaClientPage.tsx b/generator/nextjs/template/src/components/iota/IotaClientPage.tsx index a0376da..a31c421 100644 --- a/generator/nextjs/template/src/components/iota/IotaClientPage.tsx +++ b/generator/nextjs/template/src/components/iota/IotaClientPage.tsx @@ -6,8 +6,9 @@ import { OpenMode, Session, } from "@affinidi-tdk/iota-browser"; +import { useQuery } from "@tanstack/react-query"; import { useSession } from "next-auth/react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import Button from "../core/Button"; import Select, { SelectOption } from "../core/Select"; @@ -29,103 +30,86 @@ type DataRequests = { }; }; +const fetchIotaConfigurations = (): Promise => + fetch("/api/iota/configuration-options", { method: "GET" }).then((res) => + res.json() + ); + +const getQueryOptions = async (configurationId: string) => { + const response = await fetch( + "/api/iota/query-options?" + + new URLSearchParams({ + iotaConfigurationId: configurationId, + }), + { + method: "GET", + } + ); + return (await response.json()) as SelectOption[]; +}; + +const getIotaCredentials = async (configurationId: string) => { + const response = await fetch( + "/api/iota/start?" + + new URLSearchParams({ + iotaConfigurationId: configurationId, + }), + { + method: "GET", + } + ); + return (await response.json()) as IotaCredentials; +}; + export default function IotaSessionMultipleRequestsPage({ featureAvailable, }: { featureAvailable: boolean; }) { - const [holderDid, setHolderDid] = useState(""); - const [configOptions, setConfigOptions] = useState([]); - const [selectedConfig, setSelectedConfig] = useState(""); - const [iotaSession, setIotaSession] = useState(); - const [iotaIsInitializing, setIotaIsInitializing] = useState(false); - const [queryOptions, setQueryOptions] = useState([]); + const [selectedConfigId, setSelectedConfigId] = useState(""); const [selectedQuery, setSelectedQuery] = useState(""); const [openMode, setOpenMode] = useState(OpenMode.Popup); const [dataRequests, setDataRequests] = useState({}); const [isFormDisabled, setIsFormDisabled] = useState(false); - // Prefill did from session + // Get did from session const { data: session } = useSession(); - useEffect(() => { - if (!session || !session.user) return; - setHolderDid(session.userId); - }, [session]); - useEffect(() => { - const initConfigurations = async () => { - try { - const response = await fetch("/api/iota/configuration-options", { - method: "GET", - }); - const configurations = await response.json(); - setConfigOptions(configurations); - } catch (error) { - console.error("Error getting Iota configurations:", error); - } - }; - if (featureAvailable) { - initConfigurations(); - } - }, [featureAvailable]); + const configurationsQuery = useQuery({ + queryKey: ["iotaConfigurations"], + queryFn: fetchIotaConfigurations, + enabled: !!featureAvailable, + }); - async function handleConfigurationChange(value: string | number) { - const configId = value as string; - clearSession(); - setSelectedConfig(configId); - if (!configId) { - return; - } - try { - setIotaIsInitializing(true); - getQueryOptions(configId); - const credentials = await getIotaCredentials(configId); + const iotaSessionQuery = useQuery({ + queryKey: ["iotaSession", selectedConfigId], + queryFn: async ({ queryKey }) => { + const credentials = await getIotaCredentials(queryKey[1]); const iotaSession = new Session({ credentials }); await iotaSession.initialize(); - setIotaSession(iotaSession); - } catch (error) { - if (error instanceof IotaError) { - console.log(error.code); - } - } finally { - setIotaIsInitializing(false); - } - } + return iotaSession; + }, + enabled: !!selectedConfigId, + }); - async function getQueryOptions(configurationId: string) { - const response = await fetch( - "/api/iota/query-options?" + - new URLSearchParams({ - iotaConfigurationId: configurationId, - }), - { - method: "GET", - }, - ); - const options = (await response.json()) as SelectOption[]; - setQueryOptions(options); - } + const iotaQueryOptionsQuery = useQuery({ + queryKey: ["queryOptions", selectedConfigId], + queryFn: ({ queryKey }) => getQueryOptions(queryKey[1]), + enabled: !!selectedConfigId, + }); - async function getIotaCredentials(configurationId: string) { - const response = await fetch( - "/api/iota/start?" + - new URLSearchParams({ - iotaConfigurationId: configurationId, - }), - { - method: "GET", - }, - ); - return (await response.json()) as IotaCredentials; + async function handleConfigurationChange(value: string | number) { + clearSession(); + setSelectedConfigId(value as string); } async function handleTDKShare(queryId: string) { - if (!iotaSession) { + if (!iotaSessionQuery.data) { throw new Error("Iota session not initialized"); } try { setIsFormDisabled(true); - const request = await iotaSession.prepareRequest({ queryId }); + const request = await iotaSessionQuery.data.prepareRequest({ queryId }); setIsFormDisabled(false); addNewDataRequest(request); request.openVault({ mode: openMode }); @@ -133,7 +117,7 @@ export default function IotaSessionMultipleRequestsPage({ updateDataRequestWithResponse(response); } catch (error) { if (error instanceof IotaError) { - updateDataRequestWithError(error) + updateDataRequestWithError(error); console.log(error.code); } } @@ -168,135 +152,159 @@ export default function IotaSessionMultipleRequestsPage({ } }; - async function handleOpenModeChange(value: string | number) { - setOpenMode(value as number); - } - - async function handleQueryChange(value: string | number) { - setSelectedQuery(value as string); - } - async function clearSession() { - setIotaSession(undefined); - setQueryOptions([]); setSelectedQuery(""); setIsFormDisabled(false); } - return ( - <> -

Receive Credentials

+ const renderVerifiedHolder = (userId: string) => { + return ( +
+

+ Verified holder did (From Affinidi Login) +

+

{userId}

+
+ ); + }; - {!featureAvailable && ( + const hasErrors = !featureAvailable || !session || !session.userId; + const renderErrors = () => { + if (!featureAvailable) { + return (
Feature not available. Please set your Personal Access Token in your environment secrets.
- )} + ); + } - {featureAvailable && !holderDid && ( + if (!session || !session.userId) { + return (
- You must be logged in to share credentials from your Affinidi Vault + You must be logged in to issue credentials to your Affinidi Vault
- )} + ); + } + }; - {featureAvailable && holderDid && ( + return ( + <> +

Receive Credentials

+ + {renderErrors()} + {!hasErrors && ( <> -
-

- Verified holder did (From Affinidi Login) -

-

{holderDid}

-
+ {renderVerifiedHolder(session.userId)} - {configOptions.length === 0 && ( + {configurationsQuery.isPending && (
Loading configurations...
)} - - {configOptions.length > 0 && ( + {configurationsQuery.isSuccess && + configurationsQuery.data.length === 0 && ( +
+ You don't have any configurations. Go to the{" "} + + Affinidi Portal + {" "} + to create one. +
+ )} + {configurationsQuery.isSuccess && + configurationsQuery.data.length > 0 && ( + setOpenMode(val as number)} /> )} - {selectedConfig && ( -
+ {iotaQueryOptionsQuery.isFetching && ( +
Loading queries...
+ )} + {iotaQueryOptionsQuery.isSuccess && + iotaQueryOptionsQuery.data.length === 0 && ( +
+ You don't have any queries. Go to the{" "} + + Affinidi Portal + {" "} + to create one. +
+ )} + {iotaQueryOptionsQuery.isSuccess && + iotaQueryOptionsQuery.data.length > 0 && ( - )} - - {iotaSession && selectedQuery && ( - - )} + {iotaSessionQuery.isSuccess && selectedQuery && ( + + )} - {iotaIsInitializing && ( -
- Initializing session with Affinidi Iota Framework... -
- )} - {selectedConfig && !iotaIsInitializing && !iotaSession && ( -
Failed to initialize Iota
- )} + {iotaSessionQuery.isFetching && ( +
+ Initializing session with Affinidi Iota Framework... +
+ )} + {iotaSessionQuery.isError &&
Failed to initialize Iota
} - {Object.keys(dataRequests).length > 0 && ( -
- - - - - - - - - {Object.keys(dataRequests).map((id: string) => ( - - - - - ))} - -
Request IDResult
{id} -
-                              {dataRequests[id].result instanceof IotaError && 

Error received:

} - {!(dataRequests[id].result instanceof IotaError) &&

Response received:

} - {JSON.stringify( - dataRequests[id].result, - undefined, - 2, - )} -
-
-
- )} + {Object.keys(dataRequests).length > 0 && ( +
+ + + + + + + + + {Object.keys(dataRequests).map((id: string) => ( + + + + + ))} + +
Request IDResult
{id} +
+                          {dataRequests[id].result instanceof IotaError && (
+                            

Error received:

+ )} + {!(dataRequests[id].result instanceof IotaError) && ( +

Response received:

+ )} + {JSON.stringify( + dataRequests[id].result, + undefined, + 2 + )} +
+
)} diff --git a/generator/nextjs/template/src/pages/_app.tsx b/generator/nextjs/template/src/pages/_app.tsx index 929f18f..7cd39f7 100644 --- a/generator/nextjs/template/src/pages/_app.tsx +++ b/generator/nextjs/template/src/pages/_app.tsx @@ -3,17 +3,22 @@ import type { AppProps } from "next/app"; import NavBar from "src/components/NavBar"; import "../styles/globals.css"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; export default function App({ Component, pageProps: { session, ...pageProps }, }: AppProps) { + const queryClient = new QueryClient(); + return ( - -
- -
+ + +
+ +
+
); } diff --git a/generator/nextjs/template/src/pages/credential-issuance.tsx b/generator/nextjs/template/src/pages/credential-issuance.tsx index 68976c8..a5752c7 100644 --- a/generator/nextjs/template/src/pages/credential-issuance.tsx +++ b/generator/nextjs/template/src/pages/credential-issuance.tsx @@ -2,13 +2,14 @@ import { IssuanceConfigDtoCredentialSupportedInner, StartIssuanceInputClaimModeEnum, } from "@affinidi-tdk/credential-issuance-client"; +import { useQuery } from "@tanstack/react-query"; import { GetServerSideProps, InferGetServerSidePropsType } from "next"; import { useSession } from "next-auth/react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import Message from "src/components/Message"; import Button from "src/components/core/Button"; import Select, { SelectOption } from "src/components/core/Select"; -import DynamicForm, { FormSchema } from "src/components/issuance/DynamicForm"; +import DynamicForm from "src/components/issuance/DynamicForm"; import Offer from "src/components/issuance/Offer"; import { personalAccessTokenConfigured } from "src/lib/env"; import { MessagePayload, OfferPayload } from "src/types/types"; @@ -22,6 +23,29 @@ const claimModeOptions = [ }, ]; +const fetchCredentialSchema = async (jsonSchemaUrl: string) => { + const response = await fetch(jsonSchemaUrl, { + method: "GET", + }); + const schema = await response.json(); + return schema; +}; + +const fetchCredentialTypes = ( + issuanceConfigurationId: string +): Promise => { + return fetch( + "/api/issuance/credential-types?" + + new URLSearchParams({ issuanceConfigurationId }), + { method: "GET" } + ).then((res) => res.json()); +}; + +const fetchIssuanceConfigurations = (): Promise => + fetch("/api/issuance/configuration-options", { method: "GET" }).then((res) => + res.json() + ); + export const getServerSideProps = (async () => { return { props: { featureAvailable: personalAccessTokenConfigured() } }; }) satisfies GetServerSideProps<{ featureAvailable: boolean }>; @@ -29,121 +53,61 @@ export const getServerSideProps = (async () => { export default function CredentialIssuance({ featureAvailable, }: InferGetServerSidePropsType) { - const [holderDid, setHolderDid] = useState(""); - const [configOptions, setConfigOptions] = useState([]); - const [selectedConfig, setSelectedConfig] = useState(""); - const [types, setTypes] = useState< - IssuanceConfigDtoCredentialSupportedInner[] - >([]); - const [typeOptions, setTypeOptions] = useState([]); - const [selectedType, setSelectedType] = useState(""); - const [formProperties, setFormProperties] = useState(); + const [formData, setFormData] = useState<{ + selectedConfigId: string; + selectedTypeId: string; + }>({ selectedConfigId: "", selectedTypeId: "" }); const [isFormDisabled, setIsFormDisabled] = useState(false); const [offer, setOffer] = useState(); const [message, setMessage] = useState(); const [claimMode, setClaimMode] = useState( - StartIssuanceInputClaimModeEnum.TxCode, + StartIssuanceInputClaimModeEnum.TxCode ); - // Prefill did from session + const { selectedConfigId, selectedTypeId } = formData; + + // Get did from session const { data: session } = useSession(); - useEffect(() => { - if (!session || !session.user) return; - setHolderDid(session.userId); - }, [session]); - useEffect(() => { - const initConfigurations = async () => { - try { - const response = await fetch("/api/issuance/configuration-options", { - method: "GET", - }); - const configurations = await response.json(); - console.log(configurations); - setConfigOptions(configurations); - } catch (error) { - console.error("Error getting issuance configurations:", error); - } - }; - if (featureAvailable) { - initConfigurations(); - } - }, [featureAvailable]); + const configurationsQuery = useQuery({ + queryKey: ["issuanceConfigurations"], + queryFn: fetchIssuanceConfigurations, + enabled: !!featureAvailable, + }); - async function handleConfigurationChange(value: string | number) { - const configId = value as string; - clearIssuance(); - setTypeOptions([]); - setTypes([]); - setSelectedConfig(configId); - if (!configId) { - return; - } - const response = await fetch( - "/api/issuance/credential-types?" + - new URLSearchParams({ issuanceConfigurationId: configId }), - { - method: "GET", - }, - ); - const credentialTypes = await response.json(); - console.log(credentialTypes); - const credentialTypeOptions = credentialTypes.map( - (type: IssuanceConfigDtoCredentialSupportedInner) => ({ - label: type.credentialTypeId, - value: type.credentialTypeId, - }), - ); - setTypes(credentialTypes); - setTypeOptions(credentialTypeOptions); - } + const credentialTypesQuery = useQuery({ + queryKey: ["types", selectedConfigId], + queryFn: ({ queryKey }) => fetchCredentialTypes(queryKey[1]), + enabled: !!selectedConfigId, + }); - async function handleCredentialTypeChange(value: string | number) { - clearIssuance(); - setSelectedType(value as string); - if (!value) { - return; - } - const credentialType = types.find( - (type) => type.credentialTypeId === value, - ); - if (!credentialType) { - setMessage({ - message: "Unable to fetch credential schema to build the form", - type: "error", - }); - return; - } - - const response = await fetch(credentialType.jsonSchemaUrl, { - method: "GET", - }); - const schema = await response.json(); - console.log(schema); - setFormProperties(schema.properties.credentialSubject); - console.log(formProperties); - } + const schemaQuery = useQuery({ + queryKey: ["schema", selectedTypeId], + queryFn: () => { + const credentialType = credentialTypesQuery.data?.find( + (type) => type.credentialTypeId === selectedTypeId + ); + if (!credentialType) { + setMessage({ + message: "Unable to fetch credential schema to build the form", + type: "error", + }); + return; + } - function handleClaimModeChange(value: string | number) { - setClaimMode(value as string); - } + return fetchCredentialSchema(credentialType.jsonSchemaUrl); + }, + enabled: !!selectedTypeId, + }); const handleSubmit = async (credentialData: any) => { - console.log(credentialData); - if (!selectedType) { - setMessage({ - message: "Holder's DID and Credential Type ID are required", - type: "error", - }); - return; - } console.log("credentialData:", credentialData); setIsFormDisabled(true); const response = await fetch("/api/issuance/start", { method: "POST", body: JSON.stringify({ credentialData, - credentialTypeId: selectedType, + credentialTypeId: selectedTypeId, claimMode, }), headers: { @@ -170,80 +134,139 @@ export default function CredentialIssuance({ setOffer(undefined); setIsFormDisabled(false); setMessage(undefined); - setFormProperties(undefined); - setSelectedType(""); + setFormData({ selectedConfigId: "", selectedTypeId: "" }); } - return ( - <> -

Issue Credentials

- - {!featureAvailable && ( + const hasErrors = !featureAvailable || !session || !session.userId; + const renderErrors = () => { + if (!featureAvailable) { + return (
Feature not available. Please set your Personal Access Token in your environment secrets.
- )} + ); + } - {featureAvailable && !holderDid && ( + if (!session || !session.userId) { + return (
You must be logged in to issue credentials to your Affinidi Vault
- )} - - {featureAvailable && holderDid && ( - <> -
-

- Verified holder did (From Affinidi Login) -

-

{holderDid}

-
- - {offer && ( -
- - -
- )} + ); + } + }; - {!offer && configOptions.length === 0 && ( -
Loading configurations...
- )} + const renderVerifiedHolder = (userId: string) => { + return ( +
+

+ Verified holder did (From Affinidi Login) +

+

{userId}

+
+ ); + }; - {!offer && configOptions.length > 0 && ( - - {typeOptions.length === 0 && ( -
Loading credential types...
+ {configurationsQuery.isPending && ( +
Loading configurations...
+ )} + {configurationsQuery.isSuccess && + configurationsQuery.data.length === 0 && ( +
+ You don't have any configurations. Go to the{" "} + + Affinidi Portal + {" "} + to create one. +
+ )} + {configurationsQuery.isSuccess && + configurationsQuery.data.length > 0 && ( + setClaimMode(val as string)} + /> )} - {typeOptions.length > 0 && ( + {credentialTypesQuery.isFetching && ( +
Loading credential types...
+ )} + {credentialTypesQuery.isSuccess && + credentialTypesQuery.data.length === 0 && ( +
+ You don't have any credential types. Go to the{" "} + + Affinidi Portal + {" "} + to create one. +
+ )} + {credentialTypesQuery.isSuccess && ( + )} + {selectedConfigId && ( setSelectedQuery(val as string)} /> + )} - {queryOptions.length === 0 && ( -
Loading queries...
- )} - {queryOptions.length > 0 && ( - - )} + const renderOffer = (offerTorender: OfferPayload) => { + return ( +
+ + +
+ ); + }; - {!offer && selectedConfig && ( + return ( + <> +

Issue Credentials

+ {renderErrors()} + {!hasErrors && ( +
+ {renderVerifiedHolder(session.userId)} + {offer ? ( + renderOffer(offer) + ) : (
- + setFormData({ + ...formData, + selectedConfigId: value as string, + }) + } + /> + )} + {selectedConfigId && ( + ({ + label: type.credentialTypeId, + value: type.credentialTypeId, + }) + ) || [] + } + value={selectedTypeId} disabled={isFormDisabled} - onChange={handleCredentialTypeChange} + onChange={(value) => + setFormData({ + ...formData, + selectedTypeId: value as string, + }) + } /> )} {message && ( @@ -251,21 +274,23 @@ export default function CredentialIssuance({
)} - {formProperties && claimMode && ( + {schemaQuery.data?.properties.credentialSubject && (

Credential data

)}
)} - +
)} ); diff --git a/samples/auth0-nextjs-nextauthjs/package-lock.json b/samples/auth0-nextjs-nextauthjs/package-lock.json index 1377d15..7c17563 100644 --- a/samples/auth0-nextjs-nextauthjs/package-lock.json +++ b/samples/auth0-nextjs-nextauthjs/package-lock.json @@ -15,6 +15,7 @@ "@affinidi-tdk/iota-browser": "^1.0.0", "@affinidi-tdk/iota-client": "^1.4.0", "@affinidi-tdk/iota-core": "^1.0.0", + "@tanstack/react-query": "^5.49.2", "next": "^14.2.4", "next-auth": "^4.24.3", "qrcode.react": "^3.1.0", @@ -2524,6 +2525,30 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.49.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.49.1.tgz", + "integrity": "sha512-JnC9ndmD1KKS01Rt/ovRUB1tmwO7zkyXAyIxN9mznuJrcNtOrkmOnQqdJF2ib9oHzc2VxHomnEG7xyfo54Npkw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.49.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.49.2.tgz", + "integrity": "sha512-6rfwXDK9BvmHISbNFuGd+wY3P44lyW7lWiA9vIFGT/T0P9aHD1VkjTvcM4SDAIbAQ9ygEZZoLt7dlU1o3NjMVA==", + "dependencies": { + "@tanstack/query-core": "5.49.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", diff --git a/samples/auth0-nextjs-nextauthjs/package.json b/samples/auth0-nextjs-nextauthjs/package.json index a8d7d20..a72e5bf 100644 --- a/samples/auth0-nextjs-nextauthjs/package.json +++ b/samples/auth0-nextjs-nextauthjs/package.json @@ -15,6 +15,7 @@ "@affinidi-tdk/iota-browser": "^1.0.0", "@affinidi-tdk/iota-client": "^1.4.0", "@affinidi-tdk/iota-core": "^1.0.0", + "@tanstack/react-query": "^5.49.2", "next": "^14.2.4", "next-auth": "^4.24.3", "qrcode.react": "^3.1.0", diff --git a/samples/auth0-nextjs-nextauthjs/src/components/iota/IotaClientPage.tsx b/samples/auth0-nextjs-nextauthjs/src/components/iota/IotaClientPage.tsx index a0376da..a31c421 100644 --- a/samples/auth0-nextjs-nextauthjs/src/components/iota/IotaClientPage.tsx +++ b/samples/auth0-nextjs-nextauthjs/src/components/iota/IotaClientPage.tsx @@ -6,8 +6,9 @@ import { OpenMode, Session, } from "@affinidi-tdk/iota-browser"; +import { useQuery } from "@tanstack/react-query"; import { useSession } from "next-auth/react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import Button from "../core/Button"; import Select, { SelectOption } from "../core/Select"; @@ -29,103 +30,86 @@ type DataRequests = { }; }; +const fetchIotaConfigurations = (): Promise => + fetch("/api/iota/configuration-options", { method: "GET" }).then((res) => + res.json() + ); + +const getQueryOptions = async (configurationId: string) => { + const response = await fetch( + "/api/iota/query-options?" + + new URLSearchParams({ + iotaConfigurationId: configurationId, + }), + { + method: "GET", + } + ); + return (await response.json()) as SelectOption[]; +}; + +const getIotaCredentials = async (configurationId: string) => { + const response = await fetch( + "/api/iota/start?" + + new URLSearchParams({ + iotaConfigurationId: configurationId, + }), + { + method: "GET", + } + ); + return (await response.json()) as IotaCredentials; +}; + export default function IotaSessionMultipleRequestsPage({ featureAvailable, }: { featureAvailable: boolean; }) { - const [holderDid, setHolderDid] = useState(""); - const [configOptions, setConfigOptions] = useState([]); - const [selectedConfig, setSelectedConfig] = useState(""); - const [iotaSession, setIotaSession] = useState(); - const [iotaIsInitializing, setIotaIsInitializing] = useState(false); - const [queryOptions, setQueryOptions] = useState([]); + const [selectedConfigId, setSelectedConfigId] = useState(""); const [selectedQuery, setSelectedQuery] = useState(""); const [openMode, setOpenMode] = useState(OpenMode.Popup); const [dataRequests, setDataRequests] = useState({}); const [isFormDisabled, setIsFormDisabled] = useState(false); - // Prefill did from session + // Get did from session const { data: session } = useSession(); - useEffect(() => { - if (!session || !session.user) return; - setHolderDid(session.userId); - }, [session]); - useEffect(() => { - const initConfigurations = async () => { - try { - const response = await fetch("/api/iota/configuration-options", { - method: "GET", - }); - const configurations = await response.json(); - setConfigOptions(configurations); - } catch (error) { - console.error("Error getting Iota configurations:", error); - } - }; - if (featureAvailable) { - initConfigurations(); - } - }, [featureAvailable]); + const configurationsQuery = useQuery({ + queryKey: ["iotaConfigurations"], + queryFn: fetchIotaConfigurations, + enabled: !!featureAvailable, + }); - async function handleConfigurationChange(value: string | number) { - const configId = value as string; - clearSession(); - setSelectedConfig(configId); - if (!configId) { - return; - } - try { - setIotaIsInitializing(true); - getQueryOptions(configId); - const credentials = await getIotaCredentials(configId); + const iotaSessionQuery = useQuery({ + queryKey: ["iotaSession", selectedConfigId], + queryFn: async ({ queryKey }) => { + const credentials = await getIotaCredentials(queryKey[1]); const iotaSession = new Session({ credentials }); await iotaSession.initialize(); - setIotaSession(iotaSession); - } catch (error) { - if (error instanceof IotaError) { - console.log(error.code); - } - } finally { - setIotaIsInitializing(false); - } - } + return iotaSession; + }, + enabled: !!selectedConfigId, + }); - async function getQueryOptions(configurationId: string) { - const response = await fetch( - "/api/iota/query-options?" + - new URLSearchParams({ - iotaConfigurationId: configurationId, - }), - { - method: "GET", - }, - ); - const options = (await response.json()) as SelectOption[]; - setQueryOptions(options); - } + const iotaQueryOptionsQuery = useQuery({ + queryKey: ["queryOptions", selectedConfigId], + queryFn: ({ queryKey }) => getQueryOptions(queryKey[1]), + enabled: !!selectedConfigId, + }); - async function getIotaCredentials(configurationId: string) { - const response = await fetch( - "/api/iota/start?" + - new URLSearchParams({ - iotaConfigurationId: configurationId, - }), - { - method: "GET", - }, - ); - return (await response.json()) as IotaCredentials; + async function handleConfigurationChange(value: string | number) { + clearSession(); + setSelectedConfigId(value as string); } async function handleTDKShare(queryId: string) { - if (!iotaSession) { + if (!iotaSessionQuery.data) { throw new Error("Iota session not initialized"); } try { setIsFormDisabled(true); - const request = await iotaSession.prepareRequest({ queryId }); + const request = await iotaSessionQuery.data.prepareRequest({ queryId }); setIsFormDisabled(false); addNewDataRequest(request); request.openVault({ mode: openMode }); @@ -133,7 +117,7 @@ export default function IotaSessionMultipleRequestsPage({ updateDataRequestWithResponse(response); } catch (error) { if (error instanceof IotaError) { - updateDataRequestWithError(error) + updateDataRequestWithError(error); console.log(error.code); } } @@ -168,135 +152,159 @@ export default function IotaSessionMultipleRequestsPage({ } }; - async function handleOpenModeChange(value: string | number) { - setOpenMode(value as number); - } - - async function handleQueryChange(value: string | number) { - setSelectedQuery(value as string); - } - async function clearSession() { - setIotaSession(undefined); - setQueryOptions([]); setSelectedQuery(""); setIsFormDisabled(false); } - return ( - <> -

Receive Credentials

+ const renderVerifiedHolder = (userId: string) => { + return ( +
+

+ Verified holder did (From Affinidi Login) +

+

{userId}

+
+ ); + }; - {!featureAvailable && ( + const hasErrors = !featureAvailable || !session || !session.userId; + const renderErrors = () => { + if (!featureAvailable) { + return (
Feature not available. Please set your Personal Access Token in your environment secrets.
- )} + ); + } - {featureAvailable && !holderDid && ( + if (!session || !session.userId) { + return (
- You must be logged in to share credentials from your Affinidi Vault + You must be logged in to issue credentials to your Affinidi Vault
- )} + ); + } + }; - {featureAvailable && holderDid && ( + return ( + <> +

Receive Credentials

+ + {renderErrors()} + {!hasErrors && ( <> -
-

- Verified holder did (From Affinidi Login) -

-

{holderDid}

-
+ {renderVerifiedHolder(session.userId)} - {configOptions.length === 0 && ( + {configurationsQuery.isPending && (
Loading configurations...
)} - - {configOptions.length > 0 && ( + {configurationsQuery.isSuccess && + configurationsQuery.data.length === 0 && ( +
+ You don't have any configurations. Go to the{" "} + + Affinidi Portal + {" "} + to create one. +
+ )} + {configurationsQuery.isSuccess && + configurationsQuery.data.length > 0 && ( + setOpenMode(val as number)} /> )} - {selectedConfig && ( -
+ {iotaQueryOptionsQuery.isFetching && ( +
Loading queries...
+ )} + {iotaQueryOptionsQuery.isSuccess && + iotaQueryOptionsQuery.data.length === 0 && ( +
+ You don't have any queries. Go to the{" "} + + Affinidi Portal + {" "} + to create one. +
+ )} + {iotaQueryOptionsQuery.isSuccess && + iotaQueryOptionsQuery.data.length > 0 && ( - )} - - {iotaSession && selectedQuery && ( - - )} + {iotaSessionQuery.isSuccess && selectedQuery && ( + + )} - {iotaIsInitializing && ( -
- Initializing session with Affinidi Iota Framework... -
- )} - {selectedConfig && !iotaIsInitializing && !iotaSession && ( -
Failed to initialize Iota
- )} + {iotaSessionQuery.isFetching && ( +
+ Initializing session with Affinidi Iota Framework... +
+ )} + {iotaSessionQuery.isError &&
Failed to initialize Iota
} - {Object.keys(dataRequests).length > 0 && ( -
- - - - - - - - - {Object.keys(dataRequests).map((id: string) => ( - - - - - ))} - -
Request IDResult
{id} -
-                              {dataRequests[id].result instanceof IotaError && 

Error received:

} - {!(dataRequests[id].result instanceof IotaError) &&

Response received:

} - {JSON.stringify( - dataRequests[id].result, - undefined, - 2, - )} -
-
-
- )} + {Object.keys(dataRequests).length > 0 && ( +
+ + + + + + + + + {Object.keys(dataRequests).map((id: string) => ( + + + + + ))} + +
Request IDResult
{id} +
+                          {dataRequests[id].result instanceof IotaError && (
+                            

Error received:

+ )} + {!(dataRequests[id].result instanceof IotaError) && ( +

Response received:

+ )} + {JSON.stringify( + dataRequests[id].result, + undefined, + 2 + )} +
+
)} diff --git a/samples/auth0-nextjs-nextauthjs/src/pages/_app.tsx b/samples/auth0-nextjs-nextauthjs/src/pages/_app.tsx index 929f18f..7cd39f7 100644 --- a/samples/auth0-nextjs-nextauthjs/src/pages/_app.tsx +++ b/samples/auth0-nextjs-nextauthjs/src/pages/_app.tsx @@ -3,17 +3,22 @@ import type { AppProps } from "next/app"; import NavBar from "src/components/NavBar"; import "../styles/globals.css"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; export default function App({ Component, pageProps: { session, ...pageProps }, }: AppProps) { + const queryClient = new QueryClient(); + return ( - -
- -
+ + +
+ +
+
); } diff --git a/samples/auth0-nextjs-nextauthjs/src/pages/credential-issuance.tsx b/samples/auth0-nextjs-nextauthjs/src/pages/credential-issuance.tsx index 68976c8..a5752c7 100644 --- a/samples/auth0-nextjs-nextauthjs/src/pages/credential-issuance.tsx +++ b/samples/auth0-nextjs-nextauthjs/src/pages/credential-issuance.tsx @@ -2,13 +2,14 @@ import { IssuanceConfigDtoCredentialSupportedInner, StartIssuanceInputClaimModeEnum, } from "@affinidi-tdk/credential-issuance-client"; +import { useQuery } from "@tanstack/react-query"; import { GetServerSideProps, InferGetServerSidePropsType } from "next"; import { useSession } from "next-auth/react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import Message from "src/components/Message"; import Button from "src/components/core/Button"; import Select, { SelectOption } from "src/components/core/Select"; -import DynamicForm, { FormSchema } from "src/components/issuance/DynamicForm"; +import DynamicForm from "src/components/issuance/DynamicForm"; import Offer from "src/components/issuance/Offer"; import { personalAccessTokenConfigured } from "src/lib/env"; import { MessagePayload, OfferPayload } from "src/types/types"; @@ -22,6 +23,29 @@ const claimModeOptions = [ }, ]; +const fetchCredentialSchema = async (jsonSchemaUrl: string) => { + const response = await fetch(jsonSchemaUrl, { + method: "GET", + }); + const schema = await response.json(); + return schema; +}; + +const fetchCredentialTypes = ( + issuanceConfigurationId: string +): Promise => { + return fetch( + "/api/issuance/credential-types?" + + new URLSearchParams({ issuanceConfigurationId }), + { method: "GET" } + ).then((res) => res.json()); +}; + +const fetchIssuanceConfigurations = (): Promise => + fetch("/api/issuance/configuration-options", { method: "GET" }).then((res) => + res.json() + ); + export const getServerSideProps = (async () => { return { props: { featureAvailable: personalAccessTokenConfigured() } }; }) satisfies GetServerSideProps<{ featureAvailable: boolean }>; @@ -29,121 +53,61 @@ export const getServerSideProps = (async () => { export default function CredentialIssuance({ featureAvailable, }: InferGetServerSidePropsType) { - const [holderDid, setHolderDid] = useState(""); - const [configOptions, setConfigOptions] = useState([]); - const [selectedConfig, setSelectedConfig] = useState(""); - const [types, setTypes] = useState< - IssuanceConfigDtoCredentialSupportedInner[] - >([]); - const [typeOptions, setTypeOptions] = useState([]); - const [selectedType, setSelectedType] = useState(""); - const [formProperties, setFormProperties] = useState(); + const [formData, setFormData] = useState<{ + selectedConfigId: string; + selectedTypeId: string; + }>({ selectedConfigId: "", selectedTypeId: "" }); const [isFormDisabled, setIsFormDisabled] = useState(false); const [offer, setOffer] = useState(); const [message, setMessage] = useState(); const [claimMode, setClaimMode] = useState( - StartIssuanceInputClaimModeEnum.TxCode, + StartIssuanceInputClaimModeEnum.TxCode ); - // Prefill did from session + const { selectedConfigId, selectedTypeId } = formData; + + // Get did from session const { data: session } = useSession(); - useEffect(() => { - if (!session || !session.user) return; - setHolderDid(session.userId); - }, [session]); - useEffect(() => { - const initConfigurations = async () => { - try { - const response = await fetch("/api/issuance/configuration-options", { - method: "GET", - }); - const configurations = await response.json(); - console.log(configurations); - setConfigOptions(configurations); - } catch (error) { - console.error("Error getting issuance configurations:", error); - } - }; - if (featureAvailable) { - initConfigurations(); - } - }, [featureAvailable]); + const configurationsQuery = useQuery({ + queryKey: ["issuanceConfigurations"], + queryFn: fetchIssuanceConfigurations, + enabled: !!featureAvailable, + }); - async function handleConfigurationChange(value: string | number) { - const configId = value as string; - clearIssuance(); - setTypeOptions([]); - setTypes([]); - setSelectedConfig(configId); - if (!configId) { - return; - } - const response = await fetch( - "/api/issuance/credential-types?" + - new URLSearchParams({ issuanceConfigurationId: configId }), - { - method: "GET", - }, - ); - const credentialTypes = await response.json(); - console.log(credentialTypes); - const credentialTypeOptions = credentialTypes.map( - (type: IssuanceConfigDtoCredentialSupportedInner) => ({ - label: type.credentialTypeId, - value: type.credentialTypeId, - }), - ); - setTypes(credentialTypes); - setTypeOptions(credentialTypeOptions); - } + const credentialTypesQuery = useQuery({ + queryKey: ["types", selectedConfigId], + queryFn: ({ queryKey }) => fetchCredentialTypes(queryKey[1]), + enabled: !!selectedConfigId, + }); - async function handleCredentialTypeChange(value: string | number) { - clearIssuance(); - setSelectedType(value as string); - if (!value) { - return; - } - const credentialType = types.find( - (type) => type.credentialTypeId === value, - ); - if (!credentialType) { - setMessage({ - message: "Unable to fetch credential schema to build the form", - type: "error", - }); - return; - } - - const response = await fetch(credentialType.jsonSchemaUrl, { - method: "GET", - }); - const schema = await response.json(); - console.log(schema); - setFormProperties(schema.properties.credentialSubject); - console.log(formProperties); - } + const schemaQuery = useQuery({ + queryKey: ["schema", selectedTypeId], + queryFn: () => { + const credentialType = credentialTypesQuery.data?.find( + (type) => type.credentialTypeId === selectedTypeId + ); + if (!credentialType) { + setMessage({ + message: "Unable to fetch credential schema to build the form", + type: "error", + }); + return; + } - function handleClaimModeChange(value: string | number) { - setClaimMode(value as string); - } + return fetchCredentialSchema(credentialType.jsonSchemaUrl); + }, + enabled: !!selectedTypeId, + }); const handleSubmit = async (credentialData: any) => { - console.log(credentialData); - if (!selectedType) { - setMessage({ - message: "Holder's DID and Credential Type ID are required", - type: "error", - }); - return; - } console.log("credentialData:", credentialData); setIsFormDisabled(true); const response = await fetch("/api/issuance/start", { method: "POST", body: JSON.stringify({ credentialData, - credentialTypeId: selectedType, + credentialTypeId: selectedTypeId, claimMode, }), headers: { @@ -170,80 +134,139 @@ export default function CredentialIssuance({ setOffer(undefined); setIsFormDisabled(false); setMessage(undefined); - setFormProperties(undefined); - setSelectedType(""); + setFormData({ selectedConfigId: "", selectedTypeId: "" }); } - return ( - <> -

Issue Credentials

- - {!featureAvailable && ( + const hasErrors = !featureAvailable || !session || !session.userId; + const renderErrors = () => { + if (!featureAvailable) { + return (
Feature not available. Please set your Personal Access Token in your environment secrets.
- )} + ); + } - {featureAvailable && !holderDid && ( + if (!session || !session.userId) { + return (
You must be logged in to issue credentials to your Affinidi Vault
- )} - - {featureAvailable && holderDid && ( - <> -
-

- Verified holder did (From Affinidi Login) -

-

{holderDid}

-
- - {offer && ( -
- - -
- )} + ); + } + }; - {!offer && configOptions.length === 0 && ( -
Loading configurations...
- )} + const renderVerifiedHolder = (userId: string) => { + return ( +
+

+ Verified holder did (From Affinidi Login) +

+

{userId}

+
+ ); + }; - {!offer && configOptions.length > 0 && ( - - {typeOptions.length === 0 && ( -
Loading credential types...
+ {configurationsQuery.isPending && ( +
Loading configurations...
+ )} + {configurationsQuery.isSuccess && + configurationsQuery.data.length === 0 && ( +
+ You don't have any configurations. Go to the{" "} + + Affinidi Portal + {" "} + to create one. +
+ )} + {configurationsQuery.isSuccess && + configurationsQuery.data.length > 0 && ( + setClaimMode(val as string)} + /> )} - {typeOptions.length > 0 && ( + {credentialTypesQuery.isFetching && ( +
Loading credential types...
+ )} + {credentialTypesQuery.isSuccess && + credentialTypesQuery.data.length === 0 && ( +
+ You don't have any credential types. Go to the{" "} + + Affinidi Portal + {" "} + to create one. +
+ )} + {credentialTypesQuery.isSuccess && (