-
Notifications
You must be signed in to change notification settings - Fork 5
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: implement ICP verfiable Credential with Dacade as relying party #1284
Changes from all commits
3eb9e96
d827bff
e097bda
d0f58e7
7edf22b
79842a9
afec598
0da6c3f
3f9230d
caae4c5
41812f4
2e1ccad
5617aa6
3dda8c0
aa11662
c8b19b6
356aed3
0228735
5827fc3
8b8550a
9d19e81
5b424b2
797ae20
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
v20.14.0 | ||
v22.6.0 |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,100 @@ | ||||||||||||||||||||||
import { canisterId, createActor } from "@/utilities/icp/issuerFactory"; | ||||||||||||||||||||||
import { Identity } from "@dfinity/agent"; | ||||||||||||||||||||||
import { AuthClient } from "@dfinity/auth-client"; | ||||||||||||||||||||||
import { Principal } from "@dfinity/principal"; | ||||||||||||||||||||||
import { useCallback, useEffect } from "react"; | ||||||||||||||||||||||
|
||||||||||||||||||||||
type Auth = { | ||||||||||||||||||||||
client: AuthClient; | ||||||||||||||||||||||
isAuthenticated: boolean; | ||||||||||||||||||||||
identity: Identity; | ||||||||||||||||||||||
principal: Principal; | ||||||||||||||||||||||
principalText: string; | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
declare global { | ||||||||||||||||||||||
interface Window { | ||||||||||||||||||||||
auth: Auth; | ||||||||||||||||||||||
issuerCanister: any; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
serapieTuyishime marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
|
||||||||||||||||||||||
export const MAX_TTL = BigInt(7 * 24 * 60 * 60 * 1000 * 1000 * 1000); | ||||||||||||||||||||||
|
||||||||||||||||||||||
/** | ||||||||||||||||||||||
* For production ready we shall use https://identity.ic0.app/ as identity provider | ||||||||||||||||||||||
*/ | ||||||||||||||||||||||
export const IDENTITY_PROVIDER = "https://identity.ic0.app/"; | ||||||||||||||||||||||
serapieTuyishime marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
const useIcpAuth = () => { | ||||||||||||||||||||||
useEffect(() => { | ||||||||||||||||||||||
if (!window) return; | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use To prevent potential Apply this diff to fix the issue: - if (!window) return;
+ if (typeof window === 'undefined') return; 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||
async function initializeContract() { | ||||||||||||||||||||||
const authClient = await AuthClient.create(); | ||||||||||||||||||||||
window.auth = {} as any; | ||||||||||||||||||||||
window.auth.client = authClient; | ||||||||||||||||||||||
window.auth.isAuthenticated = await authClient.isAuthenticated(); | ||||||||||||||||||||||
window.auth.identity = authClient.getIdentity(); | ||||||||||||||||||||||
window.auth.principal = authClient.getIdentity()?.getPrincipal(); | ||||||||||||||||||||||
window.auth.principalText = authClient.getIdentity()?.getPrincipal().toText(); | ||||||||||||||||||||||
window.issuerCanister = createActor(canisterId ?? "", { | ||||||||||||||||||||||
agentOptions: { | ||||||||||||||||||||||
host: "https://icp0.io", | ||||||||||||||||||||||
serapieTuyishime marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
identity: authClient.getIdentity(), | ||||||||||||||||||||||
}, | ||||||||||||||||||||||
}); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
serapieTuyishime marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
|
||||||||||||||||||||||
initializeContract(); | ||||||||||||||||||||||
}, []); | ||||||||||||||||||||||
|
||||||||||||||||||||||
const popupCenter = useCallback((): string | undefined => { | ||||||||||||||||||||||
const AUTH_POPUP_WIDTH = 576; | ||||||||||||||||||||||
const AUTH_POPUP_HEIGHT = 625; | ||||||||||||||||||||||
|
||||||||||||||||||||||
if (typeof window === "undefined" || !window.top) { | ||||||||||||||||||||||
return undefined; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
const { innerWidth, innerHeight, screenX, screenY } = window; | ||||||||||||||||||||||
|
||||||||||||||||||||||
const y = innerHeight / 2 + screenY - AUTH_POPUP_HEIGHT / 2; | ||||||||||||||||||||||
const x = innerWidth / 2 + screenX - AUTH_POPUP_WIDTH / 2; | ||||||||||||||||||||||
|
||||||||||||||||||||||
return `toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=no, copyhistory=no, width=${AUTH_POPUP_WIDTH}, height=${AUTH_POPUP_HEIGHT}, top=${y}, left=${x}`; | ||||||||||||||||||||||
}, []); | ||||||||||||||||||||||
|
||||||||||||||||||||||
async function login(callback: (principal: string) => void) { | ||||||||||||||||||||||
if (!window) return; | ||||||||||||||||||||||
const authClient = window.auth.client; | ||||||||||||||||||||||
|
||||||||||||||||||||||
const isAuthenticated = await authClient.isAuthenticated(); | ||||||||||||||||||||||
if (!isAuthenticated) { | ||||||||||||||||||||||
await authClient?.login({ | ||||||||||||||||||||||
maxTimeToLive: MAX_TTL, | ||||||||||||||||||||||
identityProvider: IDENTITY_PROVIDER, | ||||||||||||||||||||||
windowOpenerFeatures: popupCenter(), | ||||||||||||||||||||||
onSuccess: async () => { | ||||||||||||||||||||||
window.auth.isAuthenticated = await authClient.isAuthenticated(); | ||||||||||||||||||||||
const principal = await authClient.getIdentity()?.getPrincipal().toText(); | ||||||||||||||||||||||
callback(principal); | ||||||||||||||||||||||
}, | ||||||||||||||||||||||
serapieTuyishime marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
}); | ||||||||||||||||||||||
} else { | ||||||||||||||||||||||
const principal = await authClient.getIdentity()?.getPrincipal().toText(); | ||||||||||||||||||||||
callback(principal); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
async function logout() { | ||||||||||||||||||||||
if (!window) return; | ||||||||||||||||||||||
const authClient = window.auth.client; | ||||||||||||||||||||||
authClient.logout(); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
Comment on lines
+88
to
+92
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure As with the Consider adding a check to confirm Await the The Apply this diff to fix the issue: async function logout() {
if (typeof window === 'undefined') return;
const authClient = window.auth.client;
- authClient.logout();
+ await authClient.logout();
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||
|
||||||||||||||||||||||
return { | ||||||||||||||||||||||
login, | ||||||||||||||||||||||
logout, | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
serapieTuyishime marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
export default useIcpAuth; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,13 +14,14 @@ import Head from "next/head"; | |
import { serverSideTranslations } from "next-i18next/serverSideTranslations"; | ||
import { useRouter } from "next/router"; | ||
import { useDispatch } from "@/hooks/useTypedDispatch"; | ||
import { findCertificate } from "@/store/services/profile/certificate.service"; | ||
import { completeIcpCertificate, findCertificate } from "@/store/services/profile/certificate.service"; | ||
import { useTranslation } from "next-i18next"; | ||
import Logo from "@/icons/logo.svg"; | ||
import MintCertificate from "@/components/sections/profile/modals/MintCertificate"; | ||
import { Certificate } from "@/types/certificate"; | ||
import { User } from "@/types/bounty"; | ||
import { IRootState } from "@/store"; | ||
import useIcpAuth from "@/hooks/useIcpAuth"; | ||
|
||
/** | ||
* interface for Achievement multiSelector | ||
|
@@ -45,6 +46,8 @@ const Achievement = () => { | |
const [showMintCertificate, setShowMintCertificate] = useState(false); | ||
const dispatch = useDispatch(); | ||
const { locale, query } = useRouter(); | ||
const { login } = useIcpAuth(); | ||
const [addCourseToCompletionPending, setAddCourseToCompletionPending] = useState(false); | ||
|
||
const findCertificateById = useCallback(async () => { | ||
await dispatch(findCertificate({ id: query.id as string })); | ||
|
@@ -96,14 +99,38 @@ const Achievement = () => { | |
return user.id === achievement?.user_id; | ||
}, [user, achievement]); | ||
|
||
const mintable = useMemo(() => { | ||
return achievement?.community?.can_mint_certificates; | ||
const isICPSubmission = useMemo(() => { | ||
return achievement?.community.name === "Internet Computer"; | ||
Comment on lines
+102
to
+103
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid direct string comparison for community names. Comparing community names using direct strings can lead to errors if the name changes or has variations. Consider using a constant or an enumeration for the community name. Apply this diff to improve robustness: - return achievement?.community.name === "Internet Computer";
+ const INTERNET_COMPUTER_COMMUNITY = "Internet Computer";
+ return achievement?.community.name === INTERNET_COMPUTER_COMMUNITY;
|
||
}, [achievement]); | ||
|
||
const mintable = useMemo(() => { | ||
return achievement?.community?.can_mint_certificates || isICPSubmission; | ||
}, [achievement, isICPSubmission]); | ||
|
||
const isNotCertificateIcon = useMemo(() => { | ||
return !achievement?.metadata?.image?.includes("/img/certificates/"); | ||
}, [achievement]); | ||
|
||
const addCourseCompletionToTheIssuerCanister = useCallback(async () => { | ||
if (!achievement?.metadata) return; | ||
const { name } = achievement?.metadata; | ||
if (!window.issuerCanister) return; | ||
try { | ||
setAddCourseToCompletionPending(true); | ||
await window.issuerCanister.add_course_completion(name.toLowerCase().replaceAll(" ", "-")); | ||
await dispatch(completeIcpCertificate({ id: achievement.id })); | ||
serapieTuyishime marked this conversation as resolved.
Show resolved
Hide resolved
serapieTuyishime marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} catch (err) { | ||
console.error("Failed to complete course: ", err); | ||
} finally { | ||
setAddCourseToCompletionPending(false); | ||
} | ||
}, [achievement?.id, achievement?.metadata, dispatch, findCertificateById]); | ||
serapieTuyishime marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const onMint = () => { | ||
if (isICPSubmission) return login(() => addCourseCompletionToTheIssuerCanister()); | ||
setShowMintCertificate(true); | ||
}; | ||
|
||
return ( | ||
<> | ||
<Head> | ||
|
@@ -160,11 +187,23 @@ const Achievement = () => { | |
<div className="w-full flex"> | ||
{!achievementMinted && belongsToCurrentUser && <MintCertificate show={showMintCertificate} close={() => setShowMintCertificate(false)} />} | ||
|
||
{belongsToCurrentUser && !minted && ( | ||
<ArrowButton target="__blank" variant="primary" className="flex ml-auto mt-5" onClick={() => setShowMintCertificate(true)}> | ||
{belongsToCurrentUser && !minted && !isICPSubmission && ( | ||
<ArrowButton target="__blank" variant="outline-primary" className="flex ml-auto mt-5" onClick={onMint}> | ||
Mint certificate | ||
</ArrowButton> | ||
)} | ||
{belongsToCurrentUser && !achievement.completed && isICPSubmission && ( | ||
<ArrowButton | ||
target="__blank" | ||
variant="outline-primary" | ||
className="flex ml-auto mt-5" | ||
disabled={addCourseToCompletionPending} | ||
loading={addCourseToCompletionPending} | ||
onClick={onMint} | ||
> | ||
Record Course Completion | ||
</ArrowButton> | ||
)} | ||
serapieTuyishime marked this conversation as resolved.
Show resolved
Hide resolved
|
||
</div> | ||
</div> | ||
)} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
node
22
is not fully supported by many node modules yet. Especially next jsThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, it's needed for some ICPs peer dependencies to work