Skip to content

feat: qr document loader#62

Closed
RishabhS7 wants to merge 0 commit into
developfrom
feat/qr-document-loader
Closed

feat: qr document loader#62
RishabhS7 wants to merge 0 commit into
developfrom
feat/qr-document-loader

Conversation

@RishabhS7

@RishabhS7 RishabhS7 commented May 21, 2026

Copy link
Copy Markdown
Contributor

feat: QR document loader via action URL (?q=)

What this does

Adds support for loading and verifying a TrustVC document directly from a URL query parameter — the same mechanism used by QR codes generated from TradeTrust/TrustVC.

When the app is visited with a ?q= param:

https://actions.trustvc.io/?q={"type":"DOCUMENT","payload":{"uri":"...","chainId":11155111}}

the app automatically fetches the document, decrypts it if encrypted, runs verification, and displays the result — with no manual file upload needed.


Changes

ActionLoader.tsx (new)

  • Reads ?q= from the URL on mount
  • Decodes the action JSON (type, payload.uri, payload.chainId, payload.key)
  • Reads decryption key from URL hash anchor (#) — takes priority over payload.key
  • Fetches the document; unwraps OpenCerts { document: ... } wrapper
  • Decrypts OA-encrypted documents (OPEN-ATTESTATION-TYPE-1) via @govtechsg/oa-encryption
  • Calls loadDocument to run the existing verification flow
  • Cleans the URL immediately (navigate('/', { replace: true })) to prevent re-trigger on refresh
  • Renders nothing (null)

useVerify.ts

  • Exposes loadDocument(doc, chainId, name) — mirrors processFile but accepts an already-fetched document; plugs into the same runVerification path

VerifySection.tsx

  • Mounts <ActionLoader loadDocument={loadDocument} /> at the top of the component

@govtechsg/oa-encryption (new dependency)

  • Used for decryptString to handle OPEN-ATTESTATION-TYPE-1 encrypted documents

Tests

File New tests
ActionLoader.test.tsx 18 — happy path, opencerts wrapper, filename derivation, chainId coercion, no-op without ?q=, early returns for unsupported types, fetch errors, invalid JSON, OA decryption (key from payload + anchor), missing key error
useVerify.test.ts 6 — loadDocument valid/invalid/error flows, verifying state transition, second call overwrites state, null chainId
VerifySection.test.tsx `loadDocument: vi.fn

Jira Ticket

Summary by CodeRabbit

Release Notes

  • New Features
    • Added support for loading documents directly via URL query parameters, enabling seamless document sharing and verification workflows
    • Implemented automatic decryption for encrypted document types when appropriate keys are available
    • Enhanced the document verification process to efficiently handle externally sourced documents with automatic URL cleanup

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR adds URL-driven document loading with OA-encryption support. A new ActionLoader component parses encrypted documents from query parameters, optionally decrypts them using keys from URL anchors, and triggers verification via a loadDocument callback integrated into the useVerify hook and rendered in VerifySection.

Changes

URL-driven document loading with OA encryption

Layer / File(s) Summary
Dependencies and type contracts
package.json, src/components/home/VerifySection/ActionLoader.tsx, src/components/home/VerifySection/useVerify.ts
@govtechsg/oa-encryption dependency added; ActionLoaderProps defines the loadDocument(doc, chainId, name) callback contract; UseVerifyReturn extended with the same signature.
ActionLoader implementation and tests
src/components/home/VerifySection/ActionLoader.tsx, src/components/home/VerifySection/ActionLoader.test.tsx
ActionLoader reads ?q= parameter and optional URL-hash key, clears the URL to prevent re-triggering, fetches the document URI, unwraps OpenCerts wrappers, optionally decrypts OPEN-ATTESTATION-TYPE-1 payloads (anchor key preferred), derives filename from URI, and calls loadDocument. Extensive tests cover no-op/URL cleaning, happy path, early returns, error handling without throwing, and OA-encryption flows.
useVerify hook extension
src/components/home/VerifySection/useVerify.ts, src/components/home/VerifySection/useVerify.test.ts
useVerify gains loadDocument(doc, chainId, name) method that reuses file-verification flow with stale-request id protection, metadata reset, and error handling. Tests verify state transitions (verifyingvalid/invalid/error), filename updates, and null chainId handling.
VerifySection integration
src/components/home/VerifySection/VerifySection.tsx, src/components/home/VerifySection/VerifySection.test.tsx
VerifySection imports and renders ActionLoader above existing UI, wiring the loadDocument callback from useVerify; test mock updated to include loadDocument.

Sequence Diagram

sequenceDiagram
  participant Location as URL Hash/Query
  participant ActionLoader
  participant Fetch as Document Server
  participant Decrypt as decryptString
  participant LoadDoc as loadDocument callback
  Location->>ActionLoader: q parameter + anchor key
  ActionLoader->>ActionLoader: parse q (JSON)
  ActionLoader->>ActionLoader: clear URL navigate('/', replace)
  ActionLoader->>Fetch: fetch payload.uri
  Fetch-->>ActionLoader: document JSON
  alt document.type = OPEN-ATTESTATION-TYPE-1
    ActionLoader->>Decrypt: decryptString(document, key)
    Decrypt-->>ActionLoader: decrypted payload
  else
    ActionLoader->>ActionLoader: use document as-is
  end
  ActionLoader->>ActionLoader: unwrap { document }
  ActionLoader->>ActionLoader: derive filename from uri
  ActionLoader->>LoadDoc: call with doc, chainId, filename
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • TrustVC/trustvc-website#11: Implements DocumentRenderer that displays documents passed through VerifyResult, which the main PR now supports via loadDocument integration.
  • TrustVC/trustvc-website#55: Also modifies VerifySection and useVerify (adding isExpired tracking), so both PRs update the same verification hook and component code paths.
  • TrustVC/trustvc-website#9: Implements EndorsementChain rendering and useEndorsementChain hook that VerifySection conditionally renders alongside the new ActionLoader flow.

Poem

🐰 A query string hops through the URL,
Encrypted docs dance in the hash—
ActionLoader decrypts them all,
While useVerify handles the task,
And VerifySection binds it fast! 🔐✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: qr document loader' is directly related to the main change—adding QR/action URL document loading functionality via the ?q= parameter. It accurately summarizes the core feature addition.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/qr-document-loader

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/home/VerifySection/ActionLoader.tsx`:
- Around line 23-24: Move the potentially-throwing decode logic inside the
loader's try/catch and stop double-decoding the `q` param: don't call
decodeURIComponent on `location.hash` at top-level (replace const anchorStr =
decodeURIComponent(location.hash.substring(1)) with a safe read like const
rawHash = location.hash.substring(1)), then inside the existing try block do a
guarded decode for the hash (wrap decodeURIComponent(rawHash) in try/catch and
fallback to '{}' or an empty string on failure) to populate `anchor` (the
variable currently declared as const anchor: { key?: string }), and when reading
the search params use URLSearchParams.get('q') directly and pass that result
into JSON.parse (do NOT call decodeURIComponent on the `q` value since get
already returns a decoded string); also null-check the result of get('q') before
JSON.parse to avoid exceptions.

In `@src/components/home/VerifySection/useVerify.ts`:
- Around line 516-535: loadDocument currently proceeds to runVerification even
when chainId is missing, which bypasses the same "missing-chain /
network-select" branch used in processFile and risks verifying against a wrong
default chain; change loadDocument so that if chainId is null or undefined it
invokes the same missing-chain handling used by processFile (for example call
the existing network-select opener/handler such as setShowNetworkSelect or
handleMissingChain) or return early instead of calling runVerification, keeping
the rest of the verification-init logic (incrementing verificationIdRef,
setFileName, setVerifyStatus, clearing metadata) intact and ensuring you still
pass currentId to runVerification when a valid chainId is present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e008132c-7ba2-443d-b2e1-6d0b766de290

📥 Commits

Reviewing files that changed from the base of the PR and between 1c0d46c and 02524fa.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • package.json
  • src/components/home/VerifySection/ActionLoader.test.tsx
  • src/components/home/VerifySection/ActionLoader.tsx
  • src/components/home/VerifySection/VerifySection.test.tsx
  • src/components/home/VerifySection/VerifySection.tsx
  • src/components/home/VerifySection/useVerify.test.ts
  • src/components/home/VerifySection/useVerify.ts

Comment on lines +23 to +24
const anchorStr = decodeURIComponent(location.hash.substring(1))
const anchor: { key?: string } = anchorStr

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate file (in case of path differences)
ls -la src/components/home/VerifySection || true

# Show the relevant file with line numbers around the cited lines
if [ -f "src/components/home/VerifySection/ActionLoader.tsx" ]; then
  sed -n '1,140p' src/components/home/VerifySection/ActionLoader.tsx | nl -ba | sed -n '1,140p'
else
  echo "File not found: src/components/home/VerifySection/ActionLoader.tsx"
  exit 1
fi

# Focus on decoding usage
rg -n "decodeURIComponent|URLSearchParams|location\.hash|location\.search|substring\(1\)" src/components/home/VerifySection/ActionLoader.tsx

Repository: TrustVC/trustvc-website

Length of output: 1723


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/components/home/VerifySection/ActionLoader.tsx"

# Show the file with line numbers (cat -n is available)
if [ -f "$FILE" ]; then
  echo "=== File: $FILE ==="
  sed -n '1,120p' "$FILE" | cat -n
else
  echo "File not found: $FILE"
  exit 1
fi

echo
echo "=== rg matches ==="
rg -n "decodeURIComponent|URLSearchParams|location\.hash|location\.search|substring\(1\)|JSON\.parse" "$FILE"

Repository: TrustVC/trustvc-website

Length of output: 3898


Avoid double-decoding q and guard malformed hash decoding.

  • location.hash is decoded (decodeURIComponent(location.hash.substring(1))) outside the async try/catch; decode failures will escape and abort the loader.
  • q is decoded again via JSON.parse(decodeURIComponent(query)) even though URLSearchParams.get('q') already returns a decoded value.
💡 Suggested fix
 export const ActionLoader: React.FC<ActionLoaderProps> = ({ loadDocument }) => {
   const location = useLocation()
   const navigate = useNavigate()

   useEffect(() => {
+    const safeDecode = (value: string): string => {
+      try {
+        return decodeURIComponent(value)
+      } catch {
+        return value
+      }
+    }
+
     const params = new URLSearchParams(location.search)
     const query = params.get('q')
     if (!query) return

     // Decode optional decryption key from URL hash — anchor key takes priority over payload key
-    const anchorStr = decodeURIComponent(location.hash.substring(1))
+    const anchorStr = safeDecode(location.hash.substring(1))
     const anchor: { key?: string } = anchorStr
       ? (() => {
           try {
             return JSON.parse(anchorStr)
           } catch {
             return {}
           }
         })()
       : {}

     // Clean URL immediately so a refresh doesn't re-trigger
     navigate('/', { replace: true })
     ;(async () => {
       try {
-        const action = JSON.parse(decodeURIComponent(query))
+        const action = JSON.parse(query)
         const { type, payload } = action ?? {}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const anchorStr = decodeURIComponent(location.hash.substring(1))
const anchor: { key?: string } = anchorStr
export const ActionLoader: React.FC<ActionLoaderProps> = ({ loadDocument }) => {
const location = useLocation()
const navigate = useNavigate()
useEffect(() => {
const safeDecode = (value: string): string => {
try {
return decodeURIComponent(value)
} catch {
return value
}
}
const params = new URLSearchParams(location.search)
const query = params.get('q')
if (!query) return
// Decode optional decryption key from URL hash — anchor key takes priority over payload key
const anchorStr = safeDecode(location.hash.substring(1))
const anchor: { key?: string } = anchorStr
? (() => {
try {
return JSON.parse(anchorStr)
} catch {
return {}
}
})()
: {}
// Clean URL immediately so a refresh doesn't re-trigger
navigate('/', { replace: true })
;(async () => {
try {
const action = JSON.parse(query)
const { type, payload } = action ?? {}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/home/VerifySection/ActionLoader.tsx` around lines 23 - 24,
Move the potentially-throwing decode logic inside the loader's try/catch and
stop double-decoding the `q` param: don't call decodeURIComponent on
`location.hash` at top-level (replace const anchorStr =
decodeURIComponent(location.hash.substring(1)) with a safe read like const
rawHash = location.hash.substring(1)), then inside the existing try block do a
guarded decode for the hash (wrap decodeURIComponent(rawHash) in try/catch and
fallback to '{}' or an empty string on failure) to populate `anchor` (the
variable currently declared as const anchor: { key?: string }), and when reading
the search params use URLSearchParams.get('q') directly and pass that result
into JSON.parse (do NOT call decodeURIComponent on the `q` value since get
already returns a decoded string); also null-check the result of get('q') before
JSON.parse to avoid exceptions.

Comment on lines +516 to +535
const loadDocument = async (
doc: unknown,
chainId: string | null | undefined,
name: string
) => {
const currentId = ++verificationIdRef.current
setFileName(name)
setVerifyStatus('verifying')
setFragments([])
setPendingDoc(null)
clearVerificationMetadata()

try {
await runVerification(doc, chainId, currentId)
} catch (err) {
clearVerificationMetadata()
setErrorType(getErrorTypeFromError(err))
setVerifyStatus('error')
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep loadDocument consistent with missing-chain handling in processFile.

When chainId is absent, loadDocument skips the network-select branch used elsewhere and can verify against the wrong default chain. This is especially risky because action payload chainId is optional.

💡 Suggested fix
   const loadDocument = async (
     doc: unknown,
     chainId: string | null | undefined,
     name: string
   ) => {
@@
     try {
+      if (!chainId && (isTransferableRecord(doc as any) || isDocumentRevokable(doc as any))) {
+        setPendingDoc(doc)
+        setVerifyStatus('network-select')
+        return
+      }
       await runVerification(doc, chainId, currentId)
     } catch (err) {
       clearVerificationMetadata()
       setErrorType(getErrorTypeFromError(err))
       setVerifyStatus('error')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/home/VerifySection/useVerify.ts` around lines 516 - 535,
loadDocument currently proceeds to runVerification even when chainId is missing,
which bypasses the same "missing-chain / network-select" branch used in
processFile and risks verifying against a wrong default chain; change
loadDocument so that if chainId is null or undefined it invokes the same
missing-chain handling used by processFile (for example call the existing
network-select opener/handler such as setShowNetworkSelect or
handleMissingChain) or return early instead of calling runVerification, keeping
the rest of the verification-init logic (incrementing verificationIdRef,
setFileName, setVerifyStatus, clearing metadata) intact and ensuring you still
pass currentId to runVerification when a valid chainId is present.

@RishabhS7 RishabhS7 changed the base branch from main to develop May 21, 2026 02:34
@RishabhS7 RishabhS7 closed this May 21, 2026
@RishabhS7 RishabhS7 force-pushed the feat/qr-document-loader branch from 02524fa to a5085e0 Compare May 21, 2026 07:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant