diff --git a/TODO.md b/TODO.md
index 6776ecd..e157289 100644
--- a/TODO.md
+++ b/TODO.md
@@ -19,7 +19,7 @@
## Progress
- [x] Repo reconnaissance: reviewed `useAccessCheck`, `access-check` screen, `guild detail` screen, and core access-related hooks.
-- [ ] Locate where backend roles/eligibility requirements are computed for access-check.
-- [ ] Implement resolver module + integration points.
-- [ ] Add tests.
-- [ ] Add docs.
+- [x] Locate where backend roles/eligibility requirements are computed for access-check.
+- [x] Implement resolver module + integration points.
+- [x] Add tests (`roleEligibilityResolver.test.ts`, `useMultiChainRoleEligibility.test.ts`, `accessCheckScreen.test.tsx`).
+- [x] Add docs (`docs/multi-chain-role-eligibility.md`).
diff --git a/app/_layout.tsx b/app/_layout.tsx
index d75cf78..c7cd3c0 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -66,6 +66,22 @@ export default function RootLayout() {
>
+
+
+
+
+
+
+
+
+
+
+
=
};
const statusClassName: Record = {
+ resolved: "bg-success/10 text-success border-success/30 dark:bg-green-900/30 dark:text-green-400 dark:border-green-600/50",
+ "timed-out": "bg-amber-100 text-amber-700 border-amber-300 dark:bg-amber-900/30 dark:text-amber-400 dark:border-amber-600/50",
+ error: "bg-error/10 text-error border-error/30 dark:bg-red-900/30 dark:text-red-400 dark:border-red-600/50",
resolved:
"bg-success/10 text-success border-success/30 dark:bg-green-900/30 dark:text-green-400 dark:border-green-600/50",
"timed-out":
@@ -74,6 +77,7 @@ function PerChainEligibilityList({
{roleEligibilityError ? (
+
) : null}
+ {perChainRoleEligibility.map((chain) => (
+
+
+ Chain {chain.chainId}
+
+ {statusCopy[chain.status]}
+
+
+ {chain.resolvedRoles && chain.resolvedRoles.length > 0 ? (
+
+ Roles: {chain.resolvedRoles.join(", ")}
+
+ ) : null}
+ {chain.errorMessage ? (
+ {chain.errorMessage}
+ ) : null}
+
+ ))}
{perChainRoleEligibility.map((chain) => {
const isRetrying = retryingChainIds.includes(chain.chainId);
const canRetry = chain.chainId > 0 && chain.status !== "resolved" && onRetryChain;
@@ -409,6 +437,11 @@ export default function AccessCheck() {
accessibilityHint="Enter the guild identifier"
testID="access-check-guild-id-input"
/>
+ {guildIdError && {guildIdError}}
+
+
+
+ Resource ID
{guildIdError && (
{guildIdError}
)}
@@ -427,6 +460,7 @@ export default function AccessCheck() {
accessibilityHint="Enter the resource identifier"
testID="access-check-resource-id-input"
/>
+ {resourceIdError && {resourceIdError}}
{resourceIdError && (
{resourceIdError}
)}
@@ -468,6 +502,7 @@ export default function AccessCheck() {
accessibilityLabel="Wallet address mismatch warning. This QR payload uses a different wallet address from your connected wallet."
>
Wallet address mismatch
+ {walletMismatchWarning}
{walletMismatchWarning}
@@ -512,6 +547,11 @@ export default function AccessCheck() {
Guild ID
+ {scannedPayload.guildId}
+
+
+ Resource ID
+ {scannedPayload.resourceId}
{scannedPayload.guildId}
@@ -556,6 +596,7 @@ export default function AccessCheck() {
}}
>
{countdown.isExpired && scannedPayload?.expiresAt && (
+
{
export default function AccessScanner() {
const router = useRouter();
const [permission, requestPermission] = useCameraPermissions();
+ const [scanError, setScanError] = useState<{ message: string; isUntrusted: boolean } | null>(null);
const [scanError, setScanError] = useState<{ message: string; isUntrusted: boolean } | null>(
null,
);
@@ -140,6 +144,19 @@ export default function AccessScanner() {
setScanError(null);
AccessibilityInfo.announceForAccessibility("Processing access QR code.");
+ const result = await verifyAndParseAccessQrPayload(data);
+
+ try {
+ AccessibilityInfo.announceForAccessibility("Processing access QR code.");
+ await verifyAndParseAccessQrPayload(data);
+ setVerificationSuccess(true);
+ AccessibilityInfo.announceForAccessibility("Signature verified. Opening access check.");
+
+ setTimeout(() => {
+ setVerificationSuccess(false);
+ router.replace({ pathname: "/access-check", params: { qrPayload: data } });
+ }, 1500);
+ return;
try {
AccessibilityInfo.announceForAccessibility("Processing access QR code.");
const result = await verifyAndParseAccessQrPayload(data);
@@ -194,6 +211,7 @@ export default function AccessScanner() {
if (error instanceof QrSignatureError) {
errorMessage = describeQrSignatureError(error.code);
+ isUntrusted = true;
isUntrusted = !isRecoverableError(error);
} else if (error instanceof QrPayloadError) {
if (
@@ -208,6 +226,8 @@ export default function AccessScanner() {
} else {
errorMessage = error.message;
}
+ } else if (error instanceof Error) {
+ errorMessage = error.message;
errorMessage = describeQrPayloadError(error.code);
isUntrusted = !isRecoverableError(error);
}
@@ -244,6 +264,7 @@ export default function AccessScanner() {
+ Checking camera permission...
Checking camera permission...
@@ -266,6 +287,9 @@ export default function AccessScanner() {
+ Camera access needed
+
+ GuildPass needs camera permission to scan access check QR codes.
) : (
+ <>
+
+ Camera permission was denied. Enable camera access in your device settings to scan
+ QR codes.
+
+
+ Camera permission was denied. Open Settings to enable camera access for GuildPass.
+
+