Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5bb6618
fix(citizen-sdk): resolve whitelisted root for connected accounts
Ryjen1 Feb 1, 2026
e480b45
docs(citizen-sdk): add connected accounts claiming flow guide
Ryjen1 Feb 1, 2026
e5ac108
Adding a standalone script to verify connected account logic off-chain
Ryjen1 Feb 1, 2026
9bd79df
Adding documentation for the new connected accounts test script
Ryjen1 Feb 1, 2026
c1df3a9
Adding a simple shell script to make running the connected account te…
Ryjen1 Feb 1, 2026
e448c83
Updating test script defaults to use a real verified whitelisted address
Ryjen1 Feb 1, 2026
d7f059e
chore(citizen-sdk): remove hardcoded test addresses in favor of envir…
Ryjen1 Feb 1, 2026
902eddd
chore(citizen-sdk): use zero-address placeholders in test scripts for…
Ryjen1 Feb 1, 2026
ccae930
Address PR review feedback
Ryjen1 Feb 1, 2026
8d179ce
Implement comprehensive error handling and address all review feedback
Ryjen1 Feb 1, 2026
3f0642f
refactor(citizen-sdk): convert connected accounts test to TypeScript …
Ryjen1 Feb 2, 2026
edc8094
chore(citizen-sdk): update test runner and docs for TypeScript migration
Ryjen1 Feb 2, 2026
efd20d6
chore(citizen-sdk): bump version and add tsx devDependency
Ryjen1 Feb 2, 2026
e218df6
Move test files to test directory
Feb 4, 2026
0955bef
Chore: Update yarn.lock and cleanup test script
Feb 4, 2026
a1f19d2
move tests to main test folder
Feb 9, 2026
7e52bd0
add support for connected accounts to claim SDK
Feb 9, 2026
0fe046d
add env file for test accounts
Feb 9, 2026
a927b9a
chore: final cleanup for connected accounts
Feb 9, 2026
547951f
docs: simplify test README
Feb 9, 2026
18bde41
chore: fix pre-existing linting errors and add missing config
Feb 9, 2026
aca253c
Fix lint issues in engagement app hooks
Feb 12, 2026
1cfd097
Refine SDK logic and remove caching
Feb 12, 2026
0344180
Migrate tests to Vitest and cleanup old test suite
Feb 12, 2026
5096bf4
refactor: use identitySDK directly for root address resolution
Feb 16, 2026
f6eb039
build: remove unused tsx dependency and update test script
Feb 16, 2026
7442ed9
test: relocate connected accounts tests to package root
Feb 16, 2026
6132ef6
feat(demo): support connected accounts in IdentityCard via getWhiteli…
Feb 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions apps/demo-identity-app/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"root": true,
"env": {
"browser": true,
"es2020": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
"prettier"
],
"ignorePatterns": [
"dist",
".eslintrc.json"
],
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"react-hooks"
],
"rules": {}
}
2 changes: 1 addition & 1 deletion apps/demo-identity-app/src/components/ClaimButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const ClaimButton: React.FC = () => {
const [txHash, setTxHash] = useState<string | null>(null)
const { sdk: claimSDK, loading, error: sdkError } = useClaimSDK("development")
const [sdk, setSdk] = useState<typeof claimSDK | null>(null)
const [claimAmount, setClaimAmount] = useState<Number | null>(null)
const [claimAmount, setClaimAmount] = useState<number | null>(null)
const [altClaimAvailable, setAltClaimAvailable] = useState(false)
const [altChainId, setAltChainId] = useState<SupportedChains | null>(null)

Expand Down
59 changes: 41 additions & 18 deletions apps/demo-identity-app/src/components/IdentityCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,46 @@ export const IdentityCard: React.FC = () => {
const { address } = useAccount()
const { sdk: identitySDK, loading, error } = useIdentitySDK("development")
const [expiry, setExpiry] = useState<string | undefined>(undefined)
const [isWhitelisted, setIsWhitelisted] = useState<boolean | undefined>(undefined)
const [rootAddress, setRootAddress] = useState<string | undefined>(undefined)

useEffect(() => {
const fetchExpiry = async () => {
const fetchIdentityData = async () => {
if (!identitySDK || !address) return

const identityExpiry = await identitySDK.getIdentityExpiryData(address)
try {
const { isWhitelisted: whitelisted, root } =
await identitySDK.getWhitelistedRoot(address)

const { expiryTimestamp } = identitySDK.calculateIdentityExpiry(
identityExpiry?.lastAuthenticated ?? BigInt(0),
identityExpiry?.authPeriod ?? BigInt(0),
)
setIsWhitelisted(whitelisted)
setRootAddress(root)

const date = new Date(Number(expiryTimestamp))
const formattedExpiryTimestamp = date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "2-digit",
})
if (whitelisted) {
const identityExpiry = await identitySDK.getIdentityExpiryData(root as `0x${string}`)

if (formattedExpiryTimestamp) {
setExpiry(formattedExpiryTimestamp)
const { expiryTimestamp } = identitySDK.calculateIdentityExpiry(
identityExpiry?.lastAuthenticated ?? BigInt(0),
identityExpiry?.authPeriod ?? BigInt(0),
)

const date = new Date(Number(expiryTimestamp))
const formattedExpiryTimestamp = date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "2-digit",
})

setExpiry(formattedExpiryTimestamp)
} else {
setExpiry(undefined)
}
} catch (err) {
console.error("fetchIdentityData error:", err)
}
}

if (identitySDK) {
fetchExpiry()
fetchIdentityData()
}
}, [address, identitySDK])

Expand All @@ -53,12 +67,21 @@ export const IdentityCard: React.FC = () => {
) : (
<>
<Text color="$text" fontSize="$medium" mb="$small">
Wallet Address: {address}
Connected Wallet: {address.slice(0, 6)}...{address.slice(-4)}
</Text>
{isWhitelisted && rootAddress && rootAddress.toLowerCase() !== address.toLowerCase() && (
<Text color="$text" fontSize="$medium" mb="$small">
Root Identity: {rootAddress.slice(0, 6)}...{rootAddress.slice(-4)}
</Text>
)}
<Text color="$text" fontSize="$medium">
{expiry && new Date(expiry) < new Date() ? "Expired" : "Expiry"}:{" "}
{expiry || "Not Verified"}
Status: {isWhitelisted ? "Whitelisted" : "Not Whitelisted"}
</Text>
{isWhitelisted && expiry && (
<Text color="$text" fontSize="$medium" mt="$small">
{new Date(expiry) < new Date() ? "Expired" : "Expiry"}: {expiry}
</Text>
)}
</>
)}
</View>
Expand Down
4 changes: 1 addition & 3 deletions apps/demo-identity-app/src/components/VerifyButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ interface VerifyButtonProps {
onVerificationSuccess: () => void
}

export const VerifyButton: React.FC<VerifyButtonProps> = ({
onVerificationSuccess,
}) => {
export const VerifyButton: React.FC<VerifyButtonProps> = () => {
const { address } = useAccount()
const { sdk: identitySDK } = useIdentitySDK("development")

Expand Down
129 changes: 60 additions & 69 deletions apps/engagement-app/src/hooks/use-toast.ts
Original file line number Diff line number Diff line change
@@ -1,103 +1,94 @@
"use client";
"use client"

// Inspired by react-hot-toast library
import * as React from "react";
import * as React from "react"

import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
import type { ToastActionElement, ToastProps } from "@/components/ui/toast"

const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000

type ToasterToast = ToastProps & {
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};

const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}

let count = 0;
let count = 0

function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString();
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}

type ActionType = typeof actionTypes;

type Action =
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
type: "ADD_TOAST"
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
type: "UPDATE_TOAST"
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
type: "DISMISS_TOAST"
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
type: "REMOVE_TOAST"
toastId?: ToasterToast["id"]
}

interface State {
toasts: ToasterToast[];
toasts: ToasterToast[]
}

const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()

const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return;
return
}

const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
})
}, TOAST_REMOVE_DELAY)

toastTimeouts.set(toastId, timeout);
};
toastTimeouts.set(toastId, timeout)
}

export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
}

case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t,
),
};
}

case "DISMISS_TOAST": {
const { toastId } = action;
const { toastId } = action

// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
addToRemoveQueue(toast.id)
})
}

return {
Expand All @@ -110,44 +101,44 @@ export const reducer = (state: State, action: Action): State => {
}
: t,
),
};
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
}
};
}

const listeners: Array<(state: State) => void> = [];
const listeners: Array<(state: State) => void> = []

let memoryState: State = { toasts: [] };
let memoryState: State = { toasts: [] }

function dispatch(action: Action) {
memoryState = reducer(memoryState, action);
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState);
});
listener(memoryState)
})
}

type Toast = Omit<ToasterToast, "id">;
type Toast = Omit<ToasterToast, "id">

function toast({ ...props }: Toast) {
const id = genId();
const id = genId()

const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })

dispatch({
type: "ADD_TOAST",
Expand All @@ -156,36 +147,36 @@ function toast({ ...props }: Toast) {
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
if (!open) dismiss()
},
},
});
})

return {
id: id,
dismiss,
update,
};
}
}

function useToast() {
const [state, setState] = React.useState<State>(memoryState);
const [state, setState] = React.useState<State>(memoryState)

React.useEffect(() => {
listeners.push(setState);
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState);
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1);
listeners.splice(index, 1)
}
};
}, [state]);
}
}, [state])

return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
}

export { useToast, toast };
export { useToast, toast }
Loading