diff --git a/.env.example b/.env.example
index e69de29..1af6996 100644
--- a/.env.example
+++ b/.env.example
@@ -0,0 +1,26 @@
+VITE_SUPPORT_API_BASE_URL=
+
+# reCAPTCHA: site key (frontend).
+# Must match API secret key server-side.
+# In production, this must be set; do not allow verification bypass when missing.
+VITE_RECAPTCHA_SITE_KEY=
+
+# Optional: platform/environment for Jira (e.g. dev, staging, prod). Default: dev
+VITE_PLATFORM=dev
+
+VITE_NETWORK_TYPE=mainnet
+
+# Per-chain RPC overrides (key = chain ID)
+# Mainnet
+VITE_RPC_URL_1=
+VITE_RPC_URL_137=
+VITE_RPC_URL_50=
+VITE_RPC_URL_101010=
+VITE_RPC_URL_1338=
+
+# Testnet
+VITE_RPC_URL_11155111=
+VITE_RPC_URL_80002=
+VITE_RPC_URL_51=
+VITE_RPC_URL_20180427=
+VITE_RPC_URL_21002=
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9390336..f59f66e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -33,6 +33,9 @@ jobs:
- name: Run tests with coverage
run: npm run test:coverage
+ env:
+ VITE_RPC_URL_1: ${{ secrets.VITE_RPC_URL_1 }}
+ VITE_RPC_URL_101010: ${{ secrets.VITE_RPC_URL_101010 }}
build:
runs-on: ubuntu-latest
diff --git a/.github/workflows/deploy-trustvc-website.yml b/.github/workflows/deploy-trustvc-website.yml
new file mode 100644
index 0000000..c535293
--- /dev/null
+++ b/.github/workflows/deploy-trustvc-website.yml
@@ -0,0 +1,115 @@
+name: Deploy TrustVC Website
+
+on:
+ workflow_dispatch:
+ inputs:
+ branch:
+ description: 'Branch to deploy (always deploys from main)'
+ required: true
+ default: 'main'
+ type: choice
+ options:
+ - main
+ - develop
+ environment:
+ description: 'Deployment environment'
+ required: true
+ default: 'development'
+ type: choice
+ options:
+ - development
+ - production
+
+permissions:
+ contents: read
+
+jobs:
+ build-and-deploy:
+ name: Build and deploy to CloudFront
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ # id-token: write # enable when using OIDC for AWS (no long-lived keys)
+
+ env:
+ # Map environment input to S3 bucket and CloudFront distribution
+ AWS_REGION: ap-southeast-1
+ DEPLOY_ENV: ${{ github.event.inputs.environment }}
+
+ S3_BUCKET_DEVELOPMENT: ${{ secrets.TRUSTVC_WEB_S3_BUCKET_DEVELOPMENT }}
+ S3_BUCKET_PRODUCTION: ${{ secrets.TRUSTVC_WEB_S3_BUCKET_PRODUCTION }}
+
+ CF_DISTRIBUTION_DEVELOPMENT: ${{ secrets.TRUSTVC_WEB_CF_DISTRIBUTION_DEVELOPMENT }}
+ CF_DISTRIBUTION_PRODUCTION: ${{ secrets.TRUSTVC_WEB_CF_DISTRIBUTION_PRODUCTION }}
+
+ # Full .env content per environment (one line or multiline, as in your local .env)
+ ENV_FILE_DEVELOPMENT: ${{ secrets.TRUSTVC_WEB_ENV_DEVELOPMENT }}
+ ENV_FILE_PRODUCTION: ${{ secrets.TRUSTVC_WEB_ENV_PRODUCTION }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ ref: main
+
+ - name: Use Node.js 20
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'npm'
+ cache-dependency-path: package-lock.json
+
+ - name: Create .env for build
+ run: |
+ if [ "${DEPLOY_ENV}" = "production" ]; then
+ echo "${ENV_FILE_PRODUCTION}" > .env
+ else
+ echo "${ENV_FILE_DEVELOPMENT}" > .env
+ fi
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Lint
+ run: npm run lint
+
+ - name: Build
+ run: npm run build
+
+ - name: Configure AWS credentials
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+ aws-region: ${{ env.AWS_REGION }}
+
+ - name: Determine target bucket and distribution
+ id: env-map
+ shell: bash
+ run: |
+ if [ "${DEPLOY_ENV}" = "production" ]; then
+ echo "s3_bucket=${S3_BUCKET_PRODUCTION}" >> "$GITHUB_OUTPUT"
+ echo "distribution_id=${CF_DISTRIBUTION_PRODUCTION}" >> "$GITHUB_OUTPUT"
+ echo "deploy_env_name=production" >> "$GITHUB_OUTPUT"
+ else
+ echo "s3_bucket=${S3_BUCKET_DEVELOPMENT}" >> "$GITHUB_OUTPUT"
+ echo "distribution_id=${CF_DISTRIBUTION_DEVELOPMENT}" >> "$GITHUB_OUTPUT"
+ echo "deploy_env_name=development" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Sync assets to S3
+ if: steps.env-map.outputs.s3_bucket != ''
+ run: |
+ BUCKET="${{ steps.env-map.outputs.s3_bucket }}"
+ # Long cache only for hashed assets (JS/CSS/fonts); never cache HTML at edge/browser for 1 year
+ aws s3 sync ./dist "s3://${BUCKET}" --delete --exclude "*.html" --cache-control "public, max-age=31536000"
+ # Short cache for HTML entry points so browsers always get fresh index.html (and new asset refs) after deploy
+ aws s3 sync ./dist "s3://${BUCKET}" --cache-control "public, max-age=0, must-revalidate" --exclude "*" --include "*.html"
+
+ - name: Invalidate CloudFront cache
+ if: steps.env-map.outputs.distribution_id != ''
+ run: |
+ aws cloudfront create-invalidation \
+ --distribution-id "${{ steps.env-map.outputs.distribution_id }}" \
+ --paths "/*"
+
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100755
index 0000000..d5887cd
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,6 @@
+#!/usr/bin/env sh
+set -e
+
+echo "π Running pre-commit checks..."
+npm run lint
+npm run format:check
diff --git a/eslint b/eslint
new file mode 100644
index 0000000..e69de29
diff --git a/eslint.config.js b/eslint.config.js
index 3739a07..f0eaa74 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -7,12 +7,15 @@ import prettier from 'eslint-config-prettier'
import tseslint from 'typescript-eslint'
export default [
- { ignores: ['dist', 'coverage'] },
+ { ignores: ['dist', 'coverage', 'scripts/**/*'] },
{
files: ['**/*.{js,jsx,ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
- globals: globals.browser,
+ globals: {
+ ...globals.browser,
+ ...globals.node,
+ },
parser: tseslint.parser,
parserOptions: {
ecmaVersion: 'latest',
@@ -25,6 +28,8 @@ export default [
'tailwind.config.js',
'vite.config.js',
'src/test/setup.js',
+ 'src/shims/dotenv-config.js',
+ 'src/shims/node-fetch.js',
],
},
},
@@ -48,6 +53,14 @@ export default [
'warn',
{ allowConstantExport: true },
],
+ '@typescript-eslint/no-unused-vars': [
+ 'error',
+ {
+ argsIgnorePattern: '^_',
+ varsIgnorePattern: '^_',
+ },
+ ],
+ 'no-unused-vars': 'off',
},
},
]
diff --git a/index.html b/index.html
index 08b6e0b..df0c1f1 100644
--- a/index.html
+++ b/index.html
@@ -2,7 +2,7 @@
-
+
)
}
diff --git a/src/__tests__/__fixtures__/oa/2.0/oa_dns_txt_docstore_no_network_field_ethereum_v2.json b/src/__tests__/__fixtures__/oa/2.0/oa_dns_txt_docstore_no_network_field_ethereum_v2.json
new file mode 100644
index 0000000..321aa74
--- /dev/null
+++ b/src/__tests__/__fixtures__/oa/2.0/oa_dns_txt_docstore_no_network_field_ethereum_v2.json
@@ -0,0 +1,92 @@
+{
+ "version": "https://schema.openattestation.com/2.0/schema.json",
+ "data": {
+ "id": "99058267-23e7-4540-977b-ef9790bb21c0:string:53b75bbe",
+ "name": "c763cfdd-f543-4258-a69a-c49fa9115d77:string:Opencerts Demo Certificate",
+ "description": "41571338-6f96-43c2-82bb-2886e2c0f5e7:string:Opencerts Demo Certificate",
+ "issuedOn": "43ad0d61-48f0-4cb1-ab2a-a3ce0d4862b6:string:2025-05-29T00:00:00+08:00",
+ "admissionDate": "f91e4fa4-6e7b-48b0-85ad-1ad7acd460e7:string:2020-08-01T00:00:00+08:00",
+ "graduationDate": "fb9be3b8-96db-418e-8940-ab45ad1ff246:string:2025-08-01T00:00:00+08:00",
+ "$template": {
+ "name": "4bb2ef9c-e9c1-486c-9f65-208a0e1d666f:string:OPENCERTS_DEMO",
+ "type": "c8028dfa-6d6e-4ea7-a5e9-ea99a41da40b:string:EMBEDDED_RENDERER",
+ "url": "d98a329b-67c9-4fec-bca5-3b0785883632:string:https://demo-renderer.opencerts.io"
+ },
+ "issuers": [
+ {
+ "name": "60f44aa6-318b-45a1-b3cb-7611d3fdfebc:string:Opencerts",
+ "documentStore": "4aee735e-ba44-40b5-bde4-62a2629295e9:string:0x641bDE53Df8C249dD123e532764420Ed82cfb664",
+ "identityProof": {
+ "type": "6180bc6f-7ffe-4002-8ca0-a2dc7a06f259:string:DNS-TXT",
+ "location": "567ea620-4e2b-4557-8fc3-65f7c9941a82:string:opencerts.io"
+ }
+ }
+ ],
+ "recipient": {
+ "name": "b35e2000-f5fc-4648-b8f0-e60e47b91688:string:Your Name",
+ "nric": "889d200a-2f77-4436-b24f-66da01f6f977:string:SXXXXXXXY",
+ "course": "976359b5-abd7-4589-ab9d-626bef6b87b4:string:OpenCerts Demo"
+ },
+ "transcript": [
+ {
+ "name": "80220920-efb3-4891-ba4d-f65ecbe94e08:string:Introduction to Programming",
+ "grade": "0c11d304-8a33-4607-8500-ace83f64a84b:string:A+",
+ "courseCredit": "ca5023ca-e1ba-464c-970a-3e95c800886d:string:3",
+ "courseCode": "149388ce-d243-4214-947d-bdce88275248:string:CS 1110",
+ "examinationDate": "4b56a0b6-911b-469b-a0af-ece45d1b1c3a:string:2020-12-01T00:00:00+08:00",
+ "semester": "097886a2-e1ea-4a56-8584-83a765623eec:string:1"
+ },
+ {
+ "name": "aef69586-5c89-47a6-b19d-5cc8d63b8d06:string:Object Oriented Programming in Java",
+ "grade": "93c11754-a338-4b48-ba60-cf685087ddf5:string:A+",
+ "courseCredit": "8d72b490-02bd-41d2-b2c6-384b2216c146:string:4",
+ "courseCode": "e604a19b-8d27-439a-af9b-fe4d0447677e:string:CS 2110",
+ "examinationDate": "524e1b75-0c0d-4f7b-bf48-bd48feed2e08:string:2021-12-01T00:00:00+08:00",
+ "semester": "80f8ae4c-a31e-4317-88d9-368a9267bbfb:string:2"
+ },
+ {
+ "name": "0631f3c1-4e93-407c-89bb-f7a4cd0e2492:string:Microeconomics",
+ "grade": "f958cd24-e9d0-4945-8e89-0a1915dcb9ec:string:A+",
+ "courseCredit": "5c0fe648-f011-4b48-8d7a-92748f9158b2:string:4",
+ "courseCode": "0aadd6a3-9319-4d36-b101-d6fe5437816b:string:ECON 3030",
+ "examinationDate": "8f4b8ce3-5b4e-4bce-8e2f-19ea61cb23a6:string:2022-05-01T00:00:00+08:00",
+ "semester": "24a2f0cb-914f-41d5-9301-3a6370c9a84b:string:3"
+ },
+ {
+ "name": "6536359b-cf78-4721-8913-7269e6afb6e1:string:Macroeconomics",
+ "grade": "a0228362-4179-4407-a0ca-aabab8027e51:string:A",
+ "courseCredit": "2dab9b50-9adc-494f-94f2-3787d072b80e:string:4",
+ "courseCode": "7fff22b6-b9c7-49ea-9eae-67cb15fc22cd:string:ECON 3040",
+ "examinationDate": "60afa71e-2680-46ac-be21-650353fc2041:string:2023-05-01T00:00:00+08:00",
+ "semester": "db7c8385-1c18-4452-8a3b-e68d63178acb:string:4"
+ },
+ {
+ "name": "490ee6ae-9db8-46e3-90f0-22fd3bf30ba0:string:Econometrics",
+ "grade": "c89b2f5b-e7a2-4795-9382-b229d7b45dbe:string:A-",
+ "courseCredit": "0c0450a3-7f5e-410c-8662-2184886045ce:string:4",
+ "courseCode": "1cee504f-11cd-4bb8-a074-2bc09ccb878a:string:ECON 3120",
+ "examinationDate": "79fa5252-3155-459b-8d77-f468109114dc:string:2024-05-01T00:00:00+08:00",
+ "semester": "eae9c859-de7d-4a34-a57c-0c3c2bab9ef3:string:5"
+ }
+ ],
+ "additionalData": {
+ "merit": "af2774a7-79f3-43b4-879e-02e7021abbba:string:Y",
+ "studentId": "d968a002-834f-490e-aedd-da8430ee9c66:string:123456",
+ "transcriptId": "47085854-5b87-420b-9e3e-e7e6b33222d0:string:001",
+ "certSignatories": [
+ {
+ "signature": "5960e70e-8a78-479e-9ff1-43a536a8db47:string:data:image/png;base64,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",
+ "name": "5ca93796-e8a2-4ddb-bdae-77fee89e09ae:string:John Demo",
+ "position": "a7555002-b96a-4d33-8aec-6e5692440ff5:string:Dean of Demos",
+ "organisation": "28f18be7-037b-4b49-a188-3d6bf2ae5638:string:Opencerts"
+ }
+ ]
+ }
+ },
+ "signature": {
+ "type": "SHA3MerkleProof",
+ "targetHash": "0aeccde6a769adf132e17ec171cd4ef4eb5e707337965f69ad7b777d6f73c050",
+ "proof": [],
+ "merkleRoot": "0aeccde6a769adf132e17ec171cd4ef4eb5e707337965f69ad7b777d6f73c050"
+ }
+}
diff --git a/src/__tests__/__fixtures__/oa/2.0/signed_wrapped_oa_dns_did_v2.json b/src/__tests__/__fixtures__/oa/2.0/signed_wrapped_oa_dns_did_v2.json
new file mode 100644
index 0000000..e3de200
--- /dev/null
+++ b/src/__tests__/__fixtures__/oa/2.0/signed_wrapped_oa_dns_did_v2.json
@@ -0,0 +1,168 @@
+{
+ "version": "https://schema.openattestation.com/2.0/schema.json",
+ "data": {
+ "issuers": [
+ {
+ "id": "3d6d890c-e560-470d-8950-9013665afddc:string:did:ethr:0x433097a1C1b8a3e9188d8C54eCC057B1D69f1638",
+ "name": "cf9b3757-dcde-4d40-b1b3-1009678bc5a8:string:DEMO STORE",
+ "revocation": {
+ "type": "dbed69d0-e79f-4118-ad7f-fd73c583331b:string:NONE"
+ },
+ "identityProof": {
+ "type": "ae510d68-fc15-49d2-a735-c6180a106b0d:string:DNS-DID",
+ "key": "397c8962-014b-4db8-849c-f7fd93a00e0e:string:did:ethr:0x433097a1C1b8a3e9188d8C54eCC057B1D69f1638#controller",
+ "location": "bb62dc90-b899-428d-a047-23f0836c7acf:string:example.tradetrust.io"
+ }
+ }
+ ],
+ "network": {
+ "chain": "bcccaacd-7489-42d6-b7be-0a0a71c0c0e9:string:FREE",
+ "chainId": "20684d2d-215d-4e64-b628-5717993a88dc:string:101010"
+ },
+ "$template": {
+ "type": "a64a6bc1-77ba-4775-b371-e586ddbbb2d4:string:EMBEDDED_RENDERER",
+ "name": "c4ea0f16-a4ca-45f8-b1f4-18fd806f4cd7:string:CHAFTA_COO",
+ "url": "f8c569ef-4af3-47eb-a77e-dea496fca42b:string:https://generic-templates.tradetrust.io"
+ },
+ "firstSignatoryAuthentication": {
+ "signature": "1f986756-0011-48ef-8e88-19a36d2823be:string:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABCwAAAG6CAYAAADDFddpAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3QHWoziWJtDoleX0ymJ6ZTO5splUVdDtcAASIKEn6f7n5MnqTgzSfcI2n4X4jx/+CBAgQIAAAQIECBAgQIAAAQLBBP4jWHs0hwABAgQIECBAgAABAgQIECDwQ2BhEBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAgTYC/+vHjx/pn//76582R7FXAgQIECBAgMCkAgKLSQurW90EPi9Q0v/+391a4sAECPQU+D+/worPNvyX94SeJXFsAgQIECBAYDQBgcVoFdPeyAJ7FyipvekiJf0JLyJXT9sI1BP4fye7SrMt/vZ+UA/bnggQIECAAIF5BQQW89ZWz94TSDMpfu78mrrXAr+wvlcXRyLQQyC9H6TwMvf3n24TyRH57wQIECBAgMDqAgKL1UeA/tcQOPs19Wj/KbhwX3sNffsgEEsgzaRKAWbJnwCzRMk2BAgQIECAwLICAotlS6/jlQSuXJzsHdLtIpUKYTcEgggc3Rp2Fl66XSxI8TSDAAECBAgQiCUgsIhVD60ZT+DO7Iq9XrqvfbzaazGBPYG9EDOd3+lWEaGFMUOAAAECBAgQuCAgsLiAZVMCXwJnsytyFyhnmO5tN9QIjCtw9HSQ1KOzW0XcHjJuzbWcAAECBAgQaCQgsGgEa7dLCJwFFtu5tU31Lr2nfYNz8bLEENLJCQX2AosthMwtyOm8n3BA6BIBAgQIECBwX0Bgcd/OKwkc3aueZlekC5TvvxRe/FX4NBHBhfFFYEyBs8Ai9SgXWphhNWbdtZoAAQIECBBoICCwaIBql8sIHK1fcRRYbDBXZ1341XWZIaWjEwjkAovUxdxivUKLCQaCLhAgQIAAAQLPBQQWzw3tYV2Bs6cBlFxwpF9a0z+lt4sILtYda3o+jkBJYFESWvh8HqfmWkqAAAECBAg0EvCFqBGs3S4hcBZY5GZZfAPlfnHdthdaLDG0dHJggb2ZV0cBZu4RqCXB58BUmk6AAAECBAgQOBcQWBghBO4L5C42roYLpWtcXN3v/R56JQECVwX23hfOPmvPHo18Nfi82lbbEyBAgAABAgRCCwgsQpdH44IL5BbPS82/Ey6UzLZIFzJp3+nf/ggQiCNwZYZFanXufL/zHhJHQ0sIECBAgAABAg8EBBYP8LyUwD/rT5z9OroB3Z3WnZvBkfZ/d9+KR4BAG4GrMyxSK3LnuvO8Ta3slQABAgQIEAguILAIXiDNCy+Qu9B4Glrkfn0VWoQfIhq4mMDVGRaJJzdby60hiw0i3SVAgAABAgT+LSCwMBIIPBPIXWhse39ywVESWpg2/qyOXk2glsCdGRbp2Lnw0zleq0L2Q4AAAQIECAwjILAYplQaGligJFBIzX8SWrigCTwANI3Ah0DpY02/0UrCT5/ZhhoBAgQIECCwlIAvP0uVW2cbCrwVWuSO41fYhkW2awIFAncDi7Rr53cBsE0IECBAgACBdQQEFuvUWk/bC+QuNrYWPA0Vcsd5uv/2Uo5AYF6BJ4FFUsndGmIBznnHjp4RIECAAAECXwICC0OCQF2B3MXGW6GFi5q6dbU3AqUCTwOL3K0hT28tK+2H7QgQIECAAAEC3QUEFt1LoAETCkQJLcy0mHBw6VJ4gaeBRepg7j3EuR1+GGggAQIECBAgUENAYFFD0T4I/Cmw92jDPaenFx6520PMtDA6CbwrsHdOXj3PS2ZZpH2m2Rb+CBAgQIAAAQLTCggspi2tjnUWyF1wfDbvaaiQCy2uXix1pnN4AkML7J37d87B3Hnt1pChh4nGEyBAgAABAiUCAosSJdsQuCcQKbR4GorcE/AqAusJ1Aosklxuppbzer3xpccECBAgQGApAYHFUuXW2Q4CuV9JtyalX0ufTvHOHevOr7wdyBySwNACe4HF3dkQudDz7n6HBtZ4AgQIECBAYB0BgcU6tdbTfgK5IOEztEi/mD75yx3LL7JPdL2WQF6gZmCRjpZbgNM5na+JLQgQIECAAIFBBQQWgxZOs4cTyAUJnx16el7mjmWmxXDDR4MHE9i7lePueW2WxWDF11wCBAgQIECgnsDdL1D1WmBPBNYRyAUJm8Qbt4c499cZd3r6vsDerIgn55xZFu/X0BEJECBAgACBAAJPvkAFaL4mEBhOIHfh8RlauD1kuPJqMIF/Ceyd509v3cgtwOnz3OAjQIAAAQIEphPwBWe6kurQAAKRQgvvAQMMGE0cTqBFYJGboeVWr+GGiQYTIECAAAECOQEXKzkh/51AfYF0T/rPHz9+pH/n/mo8BSB3ofP0l99cH/x3AqsJ1Hy06afd2SyLGu8Vq9VJfwkQIECAAIHgAgKL4AXSvKkFclO8Pzv/9Fw9Cy1c6Ew9zHSug0DtJ4VsXRA+diimQxIgQIAAAQL9BJ5eBPVruSMTmEPgyu0hacp3Chfu/uWOZabFXVmvI/C7QKvAIh3FLAujjQABAgQIEFhGQGCxTKl1NKjA1dtDnoYWfqENOhA0azqBmo82/cRxDk83VHSIAAECBAgQOBIQWBgbBPoL7P0ae9QqjzztXy8tIFAi0GLhze24ZlmUVMA2BAgQIECAwPACAovhS6gDkwhcCS1Sl58+EcDtIZMMHN0IK7A3E6LWbVfO37Bl17CFBbYZk3//MkjvAf4IECBA4KGAwOIhoJcTqCyQuxD5PNzT0CK3EOfT208q09gdgaEEWq5jkSDMshhqOGjspALpPE///LXz5C8LWk9adN0iQOBdAYHFu96ORqBE4M3QInesWr8Il/TbNgRmEmgdWDh3Zxot+jKaQOn6U09/WBjNRXsJECBQXUBgUZ3UDglUEchdjHwe5OkXIov4VSmZnRD4Q6DVwpvpQLnbyJ6+LygnAQJ/CpQGFdsrzbIwiggQIPBQQGDxENDLCTQUyAUJNUOLXEBipkXDQtv1tAJ751XNz92z89aF0rTDSsc6CFwNKj6bWPOc79B1hyRAgEBfAW+iff0dnUBOQGiRE/LfCcQVaLnwZup1bpaFoDHu2NCyMQS2NSp+3myumU434byMAAECm4DAwlggEF/gSmjx9FfV3LFcAMUfL1oYR2AvUKh9Dll8M069tWQugdznYUlva5/vJce0DQECBKYSEFhMVU6dmVjgyhcnocXEA0HXhhJovfBmwsi9N/icH2rIaGwAgdw5VdLE9DnsSVslUrYhQIBARsAXGUOEwDgCV79EPfllx5oW44wLLY0t8D0D4mmguNfbs1kWT94HYstqHYF6AtsaFWmP6X/f/dtCinSe+yNAgACBCgICiwqIdkHgRYHcPevfTXlysZJb0M+vRy8W3qGGFWi98GaCyZ2r6X3AHwECfwo8WUxz25vZFEYWAQIEGgoILBri2jWBhgK5GRCfh36y6JcLoYZFtOslBFovvJkQc0Hmk+ByiSLp5HICNYIKsymWGzY6TIBADwGBRQ91xyRQR+DKLSJPQouz47SY3l5Hx14IxBDYCxOenI9HvRIuxqi3VsQWqBFU+NyLXWOtI0BgMgGBxWQF1Z3lBN4KLVwMLTe0dLiSwBsLb+ZmWbjAqlRMuxlWQFAxbOk0nACB1QUEFquPAP2fQSA3Hfyzj09+2T0LLZ7sd4Ya6AOBM4G9RTFbfP5afNM4JPC7QI2gwuebUUWAAIGOAi2+MHXsjkMTWFqgdF2LJ7+2Ci2WHmI6f1Ng77xpsa6EmVA3C+Rl0wkIKqYrqQ4RILCqgMBi1crr96wCV24RuXv+n/2K65eoWUeWfj0ReGPhzdS+3GyrFiHJExevJVBbIJ0Dn48ovbN/T/24o+Y1BAgQaCRw94KlUXPslgCBCgJXQos7FzAuiioUyS6WEnhr4c2EahbUUkNLZz8Ernz27cE9mX2oEAQIECDQSEBg0QjWbgl0Fij94nb3l6Tc/r23dB4ADh9O4HtmUquLo7NAsdUxw2Fr0DICtW79SOdG+scfAQIECAQTcFERrCCaQ6CyQOm6FndmWnjcaeVi2d3UAm8FFgnx7Ly/c65PXRidG1KgVlCRPsf8ESBAgEBgAYFF4OJoGoFKArnZENth7qw/Yfp5pSLZzfQCb61jkSCFidMPp2U7WCOoMNNo2eGj4wQIjCggsBixatpM4LpAr9DCr7nXa+UVcwrshXstP4M94nTOcbRqrwQVq1ZevwkQWF6g5Zel5XEBEAgmkFss8+5MC/fMByu05oQU2DtPWv7Sa/ZTyGGgURcFngYV27oUKTz3R4AAAQIDCggsBiyaJhN4KFCyrsXV20POZnBc3dfD7nk5gZACbz4pJAHkAkqf/yGHiUb9EngaVKTd+OwxnAgQIDCBgC8sExRRFwjcECi5ReTql72zIMR7zY0iecl0At/nSMsZFgnP4pvTDaHpOySomL7EOkiAAIFrAi4irnnZmsBMArVDC7eGzDQ69KWFwNvrWDgnW1TRPlsI1AoqPJ60RXXskwABAh0FBBYd8R2aQACB3LTx1MQrMy3O9mcBzgAF14SuAm8+KWTrqMU3u5bcwU8EtpAibZL+992/9BklqLir53UECBAILiCwCF4gzSPwkkBuXYsrU9eP9nVlHy9122EIvCrw9joW24VgOif3/pyTr5bfwX4J1JhNkXYlqDCkCBAgsICAwGKBIusigUKB3C0ipTMtzmZZlO6jsMk2IzCUQI/AIgGZZTHUMJm2sYKKaUurYwQIEGgnILBoZ2vPBEYUyIUWpb/ImmUxYvW1+Q2BtxfeTH06O69Lz+k3bBxjPoHtVo+fD2/7SDJprG6zKuaT0iMCBAgQ2BUQWBgYBAh8C+RCi9JZEkehRenrVYbAjAJvL7y5GQoRZxxNcftUazZF6qFbP+LWWcsIECDQXEBg0ZzYAQgMKZALLUp+lT3aR8lrh0TTaAIFAnvnxRufxWfr1AgRCwpnkyKBWkFF+pz4+58jpvPFHwECBAgsLPDGl6SFeXWdwPACTy9yzLIYfgjoQGWBXutYpG6YZVG5mHb33wKCCoOBAAECBJoICCyasNopgakEntz/bpbFVENBZyoI9Awszs5lsywqFHexXdQKKRKb9SkWGzy6S4AAgVIBgUWplO0IrC3wZKbF0RMK/vPXl9S1ZfV+RYEeC28m57Mn+KT/7jvBiqPxWp/TGNqCimuv3N9aUFFD0T4IECAwsYAvJxMXV9cIVBbIrWtxFEAcXSRZy6JygexuGIG9EO+tz+Oz83jFEHF7isX2779+rZ2wDab0PpX+WfnvM6DYnJ56mNHzVNDrCRAgsIjAW1+QFuHUTQLTC5z9Qnu2SJpZFtMPDR28INDrSSGpiblzOIUWs//dvQDfgou0GOTn3/b/nynYuGuUGzuCipyQ/06AAAECvwkILAwIAgSuCqRfaNOvkEe/tO39SmuWxVVl288ssHc+vDm74ewWrzfb8WaNa663UNLu73BjhJkarYzc9lEyYmxDgAABArsCAgsDgwCBOwK5e+H3fkUzy+KOtNfMKNBz4c3kudIsi1zA+vb42maipeP2CjE+w+afvwBq3erx6SmoeHt0OR4BAgQmFBBYTFhUXSLwkkDu17jvX2o9MeSlwjhMeIHegUUCmnmWRe69KeoA+Q4zUj++bzPJ3XbyHTyk/zvNiNuCqjf6ngLrXmHMG/1zDAIECBB4UUBg8SK2QxGYVOBo5kTq7vdMC7MsJh0EunVZoNeTQraGns2yGHWdgVGDisuDJ+ALzKYIWBRNIkCAwAwCAosZqqgPBPoLnD154PPixyyL/rXSghgCPRfe3ATOwsaR1rIQVPQZ01tIkY6em/nRp4WOSoAAAQLDCwgshi+hDhAII3A2xfzzEaZ7F0kecRqmjBryksB3eNfjHJhhlkXuccsvlXOZw5hJsUypdZQAAQIxBAQWMeqgFQRmESiZaXEUbIw6DX2W2unHuwK9nxRSMssi+neEp2FFyQyBVKfPdSHeXg/i3VG5fzQhRYQqaAMBAgQWFYj+ZWTRsug2gaEFzi4itl+RzbIYusQaX0EgwsKbqRtn52vk20LuhhXbwpa1F4XcQo3vcKPF0zcqDL/sLoQUWSIbECBAgMAbAgKLN5Qdg8B6ArnHnh6JRL5AWq+Ketxa4Du46zH+c+dqxO8Jd8KKnk+u+JylkWZoRAsxtvUn/vZ0j9anvP0TIECAwFWBiF9ErvbB9gQIxBS4c1HR4z7+mHpatYLAd2DRa/yP9ojTs8VCP8dN9FkC37ebpLanQCMFBz9/hQc1wo1tVknadwpuPh+XarHMFd5p9JEAAQIDCwgsBi6ephMYQCD36+1eF7wvDVBYTawisHfh3WP8n52nvUKUI+CSIDR6UHF38HzedpL6uP3f2//+Dh+EEXelvY4AAQIEwgj0+GIUpvMaQoDAawKlv4imBkW7QHoNyYGWE9i7+O71uTzCLIuSsMLivcudRjpMgAABAjML9PpiNLOpvhEg8KfA1ZkW3puMohUE9i7Ae6xjkaxHmGWRCz6FFSucNfpIgAABAksJuChYqtw6S6C7QO6CY2tgr4u27kAasJTAXmDR86L7bJZF7+8LudkVPd2WGrQ6S4AAAQIE3hTo/QXkzb46FgECMQTOLoq2FrotJEattKK9QJSFN1NPo94WUjJDS8jZfqw6AgECBAgQeF1AYPE6uQMSIPCPQO7X0oTk/clQWUEgysKbm/XRLKieIWLu/cLsihXOFH0kQIAAgSUFXBAsWXadJhBCIHcR0vMCKQSQRiwhEGmGRcRZFrnZFd4nljhNdJIAAQIEVhUQWKxaef0mEEMgF1r45TRGnbSinUCkhTdTL6Mtvuk9ot3Ys2cCBAgQIBBeQGARvkQaSGB6gdwvqEKL6YfA0h3cWzei95iPtJbF2UK9vZ2WHrg6T4AAAQIE3hAQWLyh7BgECOQEhBY5If99VoG9sd/7QvxsVsObbTO7YtZRr18ECBAgQKBQQGBRCGUzAgSaC+QeeZruVU8XS+nf/gjMIrAXWPRelyHKbSFn7wm9jWYZf/pBgAABAgRCCwgsQpdH4wgsJZD7NXXDePMX3qUKoLNdBCIGFgmi920hufcD7wNdhquDEiBAgACBdwUEFu96OxoBAscCudtCPl/pYsVImklgbybBf3aeTdR7lkUusPD9ZaYzQF8IECBAgMCBgA98Q4MAgUgCZ7/qfrdTaBGpctryRGBv3PcOLFJ/zm7JaP39we0gT0aU1xIgQIAAgUkEWn/hmIRJNwgQeEngyiyL1CShxUuFcZimAnuBRYQ1Gs7Ox9bnnqeDNB1ydk6AAAECBMYQEFiMUSetJLCKwNXAQmixysiYu597tz+0DgRKRHPnY6vvELnbQSLMPinxsw0BAgQIECDwUKDVl42HzfJyAgQWFji6LST94pwuoPb+PEFk4QEzQdejLryZaHssvpkLLHx3mWDQ6wIBAgQIECgR8KFfomQbAgTeFDj6VTeFEn//c1/9z5PGRPhV+k0rx5pD4GjMR/iM7rH45llIEuFWmTlGnV4QIECAAIEBBCJ8GRqASRMJEHhZ4Oj+9fSelfv1VWjxcrEcropAxCeFbB17e5aFwKLKkLITAgQIECAwvoDAYvwa6gGBGQWOLli2e9dz99YLLWYcFXP3aW/MRxnHb8+yOAssopjMPRr1jgABAgQIBBEQWAQphGYQIPCbwNEsis/p4EILg2YmgahPCtmMz57aUXsRTE8ImWlk6wsBAgQIEHggILB4gOelBAg0EzgLI77ft85uEfFrbLMS2XFlgb1xHGm9hrPzrHY7BRaVB5fdESBAgACBUQUEFqNWTrsJzC9wdNGy92vumxdT88vf7+H3U1zShay/MoHIC29uPXhrloXAomzM2IoAAQIECEwvILCYvsQ6SGBYgaP72I9mTQgt3i91ushO//x18sjZ1CozXfK1OQosat9ukW/J8RZvnWMCiydV8loCBAgQIDCRgMBiomLqCoHJBM4eb5ou4vb+ck8Q8Z5XZ5BsQcXZI2b3jiS4OPeP/KSQreVvzLIQWNQ5T+2FAAECBAgML+DL+/Al1AECUwucPd70qOO5xTgj/WI9YvFyoVCuT+k2kb//2Sjtx9/vApGfFLK1tPUTPJy/zgoCBAgQIEDgvwUEFgYDAQKRBXKPNxVavFu93MXkldaYbfGnVvQnhWwtPpsB8fR7RW6MCRyvnGW2JUCAAAECgws8/WIxePc1nwCB4AJ3A4u3fg0Ozle1eU9nVuw1Rmjxu8qRcbTP6pZP5smNs2gWVU8yOyNAgAABAgR+F/DBb0QQIBBZ4M46Ft/9aT2FPbJfzbad/ar+5Di1H4n5pC29XzvCwpvJKDcL4sl3C4FF71Ho+AQIECBAIJDAky8VgbqhKQQITCpQI7BINC1/EZ6U/rdu5S4inxqYafFvwVECi9w59SSEyo0131uenm1eT4AAAQIEBhLwwT9QsTSVwKICdxbe3KMSWtwfQK1mV3y26MlF7v2exXvlnnVEm1azLM5mREV0iDeCtIgAAQIECEwkILCYqJi6QmBSgaOL5TuL7wktrg+S3C/e36FDegJI+vvr14yBK0c00+LHj1EW3kx1PRsbd8MFt3BdOWNsS4AAAQIEJhcQWExeYN0jMIHA04U3vwmEFtcGRUlgcRQ0lLz2uzV3L3Sv9Sru1qMsvJkEW8yyOJvNI9CKO261jAABAgQINBEQWDRhtVMCBCoKHAUWTy5ezi60nuy3YrfD7Cp3O0hJwHA1uFi5BiOtY5EGac1ZFrkA5M6sqjAnkoYQIECAAAEC1wUEFtfNvIIAgXcFji6IalzUtghD3tVpe7TcBWQ6emkdzqb67/WiJAhp2/s+ex8tsEhKZ6HW1ZCh5r76VNBRCRAgQIAAgWoCAotqlHZEgEAjgVpPCtlrXtr3z4O1FkovxBt1O8RuS2ZGXPkcKdnfZ8dXDS1GWXhzq9VZsHWlhjVna4Q4gTSCAAECBAgQeCZw5YvmsyN5NQECBO4JtAwsthYd/aq7emiRmxVx5WJ0s74aWqxYg5EW3tzqejZWSmdZ5MaG7yz33kO9igABAgQIDCvgw3/Y0mk4gaUE3vjF2e0hfw6p3PoVd8OE3IXpd0vuHmfUk+RoLEb+zK4xy8IMi1FHrHYTIECAAIFGApG//DTqst0SIDCgwBuBRWJpuV7GaOwl61eU/nK+13ehxfGIGHEdi9Sbs1kWJbNxnr5+tHNMewkQIECAAIGMgMDCECFAYASBo1/6W7yHmWnx7xFREig89S85xuf4XGWmxVFgEb3/T2dZCCxGeDfWRgIECBAg8KLA0y+bLzbVoQgQWFjgzcDi7GL9yYyC0cqXW7+i1sXz1dBilRq8Nauo9rh8EjqcvbbWeKvdX/sjQIAAAQIEGgoILBri2jUBAtUE3g4szkKLVS6c3goszqz3BlDJrQXVBl7HHY24jkXiyt1KdBY4PQk7OpbKoQkQIECAAIFWAgKLVrL2S4BATYGjC5nWv7Yf/frf+rg17e7uK7fgZm2DXEDy2Y8VQoteY/7uePl83d1bQ8ywqKFvHwQIECBAYCIBgcVExdQVAhML9Py1edWFOHOBRYvPjyuhxewzXUYPy87Gz1HYZYbFxG/iukaAAAECBO4ItPjCeacdXkOAAIEzgd6/No9+8XhndOUCi9ozLFIb0y/zP3/9u6TNLdpQctw3thl14c3N5s6tIQKLN0aWYxAgQIAAgYEEBBYDFUtTCSws0DuwSPSrzbTIzXZoFRbkLnQ/T4PZbw0ZdeHNrUZXA4ir2y/8lqjrBAgQIEBgDQGBxRp11ksCowtECCzOQotWF+8969YrsEh9vhJazHxrSM9boWqMvVwdvwMngUUNdfsgQIAAAQITCQgsJiqmrhCYWCBKYLFdTO/dtjDbhXPPwOIsHNob5rN+lkUa93ffXnKhxed5c3Yb0mzn111PryNAgAABAksJzPolb6ki6iyBBQSiXbitsKZF78AiDetcG7ahP+utIUcX+6PN6MnVMdVvCwOP3s4EFgu80esiAQIECBD4FhBYGBMECIwgEC2w2C6uZp5pkbvIfOvzI7f45zZ+R7uILznvjgKLEQOa3HjKeQgsckL+OwECBAgQmFDgrS+cE9LpEgECLwpEDCxS92eeaZELCt76/MjdUrANwxEv4ktOodEX3tz6WFrHI5O3xltJTWxDgAABAgQIvCTgC8BL0A5DgMAjgcjBwNGjOEf+RfjI+7OIb35+lLQntW1k86MTZPSFNz/7VVrHb4sZ6/roDdGLCRAgQIDAKgJvfuFcxVQ/CRCoLxB1hsXW01nWGtj6k7uw7HEBWXJLwYyzLKKP/atne25s7e1vxtt9rrrZngABAgQILCkgsFiy7DpNYDiByDMsPi/y//r1SM5P4B4X908LnLuo7NGn0lsKerTtqffZ60cY+1f7nxtfo58/Vz1sT4AAAQIECBwICCwMDQIERhAY5VfmWWZa5Nav6PWLd8ksizSee7Wvxbk008Kbnz6pX9vtVEdus4VPLcaHfRIgQIAAgakFBBZTl1fnCEwjMNKvzKmto8+0OAssel9E5sKUNOhnuzVkloU3996Q0vmS/tLeknM5AAAgAElEQVQTd7bapX+ncbY97nSaNzIdIUCAAAECBK4JCCyuedmaAIE+AiMFFkno6Ffx3hf7JdXL3XrRuw+59m19nGmWxUwLb5aMQdsQIECAAAECBP4lILAwEAgQGEFglFtCPi1HfXpIbn2BCJ8bJbeGzDTLYsTxP8L7ijYSIECAAAECwQUifPEMTqR5BAj8EkgX4Okv/Tvd8vD3r3/ngNJ233/pYvLKdO/RZlhs/R1xpkXk20E+XdMtBNuYPBqDs8yyGHX8594b/HcCBAgQIECAwKmAwMIAIUDgSOBzQbzcheFTxXSbwfa33dP+uc+jX5h7355Q2u+99kecAZCbXRHJO9fWrTYzfM7NuvBm6fljOwIECBAgQGBRgRm+yC1aOt0m0ETg6DaGJgfL7HQLMdKF/dHTBEb5Bf1spsXV2SatalESAET7zFjl1hCBRatRb78ECBAgQIBAaIFoXz5DY2kcgYkFSi5WI3Z/lMBis4s406I0pIo0u2LzLF2Ac4bPOgtvRnwH0iYCBAgQIECgqcAMX+KaAtk5gckFRg0qPsuyrYWR1srY/veV9THeLnGU9QhKg4rNJ+rnRckYjnj7zdVxJ7C4KmZ7AgQIECBAYHiBqF9Ah4fVAQIDCJRc6A3QjWwTvwONdKHeO9jouSbH1aAiAUecXfFZ+JJbQ0abjfM9sD0pJHuq24AAAQIECBCYTUBgMVtF9YdAmcAqYUWJxrZGRvr35xNN9hb/LNlf6TZHNWgZDpRc2H+3v2V7Sq1y261wa8hRH0cPYnK19d8JECBAgACBhQUEFgsXX9eXFrgaWBzdYvH5aNPtf3//O0G3fspIy2J+ztDYjlNrocw3Qos7Myq2fo4QVmxtLRnTo98asvfI2dH71PLctW8CBAgQIEBgcAGBxeAF1HwCNwVyv7Rvsw1azTJIF9GfIcZfA4ca3zMzrt5uUjO02EzTv5+YpqCiVihzc4heflnpLIuRZyQILC4PCy8gQIAAAQIERhYQWIxcPW0ncF/gLLDo/YvtTGHGd4U+Z2ukQGGbjXI0AyUFB5/bfe5vW4uj5uyV1kHV/RFb9srZZ1lYeLNsHNiKAAECBAgQmERAYDFJIXWDwEWBkgu7SL9EH7X381aVmhfuFzmH3zw5brMqRu9MbvZQ6l+ksX3F28KbV7RsS4AAAQIECAwvILAYvoQ6QOC2QMmFXZQ1DO7cNvF5e8SGlGYrpD/hxv88KWWWoGKrcemtISN+/kV5JO7tNx0vJECAAAECBAhcERjxC9uV/tmWAIFjgdILuwihxZ3AorT23+s+fF74lu5jpO1mmk1x5D5SGHdl7Byds71v47rSB9sSIECAAAECBIoFBBbFVDYkMK3ACBd3PX9Z/lx49MlClj0H0BZSpDYcPfGlZ/tqH7s0jBvx1hALb9YeLfZHgAABAgQIhBUQWIQtjYYReFWgZE2L1KBesy1azrC4C/19y8l2u0na31u3nKTwYVt887MfaTHPq08ruesQ9XUlY3rEmQkCi6gjTrsIECBAgACB6gICi+qkdkhgWIGSC7xeoUXEwOJqoXNP9djCh8/9pv/f0QyYXuHR1X733L5k9tBosywsvNlzRDk2AQIECBAg8KqAwOJVbgcjEF6gNLR4O7joeUtIhKLNENj0cCy5NWS0WRYebdpjJDkmAQIECBAg0EVAYNGF3UEJhBZIF3k/L9zW8MYv/S7Yf/xgcO+0KZll8cYYvtf6P191FMKM1IdaFvZDgAABAgQITC4gsJi8wLpH4IHAldkW6VfqtG7C5wKVDw79x0tdrP+bZPWZJnfG1GyzLAQWd0aB1xAgQIAAAQJDCggshiybRhN4TaDkYu+zMa2Ci6ML9dGm89conPDmuuJssywsvHl9DHgFAQIECBAgMKCAwGLAomkygQ4CV2ZbbM2rOUXdwpO/F11ocf0k2LvI/97LKAtwHvXFZ/r1ceEVBAgQIECAQGABX24CF0fTCAQU6BVcmGHx52AQWlw7QUrG7igzdiy8ea32tiZAgAABAgQGFRBYDFo4zSbQUSBd+P11YVHOzxkX6YIw/XP17+gCbZQLzKv9Ld3emhalUv/ebpZZFup+re62JkCAAAECBAYVEFgMWjjNJhBA4G5wcWedC7MJjgvu4rX8ZChZk2WEEMzCm+U1tyUBAgQIECAwsIDAYuDiaTqBIAIlU+2PmprWuSiZdWGGxXmx93xGuPDuMYRLFuCMvpbFUWCh5j1GlGMSIECAAAECzQQEFs1o7ZjAcgJ3Z1wkqBRcpL+jx6JadDM/nPaMai58mm/BGFvMMsvCk0LGGG9aSYAAAQIECDwQEFg8wPNSAgR2BbbQ4edNn71ZF2ZY5DHThXgyT//+/Is+WyDfs/pbzDDLwqya+uPCHgkQIECAAIFgAgKLYAXRHAKTCTy9XSRxpH2YYVE2MI5mDwgtfvebYZaFtUvKzglbESBAgAABAgMLCCwGLp6mExhI4ElwcdZNtzz8qbNnbW2DP51Gn2UhsBjoDVBTCRAgQIAAgXsCAot7bl5FgMB9gZrhhcBivw7Ws8iPz9FnWZhNk6+xLQgQIECAAIHBBQQWgxdQ8wkMLPB0rYvUdYHF8QDYCy3cGvK71+izLCy8OfAboKYTIECAAAECeQGBRd7IFgQItBV4GlyUPhq1bS/i7X3vF3i3hvxep9FnWQgs4p13WkSAAAECBAhUFBBYVMS0KwIEHgs8CS/Sxfjf/7Tg6NGojxs34A7cGpIv2sizLDwpJF9fWxAgQIAAAQIDCwgsBi6ephOYWCD98r09pvNON90q8j9qe7/Ce+//H5+RZ1lYePPOu4PXECBAgAABAsMI+NI6TKk0lMCyAiW/gB/huF3k3zNOfn4BCXR+BykZYxHX/zhqt8/2Zd8udZwAAQIECMwl4EvNXPXUGwIzCtR4qsjqt4uYZZE/M/aMPl8Vcf0PTwrJ19UWBAgQIECAwMACAouBi6fpBBYRKPn1+wrFirML9i5sV3Q4Gycl4yzaZ+ZRYBExXLlyjtqWAAECBAgQIPAvgWhfvpSFAAEC3wJnv3yni+6/fq13cVVutQv27wtyF7W/j5gR17I4avNqY/vquW97AgQIECBAYBABgcUghdJMAgsLHAUWnxfcTxbpXOXibu/WmojrMvQc6iPOsrCORc8R49gECBAgQIBAUwGBRVNeOydA4KHA2a/eR0HD3TUvVljn4jv8McvizwGaW8siWsB1FFgIox6++Xg5AQIECBAg0F9AYNG/BlpAgMCxwFn4kLtwTK91u8jvtnsXty5sfzfKBV7RQh6BhXdQAgQIECBAYFoBgcW0pdUxAlMInF08ll5oPw0uEmTaxwx/Ft/MV7FkLYvSsZc/2vMtLLz53NAeCBAgQIAAgaACAoughdEsAgT+JXA2Pf/q+9cWOvy8YTvT7SJuC8kPgNxaFpFmWRyFepHamBe3BQECBAgQIEBgR+DqF36IBAgQeEvg7JfuJxdjT4KL1Pd0K0r6G3XWhdtC8iN4pFkWZljk62kLAgQIECBAYFABgcWghdNsAgsIPFm/ooTnyZNFtv2PGF7sXeBGusWhpHZvbDPSLIujmUjq+sZIcQwCBAgQIECgmYDAohmtHRMg8FCgdWDx2bzcQoulXdkCjDQDJP0T9c9tIWWVyT0xJEogcNTO3MK0ZQq2IkCAAAECBAh0EhBYdIJ3WAIEsgI116/IHuzXBrWCi+1429oXW3gRJcT4tnVhuz9CcuPhya1JpWOyZLuj2SBR2lfSB9sQIECAAAECBP4QEFgYFAQIRBXoEVhsFk+eLFLiuc3ESNu+ORsj3Q6SFh1N//78E1jsV22UtSzOghWf8yVnpG0IECBAgACBkAK+yIQsi0YRWF7gzdtBzrBrrHNRWsxt9sXfHy+otbDnUVCxHcpnwXGVRphlUePxv6Xj1HYECBAgQIAAgdcEfEl9jdqBCBC4IBAlsPhs8tOni1zo/u6me4FG+v+lMCK1bQtXtv/fX7/28j2b4nvnZlecV2aEWRZnbYyyzsbT8e/1BAgQIECAwIICAosFi67LBAYQ6Hk7SAnPFg6kUCAXCJTsr9c2wooy+eizLM4CCzUuq7GtCBAgQIAAgYACAouARdEkAosLnF0cRl1EcJt9MVKA4UL22okW/YkhFt68Vk9bEyBAgAABAgMICCwGKJImElhMIOLtIFdLsM3ASK9Li1xG+kuhTworojyxJJLNWVtyt4b0DtOOAovUJ5/1o4wy7SRAgAABAgR+E/AlxoAgQCCawNkv2aPfj/+5iOZbszG2YEJI8Xykn4UCae89x+dZ23q267m6PRAgQIAAAQLLCggsli29jhMIKZD7FXvm96zvJ4KkQCM9MWRbPPOzYMkpBRGf/33739tTRragwkyKekM9Nz57zrKYYWZSvUrZEwECBAgQIDCFwMxf/qcokE4QWEzARddiBR+wu7lZFr0+V8/ClJ5ByoAl1mQCBAgQIEAgikCvL1ZR+q8dBAjEEji7GLRIZKxardqayLMsoj9dZ9Uxo98ECBAgQIDATQGBxU04LyNAoImAC64mrHZaWeAsWOs5m2Hm9V8ql9DuCBAgQIAAgREEBBYjVEkbCawh4HaQNeo8Sy/PwoFes4EsvDnL6NIPAgQIECBA4F8CAgsDgQCBKAICiyiV0I4SgYhrWUSd+VHiaRsCBAgQIECAwB8CAguDggCBKAJuB4lSCe0oFYi25oqFN0srZzsCBAgQIEBgCAGBxRBl0kgCSwgcBRY91wRYAl4nbwuczQpKO/3PX4+fvX2Aiy/MLQj6dnsuNt/mBAgQIECAAIHfBQQWRgQBAhEE3A4SoQracEcg2iwLC2/eqaLXECBAgAABAiEFBBYhy6JRBJYTsFjgciWfpsPRZjVYx2KaoaUjBAgQIECAgMDCGCBAIIKA9SsiVEEb7gpEmmUhsLhbRa8jQIAAAQIEwgkILMKVRIMILCfgdpDlSj5lh6PcipGb8eFzf8rhp1MECBAgQGBOAV9c5qyrXhEYSeAssLBI4EiVXLutUWY25AIL59Ta41TvCRAgQIDAUAICi6HKpbEEphRwO8iUZV2uU7mg4M3P27Nz6r/+qUwKCf0RIECAAAECBMILvPkFKjyGBhIg8LqA20FeJ3fAhgJRZllEaUdDarsmQIAAAQIEVhAQWKxQZX0kEFdAYBG3Nlp2XSA3y+Kt2zHOAovUK5/912vrFQQIECBAgEAHAV9aOqA7JAEC/y3gdhCDYTaBsxDu//748SOFFq3/ztqQjv1WcNK6n/ZPgAABAgQITC4gsJi8wLpHILDA2a/Rb13YBebRtEEFIsyyiNCGQcun2QQIECBAgEAkAYFFpGpoC4G1BCL8Er2WuN6+JRDhVqez2UsCwbdGguMQIECAAAECjwQEFo/4vJjAY4H0S+jPf6Zo//1rTyut3u9JBo+Hjx0EFYgww0FgEXRwaBYBAgQIECBQLiCwKLeyJYHaAnu/wq7yyMEIF3S162l/BD4Fes8gsvCm8UiAAAECBAgMLyCwGL6EOjCwwN4voKtM1c4tCui9aeCBren/EugdyuXOMQtvGqgECBAgQIBAeAEXBeFLpIETCxxN2V7hQuLs199VZplMPLR17ZdAz3HeOzAxCAgQIECAAAECjwUEFo8J7YDAbYGji5nZZ1nkLqQEFreHlBcGE8iN9ZbhZO7Ys7/PBBsKmkOAAAECBAjcERBY3FHzGgL1BI5Ci5YXMvVaf29Puanq3pfuuXpVTIGesywsvBlzTGgVAQIECBAgUCjgwqAQymYEGgkcXczMPMtAYNFoMNltSIHcTIeWn8MW3gw5JDSKAAECBAgQKBVo+UWptA22I7CywNnFzKznp8eZrjzi1+z7WXDQcjZVLrBoeew1K63XBAgQIECAQFWBWS+IqiLZGYHGAivNssjNrph5ZknjYWT3gQXOgsmWa0nkzjeBReBBo2kECBAgQIDAjx8CC6OAQH+Bs4uK2c5Rv/j2H29a0EegxyyL3O0oAsI+Y8FRCRAgQIAAgUKB2S6GCrttMwLhBFZZfPPsdpBUFO9J4YamBlUSOAsmW82yyAUWrY5bicxuCBAgQIAAgdUFXBysPgL0P4rACo84zU1P92tvlNGoHa0EzgK7VrdneFJIq2raLwECBAgQINBcQGDRnNgBCBQJnP0S2upCpqhhFTfK3Q4isKiIbVchBc5Cu1bj36ymkENBowgQIECAAIESAYFFiZJtCLwjMPssi9yF0yzBzDujxVFGFOix+GYuKPQ9YMSRpM0ECBAgQGARAV9UFim0bg4hMPMjTnO3g7iXfoghqpEVBN5efDMXWAgKKxTVLggQIECAAIE2AgKLNq72SuCuwNEshNEvKnKzK1pNh79bB68j0Erg7VkWubBw9PeWVnWyXwIECBAgQCCAgMAiQBE0gcCHwIy3heSeVJC676LJabCSwJuLb+YCC2HhSiNPXwkQIECAwGACAovBCqa50wvMuPhm7oIpFdV70fRDWwc/BN6cZZELDAUWhiYBAgQIECAQVsBFQtjSaNjCArPNsnA7yMKDWdcPBd6aZZELLKwfY5ASIECAAAECYQUEFmFLo2ELC5wtkjfaOVsyu8LtIAsP9oW7fnZu1AwRBBYLDzJdJ0CAAAECowuMdvEzurf2EygReHO6eEl7nmyTm12R9u196Imw144s8MYsC4HFyCNE2wkQIECAwOICLhQWHwC6H1bg7UcftoAomV3h/vkW8vY5isBbsyzOgpGaszlGcddOAgQIECBAYBABgcUghdLM5QTOLmRGucg3u2K5YavDFwVysx9qneu5c9F3gYuFszkBAgQIECDwjoAvKe84OwqBqwK5C5no6z6YXXG14rZfVSB3rtT4nD6bsZXcaxxj1frpNwECBAgQINBQwJeUhrh2TeChwNlFRvRp3LlfdBNN9NDlYfm8nECxQOtzXWBRXAobEiBAgAABApEEBBaRqqEtBH4XGHWWRe4X49TLWlPdjRkCMwi0PtcFFjOMEn0gQIAAAQILCggsFiy6Lg8lMOKFRsnsCoHFUMNQY18QaDnLYsT3kRfIHYIAAQIECBCILiCwiF4h7VtdIDdbIdqFf669qZ7Rb2dZfczpfx+B3CyLJ+d6LrBwe1afmjsqAQIECBAgkBEQWBgiBOIL5GYsRLnYKAkrkvaTC6/41dJCAvcFcufQ3XM9F1j4LnC/Zl5JgAABAgQINBTwJaUhrl0TqCSQu9iIMmMhF6wIKyoNCLuZWqDFrSG595C7QcjUhdA5AgQIECBAoL+AwKJ/DbSAQIlALgzofcGR+2V466P3nJJq22ZlgdytIXcCylxg4bxcecTpOwECBAgQCCzgS0rg4mgagQ+B3AVH2rRXaFEaVrgVxJAmUCaQO6eunuu191fWC1sRIECAAAECBB4KCCweAno5gZcEcr+6pmbc+eW1RvNzsz+2Y3i/qaFtH6sI5ELKK6FFzX2t4q+fBAgQIECAQAABFxABiqAJBAoFchcdaTdXLmIKD3u6We6X2+3FZlfU0LaP1QTOwsArAWXuPPVdYLWRpb8ECBAgQGAQAV9SBimUZhL4ZwZFySyLBPXWeZ27CBJWGLYEngnkzrHS0CIXdr4ddD5T8WoCBAgQIEBgGYG3LmyWAdVRAo0Fchce6fClFzFPm+pWkKeCXk8gL5ALLUrChtz7hu8C+TrYggABAgQIEOgg4EtKB3SHJPBAIMosi9xF1NZFt4I8KLaXEvglkAsccqHF09crBAECBAgQIECgi4DAogu7gxJ4JFASFrScZVFy/NRBYcWjMnsxgd8EcutZpPMtnfd7f7nAwncBg40AAQIECBAIKeBLSsiyaBSBU4GesyxKj5064P3FQCZQT6AkKDyaaZG7fcu5Wq9O9kSAAAECBAhUFPAlpSKmXRF4UaDk4qXFLIvcL7UbgdkVLw4Gh1pGoOS83zv3cuet7wLLDCEdJUCAAAECYwn4kjJWvbSWwKdA7lfTtG3u3vYroiUXS2l/woorqrYlcE0gFz58hoYptEz/5G4nSe8T/ggQIECAAAEC4QQEFuFKokEEigVKAoRasyxKjrU13PtKcQltSOCWwJXzseQAztkSJdsQIECAAAECrwv4kvI6uQMSqCrwxiyLKxdH3lOqltfOCBwKlM60yBHWCjVzx/HfCRAgQIAAAQKXBVxcXCbzAgKhBErDhLu3hpTuP6G4FSTU0NCYBQSunJ9HHM7bBQaKLhIgQIAAgVEFBBajVk67CfyPQMksizuBwpWLIRc9RiSBPgJXztO9Fvoe0KdujkqAAAECBAgUCPiiUoBkEwLBBa48anQLLrbF+L67lvaV/vl5sc/eSy6C2ZxAZYE7wYWgsXIR7I4AAQIECBCoK+Aio66nvRHoJfDkfvYUXqSQ4s5fem266En/9keAQH+BFFykv7PQ0Xnbv05aQIAAAQIECBQICCwKkGxCYACBq7MsanXp7toYtY5vPwQI7AtsIWT691+/Nvn7V7goYDRqCBAgQIAAgSEEBBZDlEkjCRQJvB1aCCuKymIjAgQIECBAgAABAgTuCAgs7qh5DYG4AnfuY7/TG/e+31HzGgIECBAgQIAAAQIEigUEFsVUNiQwjEDL0MK978MMAw0lQIAAAQIECBAgMLaAwGLs+mk9gTOB2sGFWRXGGwECBAgQIECAAAECrwkILF6jdiACXQTuPqb0s7FmVXQpnYMSIECAAAECBAgQWFtAYLF2/fV+LYGSxx1uIimkSE8U2F6zlpTeEiBAgAABAgQIECDQXUBg0b0EGkCgm8DnYw8/H3PokYfdSuLABAgQIECAAAECBAhsAgILY4EAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIXVqaAAAAbSSURBVIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBP4/csBUQgAdQDwAAAAASUVORK5CYII="
+ },
+ "supplyChainConsignment": {
+ "exportCountry": {
+ "code": "3455b114-4277-48f9-a307-51a2bc3891c1:string:IN"
+ },
+ "exporter": {
+ "postalAddress": {
+ "line1": "3b623f4d-24bf-4d3d-a026-9f043c6d04eb:string:Plot 123, Industrial Estate",
+ "line2": "5e0652cd-6595-405e-a769-e69503ffdd01:string:Sector 5, Tech Park",
+ "cityName": "1b78ad6e-00fc-4e30-ac80-001557030664:string:Mumbai",
+ "postcode": "2050ea9a-73c6-4357-9202-2eca9fa0a207:string:400001",
+ "countrySubDivisionName": "70cc8e28-86a6-442f-bacc-09c334392f91:string:Maharashtra",
+ "countryCode": "15ce7594-0206-4866-8330-32e57970e653:string:In"
+ },
+ "iD": "fb5591e6-6114-451b-a202-05017eebd111:string:EXP-IN-XYZ-0001",
+ "name": "c34e84b8-8db7-4f26-82d3-7f1751b2f9e6:string:XYZ Exports Pvt. Ltd."
+ },
+ "importCountry": {
+ "code": "827f7718-3844-4f07-87b9-06028126d316:string:GB"
+ },
+ "importer": {
+ "postalAddress": {
+ "line1": "3da1de14-52b0-4c2a-bca5-eed30455add4:string:Unit 88, Commercial Docks",
+ "line2": "3e62be73-e8de-4b3e-aa8e-3bc558d0484c:string:Trade Zone West",
+ "cityName": "751ac166-d747-4f07-9680-c6a494c8f0b2:string:London",
+ "postcode": "96f51837-e209-4e3c-8ddd-1087303e884a:string:E16 4HQ",
+ "countrySubDivisionName": "0c6543d6-8215-45af-aa81-794739274c66:string:Greater London",
+ "countryCode": "aa80dcf3-3448-4cb8-912c-6cf850c5c6e0:string:GB"
+ },
+ "iD": "a8279061-bd51-4da6-abdd-dd4f90cb9505:string:IMP-UK-XYZ-9999",
+ "name": "23401ac7-d889-4cbe-bea4-fa09f4651a5a:string:XYZ Foods Ltd."
+ },
+ "includedConsignmentItems": [
+ {
+ "crossBorderRegulatoryProcedure": {
+ "originCriteriaText": "2e9a7b8f-793e-4490-bcf1-7f2ef80c9ff1:string:Gross Volume: 2.5 CBM"
+ },
+ "manufacturer": {
+ "postalAddress": {
+ "line1": "7e1a78e3-8912-46ef-88a3-0b5c54809425:string:Plot 45, Agro Park",
+ "line2": "6a5bd538-1ecc-491e-8d8d-8c9e2592db4c:string:Phase II",
+ "cityName": "4372091b-259d-4929-b741-e8bf0af6b99e:string:Karnal",
+ "postcode": "d7b6a3e1-7c13-4284-bca9-e0af0e2ed30b:string:132001",
+ "countrySubDivisionName": "69f051b2-98ac-4a62-917c-58e12e5bf5e4:string:Haryana",
+ "countryCode": "664aebf6-492f-4392-932c-5a61d271d508:string:in"
+ },
+ "iD": "4ae8ecfa-3819-4eca-82bf-e91907922d6f:string:MFG-IN-XYZ-001",
+ "name": "b9356c78-bfcd-4986-ae9e-5203991f1531:string:XYZ Agro Industries"
+ },
+ "tradeLineItems": [
+ {
+ "invoiceReference": {
+ "attachedBinaryFile": {
+ "uRI": "22e422a6-76f2-44ef-b98f-a29b3cf297f7:string:https://docs.tweglobal.com/8c624a35-9497-41fb-a548-cb5cf43bac21.pdf"
+ },
+ "iD": "f419c4ee-405c-48a8-9364-dff0dc15d228:string: INV-XYZ-0001",
+ "formattedIssueDateTime": "cdbb5af6-e5be-4289-b10f-e73a901dfa52:string:2025-06-10T10:09:00.000Z"
+ },
+ "tradeProduct": {
+ "harmonisedTariffCode": {
+ "classCode": "5a41a606-6566-4a0d-a6ba-d4c1a0722fee:string:10063090",
+ "className": "9a83b06a-5b20-47c4-80c3-846de83e31dc:string:Semi-milled or wholly milled rice, whether or not polished or glazed"
+ },
+ "originCountry": {
+ "code": "69600792-0323-4c13-96dc-7bb928ff014b:string:IN"
+ },
+ "iD": "16452d53-026d-430d-a896-996513d67044:string:TP-XYZ-1001",
+ "description": "49bfa3fa-74f3-4a78-8726-370731b93afa:string:XYZ Premium Basmati Rice, 25kg Bag"
+ },
+ "transportPackages": [
+ {
+ "iD": "de6f5959-3add-4c4b-bedb-14bf890e117f:string:PKG-XYZ-0001",
+ "grossVolume": "56fab72d-088f-432e-8835-d2001fe4925c:string:3.0 CBM",
+ "grossWeight": "7b706484-3c21-47b4-ac7c-b7a5e4f083a4:string:1000 kg"
+ }
+ ],
+ "sequenceNumber": "b8c78444-e725-40b2-8829-979d7e73555f:number:1"
+ }
+ ],
+ "iD": "34865586-d59b-4a80-a346-acb427cd7ef7:string:10063090",
+ "information": "a824dfba-fab5-4732-844c-6a539e4f0469:string:Sample Rice Product, 25kg Bag"
+ }
+ ],
+ "loadingBaseportLocation": {
+ "iD": "c8c586e6-d999-42b2-880f-0743c5d2fd97:string:PORT-IN-XYZ",
+ "name": "0476a486-296a-4cb3-b35e-8ae8cc03b288:string:Nhava Sheva (JNPT), India"
+ },
+ "mainCarriageTransportMovement": {
+ "usedTransportMeans": {
+ "name": "abd46ee0-bdb2-4043-819e-3190779b2cd5:string:Vessel β XYZ CARRIER",
+ "iD": "29d85737-23f9-4cdd-8248-df5665c03f2f:string:VSL-XYZ-0001"
+ },
+ "departureEvent": {
+ "departureDateTime": "6158cfe3-cfeb-4d1e-9c75-1932a1188c2b:string:2025-06-18T10:17:00.000Z"
+ },
+ "iD": "b2f53a66-1647-46a7-a5d7-898a54391120:string:MCTM-XYZ-1234",
+ "information": "7f17f4b6-30cd-41e7-891e-834232828b8f:string:Ocean Freight via XYZ Shipping Lines"
+ },
+ "unloadingBaseportLocation": {
+ "iD": "69e796b4-80b5-4a38-ad50-71aa89e64c5e:string:PORT-UK-XYZ",
+ "name": "09130b7e-a90c-435d-bdd2-ad024dd37471:string:Port of Felixstowe, United Kingdom"
+ },
+ "iD": "72c81fc5-d83a-4f3c-9011-d185a0f9e499:string:CONS-FAKE-12345"
+ },
+ "iD": "31d1b0d5-ceb7-496c-884b-be9a96c379a7:string:COO-FAKE-0001",
+ "issueDateTime": "3f854e65-0cfe-4553-9ed0-9777bbfa7636:string:2025-06-18T10:09:00.000Z",
+ "attachments": [
+ {
+ "data": "bc917164-ae21-4e09-859b-9c8038e06e05:string:JVBERi0xLjMNCiXi48/TDQoNCjEgMCBvYmoNCjw8DQovVHlwZSAvQ2F0YWxvZw0KL091dGxpbmVzIDIgMCBSDQovUGFnZXMgMyAwIFINCj4+DQplbmRvYmoNCg0KMiAwIG9iag0KPDwNCi9UeXBlIC9PdXRsaW5lcw0KL0NvdW50IDANCj4+DQplbmRvYmoNCg0KMyAwIG9iag0KPDwNCi9UeXBlIC9QYWdlcw0KL0NvdW50IDINCi9LaWRzIFsgNCAwIFIgNiAwIFIgXSANCj4+DQplbmRvYmoNCg0KNCAwIG9iag0KPDwNCi9UeXBlIC9QYWdlDQovUGFyZW50IDMgMCBSDQovUmVzb3VyY2VzIDw8DQovRm9udCA8PA0KL0YxIDkgMCBSIA0KPj4NCi9Qcm9jU2V0IDggMCBSDQo+Pg0KL01lZGlhQm94IFswIDAgNjEyLjAwMDAgNzkyLjAwMDBdDQovQ29udGVudHMgNSAwIFINCj4+DQplbmRvYmoNCg0KNSAwIG9iag0KPDwgL0xlbmd0aCAxMDc0ID4+DQpzdHJlYW0NCjIgSg0KQlQNCjAgMCAwIHJnDQovRjEgMDAyNyBUZg0KNTcuMzc1MCA3MjIuMjgwMCBUZA0KKCBBIFNpbXBsZSBQREYgRmlsZSApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY4OC42MDgwIFRkDQooIFRoaXMgaXMgYSBzbWFsbCBkZW1vbnN0cmF0aW9uIC5wZGYgZmlsZSAtICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjY0LjcwNDAgVGQNCigganVzdCBmb3IgdXNlIGluIHRoZSBWaXJ0dWFsIE1lY2hhbmljcyB0dXRvcmlhbHMuIE1vcmUgdGV4dC4gQW5kIG1vcmUgKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NTIuNzUyMCBUZA0KKCB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDYyOC44NDgwIFRkDQooIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjE2Ljg5NjAgVGQNCiggdGV4dC4gQW5kIG1vcmUgdGV4dC4gQm9yaW5nLCB6enp6ei4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjA0Ljk0NDAgVGQNCiggbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDU5Mi45OTIwIFRkDQooIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNTY5LjA4ODAgVGQNCiggQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA1NTcuMTM2MCBUZA0KKCB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBFdmVuIG1vcmUuIENvbnRpbnVlZCBvbiBwYWdlIDIgLi4uKSBUag0KRVQNCmVuZHN0cmVhbQ0KZW5kb2JqDQoNCjYgMCBvYmoNCjw8DQovVHlwZSAvUGFnZQ0KL1BhcmVudCAzIDAgUg0KL1Jlc291cmNlcyA8PA0KL0ZvbnQgPDwNCi9GMSA5IDAgUiANCj4+DQovUHJvY1NldCA4IDAgUg0KPj4NCi9NZWRpYUJveCBbMCAwIDYxMi4wMDAwIDc5Mi4wMDAwXQ0KL0NvbnRlbnRzIDcgMCBSDQo+Pg0KZW5kb2JqDQoNCjcgMCBvYmoNCjw8IC9MZW5ndGggNjc2ID4+DQpzdHJlYW0NCjIgSg0KQlQNCjAgMCAwIHJnDQovRjEgMDAyNyBUZg0KNTcuMzc1MCA3MjIuMjgwMCBUZA0KKCBTaW1wbGUgUERGIEZpbGUgMiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY4OC42MDgwIFRkDQooIC4uLmNvbnRpbnVlZCBmcm9tIHBhZ2UgMS4gWWV0IG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NzYuNjU2MCBUZA0KKCBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY2NC43MDQwIFRkDQooIHRleHQuIE9oLCBob3cgYm9yaW5nIHR5cGluZyB0aGlzIHN0dWZmLiBCdXQgbm90IGFzIGJvcmluZyBhcyB3YXRjaGluZyApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY1Mi43NTIwIFRkDQooIHBhaW50IGRyeS4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NDAuODAwMCBUZA0KKCBCb3JpbmcuICBNb3JlLCBhIGxpdHRsZSBtb3JlIHRleHQuIFRoZSBlbmQsIGFuZCBqdXN0IGFzIHdlbGwuICkgVGoNCkVUDQplbmRzdHJlYW0NCmVuZG9iag0KDQo4IDAgb2JqDQpbL1BERiAvVGV4dF0NCmVuZG9iag0KDQo5IDAgb2JqDQo8PA0KL1R5cGUgL0ZvbnQNCi9TdWJ0eXBlIC9UeXBlMQ0KL05hbWUgL0YxDQovQmFzZUZvbnQgL0hlbHZldGljYQ0KL0VuY29kaW5nIC9XaW5BbnNpRW5jb2RpbmcNCj4+DQplbmRvYmoNCg0KMTAgMCBvYmoNCjw8DQovQ3JlYXRvciAoUmF2ZSBcKGh0dHA6Ly93d3cubmV2cm9uYS5jb20vcmF2ZVwpKQ0KL1Byb2R1Y2VyIChOZXZyb25hIERlc2lnbnMpDQovQ3JlYXRpb25EYXRlIChEOjIwMDYwMzAxMDcyODI2KQ0KPj4NCmVuZG9iag0KDQp4cmVmDQowIDExDQowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDAwMTkgMDAwMDAgbg0KMDAwMDAwMDA5MyAwMDAwMCBuDQowMDAwMDAwMTQ3IDAwMDAwIG4NCjAwMDAwMDAyMjIgMDAwMDAgbg0KMDAwMDAwMDM5MCAwMDAwMCBuDQowMDAwMDAxNTIyIDAwMDAwIG4NCjAwMDAwMDE2OTAgMDAwMDAgbg0KMDAwMDAwMjQyMyAwMDAwMCBuDQowMDAwMDAyNDU2IDAwMDAwIG4NCjAwMDAwMDI1NzQgMDAwMDAgbg0KDQp0cmFpbGVyDQo8PA0KL1NpemUgMTENCi9Sb290IDEgMCBSDQovSW5mbyAxMCAwIFINCj4+DQoNCnN0YXJ0eHJlZg0KMjcxNA0KJSVFT0YNCg==",
+ "filename": "caef164f-bfc8-4370-a959-e8a02ba56f9f:string:sample.pdf",
+ "type": "1b96588d-18e2-4910-a49d-ab9c8a8d7864:string:application/pdf"
+ },
+ {
+ "data": "a08aab84-e181-4f34-bde4-ba9c1d8d80ea:string:JVBERi0xLjYNJeLjz9MNCjI0IDAgb2JqDTw8L0ZpbHRlci9GbGF0ZURlY29kZS9GaXJzdCA0L0xlbmd0aCAyMTYvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjePI9RS8MwFIX/yn1bi9jepCQ6GYNpFBTEMsW97CVLbjWYNpImmz/fVsXXcw/f/c4SEFarepPTe4iFok8dU09DgtDBQx6TMwT74vaLTE7uSPDUdXM0Xe/73r1FnVwYYEtHR6d9WdY3kX4ipRMV6oojSmxQMoGyac5RLBAXf63p38aGA7XPorLewyvFcYaJile8rB+D/YcwiRdMMGScszO8/IW0MdhsaKKYGA46gXKTr/cUQVY4We/cYMNpnLVeXPJUXHs9fECr7kAFk+eZ5Xr9LcAAfKpQrA0KZW5kc3RyZWFtDWVuZG9iag0yNSAwIG9iag08PC9GaWx0ZXIvRmxhdGVEZWNvZGUvRmlyc3QgNC9MZW5ndGggNDkvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjeslAwULCx0XfOL80rUTDU985MKY42NAIKBsXqh1QWpOoHJKanFtvZAQQYAN/6C60NCmVuZHN0cmVhbQ1lbmRvYmoNMjYgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0ZpcnN0IDkvTGVuZ3RoIDQyL04gMi9UeXBlL09ialN0bT4+c3RyZWFtDQpo3jJTMFAwVzC0ULCx0fcrzS2OBnENFIJi7eyAIsH6LnZ2AAEGAI2FCDcNCmVuZHN0cmVhbQ1lbmRvYmoNMjcgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0ZpcnN0IDUvTGVuZ3RoIDEyMC9OIDEvVHlwZS9PYmpTdG0+PnN0cmVhbQ0KaN4yNFIwULCx0XfOzytJzSspVjAyBgoE6TsX5Rc45VdEGwB5ZoZGCuaWRrH6vqkpmYkYogGJRUCdChZgfUGpxfmlRcmpxUAzA4ryk4NTS6L1A1zc9ENSK0pi7ez0g/JLEktSFQz0QyoLUoF601Pt7AACDADYoCeWDQplbmRzdHJlYW0NZW5kb2JqDTIgMCBvYmoNPDwvTGVuZ3RoIDM1MjUvU3VidHlwZS9YTUwvVHlwZS9NZXRhZGF0YT4+c3RyZWFtDQo8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjQtYzAwNSA3OC4xNDczMjYsIDIwMTIvMDgvMjMtMTM6MDM6MDMgICAgICAgICI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnBkZj0iaHR0cDovL25zLmFkb2JlLmNvbS9wZGYvMS4zLyIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIj4KICAgICAgICAgPHBkZjpQcm9kdWNlcj5BY3JvYmF0IERpc3RpbGxlciA2LjAgKFdpbmRvd3MpPC9wZGY6UHJvZHVjZXI+CiAgICAgICAgIDx4bXA6Q3JlYXRlRGF0ZT4yMDA2LTAzLTA2VDE1OjA2OjMzLTA1OjAwPC94bXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZVBTNS5kbGwgVmVyc2lvbiA1LjIuMjwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxNi0wNy0xNVQxMDoxMjoyMSswODowMDwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6TWV0YWRhdGFEYXRlPjIwMTYtMDctMTVUMTA6MTI6MjErMDg6MDA8L3htcDpNZXRhZGF0YURhdGU+CiAgICAgICAgIDx4bXBNTTpEb2N1bWVudElEPnV1aWQ6ZmYzZGNmZDEtMjNmYS00NzZmLTgzOWEtM2U1Y2FlMmRhMmViPC94bXBNTTpEb2N1bWVudElEPgogICAgICAgICA8eG1wTU06SW5zdGFuY2VJRD51dWlkOjM1OTM1MGIzLWFmNDAtNGQ4YS05ZDZjLTAzMTg2YjRmZmIzNjwveG1wTU06SW5zdGFuY2VJRD4KICAgICAgICAgPGRjOmZvcm1hdD5hcHBsaWNhdGlvbi9wZGY8L2RjOmZvcm1hdD4KICAgICAgICAgPGRjOnRpdGxlPgogICAgICAgICAgICA8cmRmOkFsdD4KICAgICAgICAgICAgICAgPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5CbGFuayBQREYgRG9jdW1lbnQ8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6QWx0PgogICAgICAgICA8L2RjOnRpdGxlPgogICAgICAgICA8ZGM6Y3JlYXRvcj4KICAgICAgICAgICAgPHJkZjpTZXE+CiAgICAgICAgICAgICAgIDxyZGY6bGk+RGVwYXJ0bWVudCBvZiBKdXN0aWNlIChFeGVjdXRpdmUgT2ZmaWNlIG9mIEltbWlncmF0aW9uIFJldmlldyk8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6U2VxPgogICAgICAgICA8L2RjOmNyZWF0b3I+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz4NCmVuZHN0cmVhbQ1lbmRvYmoNMTEgMCBvYmoNPDwvTWV0YWRhdGEgMiAwIFIvUGFnZUxhYmVscyA2IDAgUi9QYWdlcyA4IDAgUi9UeXBlL0NhdGFsb2c+Pg1lbmRvYmoNMjMgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0xlbmd0aCAxMD4+c3RyZWFtDQpIiQIIMAAAAAABDQplbmRzdHJlYW0NZW5kb2JqDTI4IDAgb2JqDTw8L0RlY29kZVBhcm1zPDwvQ29sdW1ucyA0L1ByZWRpY3RvciAxMj4+L0ZpbHRlci9GbGF0ZURlY29kZS9JRFs8REI3Nzc1Q0NFMjI3RjZCMzBDNDQwREY0MjIxREMzOTA+PEJGQ0NDRjNGNTdGNjEzNEFCRDNDMDRBOUU0Q0ExMDZFPl0vSW5mbyA5IDAgUi9MZW5ndGggODAvUm9vdCAxMSAwIFIvU2l6ZSAyOS9UeXBlL1hSZWYvV1sxIDIgMV0+PnN0cmVhbQ0KaN5iYgACJjDByGzIwPT/73koF0wwMUiBWYxA4v9/EMHA9I/hBVCxoDOQeH8DxH2KrIMIglFwIpD1vh5IMJqBxPpArHYgwd/KABBgAP8bEC0NCmVuZHN0cmVhbQ1lbmRvYmoNc3RhcnR4cmVmDQo0NTc2DQolJUVPRg0K",
+ "filename": "5ba283ff-393d-428a-9f3a-864daf2bcb77:string:veryverylongfilenameoverhereveryverylongfilenameoverhere.pdf",
+ "type": "2a233834-f805-4057-a414-719f8fa18b2e:string:application/pdf"
+ }
+ ],
+ "links": {
+ "self": {
+ "href": "ad641112-04b2-43b1-9c1a-875c7c609742:string:https://actions.tradetrust.io?q=%7B%22type%22%3A%22DOCUMENT%22%2C%22payload%22%3A%7B%22uri%22%3A%22https%3A%2F%2Fgallery.tradetrust.io%2Fstatic%2FOA%2Fcertificate-of-origin-default.json%22%2C%22redirect%22%3A%22https%3A%2F%2Fref.tradetrust.io%2F%22%2C%22chainId%22%3A%22101010%22%7D%7D"
+ }
+ }
+ },
+ "signature": {
+ "type": "SHA3MerkleProof",
+ "targetHash": "9d10572d85cd29fdefaf17c6b94ae0e5425b2ea93bf75bcdb1840a6a93e13aab",
+ "proof": [],
+ "merkleRoot": "9d10572d85cd29fdefaf17c6b94ae0e5425b2ea93bf75bcdb1840a6a93e13aab"
+ },
+ "proof": [
+ {
+ "type": "OpenAttestationSignature2018",
+ "created": "2026-02-03T08:27:42.117Z",
+ "proofPurpose": "assertionMethod",
+ "verificationMethod": "did:ethr:0x433097a1C1b8a3e9188d8C54eCC057B1D69f1638#controller",
+ "signature": "0x3ae4db51d9ab26d01f9005ad6839f24aa3c8b218bddaf487dda48296d4f7f95e427ee45e6b784d6e6924ea0af2251a94859bcd25b73156a9f4b4e5bb5c4345ea1c"
+ }
+ ]
+}
diff --git a/src/__tests__/__fixtures__/oa/2.0/signed_wrapped_oa_dns_txt_docstore_v2.json b/src/__tests__/__fixtures__/oa/2.0/signed_wrapped_oa_dns_txt_docstore_v2.json
new file mode 100644
index 0000000..0912c6c
--- /dev/null
+++ b/src/__tests__/__fixtures__/oa/2.0/signed_wrapped_oa_dns_txt_docstore_v2.json
@@ -0,0 +1,109 @@
+{
+ "version": "https://schema.openattestation.com/2.0/schema.json",
+ "data": {
+ "name": "d9464745-6108-42d8-af4d-6f0e82d42899:string:TradeTrust ChAFTA Certificate of Origin v2",
+ "supplyChainConsignment": {
+ "exportCountry": {
+ "code": "fe024186-d712-4475-ac98-d7f82d3aff3a:string:65"
+ },
+ "exporter": {
+ "postalAddress": {
+ "line1": "dd62eff8-af36-4a89-95e0-7b26455ab870:string:10 Pasir Panjang Road #10-01 Mapletree Business City",
+ "postcode": "58f6a94a-5d09-4a5c-99e9-6c3fdacf8ff7:string:117438",
+ "cityName": "f287d49c-9139-459c-b2da-a87276f9b723:string:Singapore",
+ "countryCode": "0e7139b8-aeea-448f-b445-f6c20373e7b5:string:65"
+ },
+ "iD": "1ee27a61-719d-4a06-ba19-61828c20c1d7:string:333",
+ "name": "239966a6-2147-4692-ba43-0078d7741790:string:John Doe"
+ },
+ "importCountry": {
+ "code": "4871cf7c-ef3d-4ea7-98d7-63d4c02d6407:string:60"
+ },
+ "importer": {
+ "postalAddress": {
+ "line1": "83c4339b-fceb-43f2-b597-224d1a0d6164:string:106 Blk F7 Seksyen 1 Bdr Baru Wangsa Maju Setapak",
+ "cityName": "54caec03-7514-4f1b-ad94-8e4f1ae380b8:string:Kuala Lumpur",
+ "postcode": "34bb275d-55ec-4dea-9689-76f0fe9e4062:string:53300",
+ "countryCode": "718306ff-c27e-4cf7-a959-46b433dcccb8:string:60"
+ },
+ "iD": "2a907caf-03d0-428a-b80c-331aa67dc9e2:string:444",
+ "name": "73da8ae6-58e5-4dc6-be4b-cb52ee417eda:string:Alice Tan"
+ },
+ "loadingBaseportLocation": {
+ "iD": "bdf341f3-9e28-47d9-86a3-3abd6c5e532f:string:555",
+ "name": "9091c162-cf23-4443-a751-645144b9dd17:string:Some loading port name"
+ },
+ "mainCarriageTransportMovement": {
+ "usedTransportMeans": {
+ "name": "0894b8d4-f416-44e4-b849-462c15c527c0:string:Van",
+ "iD": "9501e81a-a023-4a27-b41e-7c1b7540924a:string:777"
+ },
+ "departureEvent": {},
+ "iD": "cc4b20ba-d319-4bbf-be49-a8c01aa83934:string:666",
+ "information": "171f9c09-d74a-45c4-916f-7b58cd9b75f5:string:Some information here"
+ },
+ "unloadingBaseportLocation": {
+ "iD": "a06f13db-dab2-4f67-8481-ddc97ea3a46d:string:888",
+ "name": "b3966acc-a5a7-4770-b047-3cdd8ae8a8ef:string:Some unloading port name"
+ },
+ "iD": "6e8c7d60-56b3-452e-be01-b656a32e0d5d:string:222",
+ "information": "bfcd4724-1486-4e36-9e5a-2969789246ee:string:Consignment Information",
+ "includedConsignmentItems": []
+ },
+ "$template": {
+ "type": "eafb8c44-2925-4fd6-b262-8992876a80d0:string:EMBEDDED_RENDERER",
+ "name": "ee7d000a-e5bf-40c1-ad7d-7833319c462d:string:CHAFTA_COO",
+ "url": "ec9da1d2-1be2-4b33-b96f-a7f0524fc57a:string:https://generic-templates.tradetrust.io"
+ },
+ "issuers": [
+ {
+ "name": "ab4b2ef9-6be3-4df6-a4c4-b59523dbd8e5:string:Demo Issuer",
+ "documentStore": "a0b4f0d3-01c0-4072-a8ad-6f8fa6ee08ef:string:0xA594f6e10564e87888425c7CC3910FE1c800aB0B",
+ "identityProof": {
+ "type": "7a473256-585c-43b2-a429-1ef5d819266e:string:DNS-TXT",
+ "location": "5ea60d6e-24d9-4537-b4ee-564fe2b8735f:string:example.tradetrust.io"
+ }
+ }
+ ],
+ "firstSignatoryAuthentication": {
+ "signature": "5c354242-e993-41f8-9fb8-eae22dea5bde:string:data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/4QOBaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjUtYzAyMSA3OS4xNTU3NzIsIDIwMTQvMDEvMTMtMTk6NDQ6MDAgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9IjI0NjlGRjdFNDlEMThFM0U5Njg1NjlEMUQxN0I2NEI0IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjI2MTIxNkJFREE1QTExRTY4QjkyQUE2NTA4NTZFNkRCIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjI2MTIxNkJEREE1QTExRTY4QjkyQUE2NTA4NTZFNkRCIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOmY2OTUxMGQ5LWM0NGMtMzM0ZC1iNzJiLTU1MWZkMTdkZTBiMCIgc3RSZWY6ZG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjUwYWY1Y2Y2LWQ5NmQtMTFlNi04NTdiLTg2NWI3MTA4OTkwZSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/uAA5BZG9iZQBkwAAAAAH/2wCEAAYEBAQFBAYFBQYJBgUGCQsIBgYICwwKCgsKCgwQDAwMDAwMEAwODxAPDgwTExQUExMcGxsbHB8fHx8fHx8fHx8BBwcHDQwNGBAQGBoVERUaHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fH//AABEIASwB9AMBEQACEQEDEQH/xACEAAEAAgIDAQAAAAAAAAAAAAAABgcEBQECAwgBAQEAAAAAAAAAAAAAAAAAAAABEAABBAECBAMFBgMFBwQDAAAAAQIDBAURBiExEgdBIhNRYXEyFIGRQlIVCGIjM6GxwXJD0eGCkqJTFrJjJDREJRcRAQEBAAAAAAAAAAAAAAAAAAABEf/aAAwDAQACEQMRAD8A+qQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA18PEAAAAAAAAAAAAAAAAAAaLeW9tu7PxD8nm7KQxJqkMKaLLM/wDJEzXVy/2J4gUtt+73W7xZVcrBlJtp7NpWVjjZTkfFYlVmiub1N6VkdovFzvIngi8UA+hkTRETXXTxUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEW3vv2ntpkFOvXflNxZDVuLwtfjLK787vyRN/E9QIjd2S7H4bMb531LFm9y16Uk9aJY0dToemxz2RVo3aouj9NXuTVfv1CUdosazH9tdvRJ1dc9OO5Or/mWa3/APIlVf8AjlUCXgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFt572/RX18Ti6y5TdGSRUxuMYvBE5LPYd/pws8XLz5J7g42XsduEfPlspY/VN1ZFEXJ5Z6acOaQwNX+nCzkjU5819wevcynLd7ebjrRJrJJjrKNT3pGq/4Ae+wLUVrYu3rEX9OXG1HNTlp/IbwA3wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjO9N5Jg21sfj4EyO5coqx4jFounW5E1dLK78EMacXOX4ANl7Nbg457+Qm/UNzZPSTL5RycXu8IovyQx8mNT48wJMB1lijmifFI1HRyNVj2ryVrk0VAK+7OWZKNDLbMtvVbu17kkEfVrq+lO5Za0ia/h0VzE/ygWGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaDem76e18SlqSN1q9Ze2ti8bH/VtWZF0jiYnx5r4IBg7G2jdx7rGe3BI23uzKoi3p04srxc2U6/Ppij8fzO4r4AS0AAAr3uBXt7d3BQ7gUGSS16jFp7kqQpq6Wi9dUlRvi6B3m+HACeUrtS9Tgu05Wz1LLGywTMXVr2PTqa5q+xUUD2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxMtlaGJxlnJ5CVsFKnG6aeV3BEa1NV+32AQPYeHyG5c0vcPccCwySsWPa+MkXX6Ok5P6zk5etOi66+DfjogWOAAAAOHsY9jmPajmORWuaqaoqLwVFQCsaM9rtnmnY6+50uwsnM5+PyD11TGTyqqugmVeUD3L5Xcmrz4KugWc1zXNRzVRzXJq1ycUVF8UA5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABWWVWXuBvpcHHo7Z+15mS5pVTqZdyCJ1R1ePBzIfmk9/BeaAWaiIiaJwRAAAAAAAY+Qx9LI0pqN6BlmpYarJoJERzXNXwVFArZKm7O2TdMdBLuLYrXa/RMXqyGOjXn6Oq/wA6Fv5Oae7xCd7a3Xt/cuPbfwt2O3BwSRrV0kjcvHoljXR7He5yAbYAAAAAAAAAAAAAAAAAAAMPLZjFYehJfyluKlTi/qTzORjU93Hmq+CIBTmU/cdbyGVdjthbbsZ/oXzW3NkRqoni2KNrndK+CuVPgTRZHbrerd4bcTKOqOoW4ppal6m9er054V0e1HaN1Tinh7iiTgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIZ3Q3ZfwuIgxmDb626c9J9FhYPY9yfzJ3exkLPMqqBuNl7Uo7V25Uw1RVkSFFfYsP4yTTyL1SyvXxc9y/dwA3YAAAAAAAACGbl7V7fy99cxQlnwO4U4pl8a/0ZXe6VqeSRq+KOTiBqm57untNOncGNbuzEs4JlMQxGX0T801Nyox6+1Y1T4ASDbXc3Y+45/pcZlI/r0VUdj7COrWkVE1cnozIx66ePSioBKAAAAAAAAAAAAAAAAACqO+3bHcm82Yezgnwyy42RyzULT3Mhka9Wrqun+XRfHReAEk3NlcZ2/wBivkxuPgr21RlbG4ymxEbLem8kcbGtRqv83HlqqIBse3u2H7Z2hjsTM71LrGLLkJddeu1O5ZZ3a+Keo9dPcBIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOk88MEEk8z0jhiar5Hu4I1rU1VV+CAV322bZ3Tm8h3DvxubXs9VHbFeRP6VCN3nmRPB1iRNfgnsUCxwAAAAA6TWIIUas0jY0e5rGK9Ubq9y6Namviq8kA7gAAAABHtz9v9m7oj6c5iYLb/wAM6t6Jm/CVnS9PvAi7e2e8MCqv2Zu+zFAio5uJzCfX1VRPwJI7+dG3/IoHK797kYJXN3Ts6S7XZqq5Lb8iWWqiLzWtKrJG8P4gNrhe8XbnLPSGPMxU7eqNWnkOqnMjl8OmdGdX/DqBMY5I5GNkjcj43Jq17VRUVF8UVAOwADrNKyKJ8r10ZG1XOX3NTVQKk7e9/wBm898P29VwcsFBzZX18isvU5EhTXqlj6ERiO1RODl0VUTxILdKAAChu+nezd+1N3UtvbZSs57oY5JkfH68r5ZXKjIunVEbqiJ711AvHHOvOx9Z2QbGy+sTFtshVVjSZWp6iMVePT1a6agZAAAqo1FVV0ROKqvJEArLbKu7gbzfuudqrtfb0j622ona9Ni2nlnuqi80Z8kfv15KgFmgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK67sWreXsYjYGOkcyxuKRX5SWNfNDjIFR07vd6nyN9vICf0aVWjSgpVImw1KsbYa8LODWRxtRrWp7kRAPYAAAAAPOetXsMRk8bZGoqOajkRdHN4o5NeSp4KB6JwQAAAAAAAABqs3tTbOdj6Mzi6t9NNEWeJj3InucqdSfYoEMXshice+SXaWbym2JHr1ejTsLJV1/iry9SO+1QOj8b37w+n0eWxG5oG823q76VhyexFgX09feoBe7G6cUjk3TsPKVEbzs4xY8jB0+LnOasatT7wPLL9/u3D9uZOWtkujIxVZVioWYZoJXSKxUazR7ERfNz0VQI5+1jaVKptqxumWRkuTy73xwta5FdFUif09KtT5VkkZ1L7kaBeYADFy2TqYvGW8lbd0VacT55nexkbVcv8AcB87dkdp3N9b/wAn3Mz0WtOC052Mjd8sllPlVNebazdET+LT8qgfSYAABW/cXKZLcOar9usBMsUtxiT7lvxrotShqmrEVOUk+vS1P8F1An2LxlDFY6tjcfC2vSqRtirws5NY1NET/eBlAAAAAAAAAAAAAAAAAACN5ruPsnDW0pXcrEuQVelKFdHWbPV7PRgSSTX7ANPJ3m21Xf1X8bmcfT49V+1jbEUDU9rnK3qT/lAmtC/SyFOG7RnjtVLDUfDPE5Hse1fFrk4Ae4AAAAAAAAAAAAAOskjIo3SSORsbEVz3LyRETVVUCtu0kb9wZLO9w7Sarmp1p4ZHJxjx1Rysb0+z1ZEVXJ/CigWWAAAAAAAAAAAAAAAAAAAAABRP7gblLcO4NvdusfFG7LZS1E+9abG10kEC68OrTX5OqRU9iIBM7PYLtdKjFgxTqM8aI1lmnPNDImiaa6o7TX7APNe0mbptT9D35naapro23Ky9HovgjJWp/eB2fhO+VBGto7kxOXamqKuRpPrvX2a/TO0X+wCte7O5+72S9LtzZo412VznTIxMVJK57oI1Vyo5Jl0ai+nqqr4IBN9vbu3btbC0sNJ22yFahQibDCmPsQXV6WporlRvQvU53FePvA2De+GMhVUym2dw4tqfNJPj3OYnxWJz/wC4D1b397XI5rLOTmpyOXTosU7bFT3qvpK3T7QMfdXfjY9Db8tvB5Stk8pKqQUaiP6P5r+DXy9fT0Rt5ucoGd2ux22sNipXtzlPMbgysv1OayUViKVZrLuPQ1UXX049dGJ9viBPGua5NWqip7uIHIAAB5W7dapWltWpWw1oGLJNM9dGtY1NVcqr4IgFLbd3Ju3uh3CiyWJs2cTsPb0+qvje+Jb0rF1Rj+nRHo/h1NXVGs97iC7igAAxrmTxtKNZbluGtGmqq+aRsbU058XKgEZud4O1tRzmzbqxnU1dHNjsxyqmvujVwEjxGZxOZoR5HE3Ib9GbX0rNd7ZI3dK6Lo5qqnBU0UDMAAAIjuHuPjMfkUwuJryZ7cTuCYymqKka+2xMvkhanj1cfcBhM2jvTcLkl3dmPoqLtdcDhnPhjVF8J7a6TScPma3pQCU4PbOAwNVtXD0IaUKeETURzve56+Zy+9ygbCSOOSN0cjUfG9Fa9jkRUVF4KiooEG7c425hc9uzBRVJK236tuGxh3Oa5I1+qi67EcSrwVjHonLkqgTsAAAAAAAAAAAAAEC725m5j9g2qmPX/wDZ5uWLE0URdFWS27oX/o6gJbt7C1cHgsfh6iaV8fXjrx+9I2o3qX3uXioGwAAAAAAAAAAAAAAAAAAAABpN4blj29hJLqRLZuyubWxtFvz2Lcy9MMTfi7n7E1XwAqftHtmxZ7rbkzuTmS/dw8bali4rVWN+Rteez6Dl+VIGs9FE/KvvAvMABpd47qobXwFjL3NX+noytXb8887+EcLE/M53+0CpOwGHym4dx53uVn3JLdsyPoY7TXpYjVRZ1Yip8rdGxsVPY4C9QAGn3ZmsDg8Dby2cRn0FZmsiPa16vXk1jWu5ucvBEAg2xe3VPL2Lm8N3YWqmQy+n0OHlhY6OlTRE9Nro3N6fXeiavcqapy4cUA3d/sp2svKrptuVWOX/ALKOhT7onMQDAf2E7WQt64sfLTRNVV8NyzFz4cV9QCN5TanaXEI5Ze4OQx7IkVUqxZvqc1reaMiRXyL9moEXkymwHuamH7n7psMdqrK0H11h3PkjkjjRPtIMmSxuW1GseHyO/rzH6elK+tHXj0/imk6HfbohRXrpO5m/8pY2pg8hlrsETFXIRZC62SJrY3afzHs0j+blxXVSCb7d7G99cdj4qFTdTMRRZqra0Fyy1rVcurvLGzp1VfeUemb2Zunb6sbufvDJi55ET066WLT3qirp1K31mPRv8XToTBvV/b9vOyxHS9yslKx6aoqfUOa5F8eNpddSjTbl/brDicPazOW31cjgpxrJNKsT1104IiazrxcvBPeB8/ptHIz14Mtar3HYG1aSCK6kerpHovmSNXaI93uTx4DRf0P7Mtuoiuk3HZVF4ppXYzRPfq9wGLtCrunY785kO3yuzG0MSrUyK3tWR25ItUmdURiL8ic3p9vUmiEH0FtTclHcu3aGco6pWvRJIjHfMxyKrXsdp4seitX4FHO49z4PbmPdfzFplaHlG1eMkj/BkTE8z3L7EQCJRx703w1JZZJtrbWkXVldiKzK2mf+45eFZjvY3Vy/AgmOD2/hcFSSliKcdOsi9SsjTi535nuXVz3e9yqpRsAAAAAAAAAAAAAAAAAABWHcZ6ZDul27wau0jbZtZOVvvqQq+L/qaoFngAAAAAAAAAAAAAAAAHnZswVa0tmw9Iq8DHSTSO4I1jE6nOX4IgFH7B7odx+4PcyaXBtjqbAxj3MuJNE1yytVFRmkmnX6r10ciNdo1OeviF6AedixBWry2bEjYoIWOklleqNa1jU1c5yryREArBM869WyXc/KMWLBYetMm0qcqaK9qt6XXnov4rC6Mi9jOP4gNt2Q2/JiO31KxaYjcnmnPy2Rf5up0ttetnUjuKOSLoaqe1AJ6B1llihifLK9I4o2q+R7l0a1rU1VVVeSIgHz93O3Y7K4abdb3pHRlkditkVpHemkj5tY7GVkReKIjdWxa/Knm8UAsDbu7+1myNr47b6bmx7mY6FsTuixHNI5/wA0j1bErnaueqryA9F73bPnTXD1srnF100x2PsvT/mkbE3+0DrZ7nbldTktUNiZX0omufJJkpKuPjaxqaq5yvkkciIicfKBBar+6vc+9jN0V8bjKGBxr3vxePycs8sE07dUSy5sTGOk6HfJr0pw4e+Dbbv3Fu/BrAzdXcbF7blm5VMZjXWpFReCO/nOmka3+LpRCjeQduJcpVhyV7uDnL9OVjZWTU7cVOvIxyao5Pp2InSvucBmN7H9tXPSa9jpcnOnFZr9qzYV2ni5HyK1fuApLd2GxXcTuBFsjt/jqeOwuLVz8jk6sEcbFci6SSPfG1Fe1ODWJr5l48uJKPpPaW1sVtbb9PB4tnRVpsRvWunXI9eL5HqnNz3cV/2FFO98e7OQ/Tp8PtyOT9Ome6lazEfBLE6po6pVd+LTlI9v+UUSrtrt7b3ajt9HNuG3DQvXNLOWszOTjM5PLCz8TvSb5Ua3XjqviBGNx/uT25eycOGwl9+Lx0zui7uWau+T0m+yCHTXqd4PenD8qgaOTbWC7hXHYLZGPkdiHTpJuDfWQR8086sXiyF8vFzna8kRPeiJzg+i6FKCjRr0a6K2vViZBCiqqqjI2o1uqrz4IUUP3l3fhtw7zr7Pv3kqbXwi/XbjmavmmexNW1o0Tir116ERPFVX8JKJZsrZ9zcGSp7l3Hjf0vF4npZtHbPBrK0TUTpsTMb/AKq8NEX5dNfYBtN3ZTIbpyMuyttzuijaqN3NmYl4VYHc68TuS2JU4afhTmUSZ+Jw+G2lNjK0LYMVTpyRpCnJIkjXq19qrxVV8QKi7Rb++i7eYvb2BrfrG6ZZLSxUWL0xQRrYevrWpeTGebX2ryT2kFibY7fOrZBu4Nz2/wBc3OqeSy9NK9Vq8fTqRLwYifm01X3cSiZAAAAAAAAAAAAAAAAPG9dq0KU923IkNWtG6WeV2ujWMTqc5dOPBEA1m1d3YPdWOdk8JMtnHpI6JlhWqxHOZ82jXaPTT+JqAbkABVu7eqLv5saVzEWOalkIWvdyRyRPevT/ABf4KBaQAAAAAAAAAAAAAAEe37vjEbK21ZzuTXqjh0bBXaqI+aV3yxt19vj7E4geHbbduT3btKpnshilxElxXOgrLJ6nVDr/AC5UVWsVEenLVPfy0Arz9x2/JoMbX2FglWbcG4ZIq8jI14simd0sYq+DpXcP8uvtAn/b/aWF2Bs2lg2zxM+mZ6t629yMSWw/jLKquVOGvBuvJqIngBze7r9tKLnNsbnxqPbwcyOzHK5F9itjVygVpu3u3t7d+Yq4OlBkshs5i+rk58fUmkdfljVFZSamjFSJV4yKvzfLyXUDX9299ZndmFx2y8PtTKYyTM2o44GZFkdH1o6/ndFG3rciJwTiqoiAWIy/3utRRx08Fg8GxrUan1lye4rUTgiI2vHEn/UB2/8AFe79x6uu72rY9i6aw47GRLw8dH2XyOQCFv2PkN1bxkwUe7M1lMHil03RZms+nXkmX5aMEcLY2dWnGVePSi+0DAXtnsZ/fPG7cxuLZ+k4jHvvZWGZZbDZJHJ0xRvWVz9Eb6jHInJQLxx+2duY1dcdiqdNfbXgiiX2fgagGxVWtbqujWtTivJERAKwvWJO6GYXF0nSM2DjJlTLXGqrEydiPlWhc3isDF4yO8eSe0DO3nvyXG5CpsfZlVljc9hkccbWsT6XHV10T1pkTgiMZxaz/cihCd29jtlY3bOTzW8dxzTZuyqOkztrijZFVF6IayO86uROlG9Sr7NAIFg2ZbuDk8b23xbpsPsXb8Pr5H1nIkz4Wv6pJZ/wpJI9/lZyZr46KBve7neyS1A3Y/b/AKnU09PHS34lc50znIkbK1Zy6qqLyV3N3w4rKLe7V9usT262ksEkrPrpk+qzWReqNasiN4p1LyiiTg3X3r4qINemeyvci3NS29M+hsqvJ6WRzjdWzXnNXz16a8FZH4Pl+xPEog6X9o2O5Vq5dfBjtidto0hx9VqaROyEjl4sjRF63dbXLw46tavtAiu6dl90+4e8Yt0R4N1rA2nJLiq1+ZkUSU0/ppIxsjXtR6eZUTiupKLJwHYmxkLVe5vyzVtQVG6Utv4uJK1GJV5q/obG6ReX+KqBgZTau5u0WUl3Bs2OTJ7OsO68xt9zlc6JV4erCvHTRPxae52vBUo3Vj9yWxZaMf6JBeyuasIja+HjrStl9RfwOerVZw8ehXAQHsNgdu5XJ7i3tu+WFL9C6v8A8e49GMgkcnqOnkR6oi+Z3Szq5Ki+PILDtdwcxvnIrgdgsljxXV6eV3crVbFCz8baiOTzyqnBF8OfLzAT7bG2sVtvDw4rGMVsEWrnyPXqllkdxfLK7ROp714qv+AFWd5e6vqTr2+2siXM9lnJSszNciMhWZUZ6SL4vXXR3g1PfylE37W9uKGxNuMx0T0sZCdfWyN3pRFklVE8reGvQzk1Pt8SwTEAAAAAAAAAAAAAAAAA4kjZIx0cjUfG9Fa9jk1RUXgqKigVFlOxd/EZCfMdts/Ntu9O5XWKT9Jacq66oisVrk0Tw6mu08APBu9P3BbZj03BtOruWszRPq8TKsUy/wATo0SXVfhG1AMqj+5XZjZFg3FjcrtudvB311R7ma+5YfUf97ELgj3dbuXsa67bO8Nu52reu7dvtlloMlRk8lWbRs6JFJ0v16W6cU8SC7cXn8JlakVzHXoLVadjZI5IpGuRWvTVq8F4faBnI5q8lRfgBz1N9qAcK5qc1RAOFliTm9qfagHjLkcfCiLNZijReSve1v8AeoGLZ3NturH6lnLU4I05vksRMT73OQDUW+6nbSo1zp904pOhFcrW3IXu0T2NY5zl+CIBQPcn91mWt2XY7Y7Ux9RF6Vy9ljXSv08WMej2Mb8UVfgB6dnP3L59161U3zM+9jGQufFkYaqumjkaqaNe2u1EVjm68Vbqi+IFo0f3BYDKxrJgdubhzMSOVvrU6KOj1Tn5lkbp9oGQvdTethquxnbjMSIvyfWSQU1X4o5ZNAMDPdxO7+Mw9jLW9qYzC0oGq981/JJNongzoha1Ve5eCIniBS6P7ld9t5x0rclWhUw8KWVhRr1qRI9yI3raqvc6SX2KvJPcBZu93d0cJHXxNbesmR3NfTpxW38VjasH8tOD5HyO9R0UTGovmVf8dAgezO1FjcPeG9i9wZWzkX4Su23nMhFM5r3ZCRGoxkcnzJ0Kqp1fwLyAvSj2L7V1JPVXAxW5l4OluvltOd8fWe9v9gEUu4fbe5snLtDYmGpU8LDJ6W59y1a8LEjanF1WrJ0+eZ3JXJqjALaw2GxmFxlfF4uuyrRqsRkMLE0RETxXxVV5qq8VXioFX7mamT/cZtWlMrXV8TjLF9kbnKn816vYitTxcnlXT2IBboFeb23dk8pmE2Js6VUzc+n6zlmIro8ZVcnmc5ycPXenCNvtAmG29uYvbmGrYjGR+nVrt01cur3vXi+SR34nvdxcoFbbAcj+/HcJ0z3LOyCi2Jn4fS6eOvvTRun2gW4qoiarwROagVhlc5kO4uWn21tuZ8G06rli3HuCPh66/ipU3rzVyf1HpyT7OoLDx+Mo4jFRY/FVmV6lSPoq1o06WoiJwT7V5qB89drO5f6NRzlVmPt5ruHmspNM/GMY5rU0a1rXSSqnS2Nq9Sr7PcnEC2cP26fbyEee3tYZnM4xWvqQdKto0VRerorQqqo5Udp/Mfq5dE5AV5mv287rvb4zmQpbgZjMBn5HOyDYutbEkUj/AFHRKzRG/N49f2eAEe33i8b227obRtfpNiTaGHgVacddqSSSWVbJq9yuVvVJ6zmOdqvLTQCc4nBb97m2UyO845MDszVH09tRPcye0iKitW25Ea/o4cuHuROZBbdDH0sfSho0YGVqddiRwQRIjWMa3kiIhRTW0P2206O5LeV3Je/VaX1T7NHFp1ei5yuXolsdWnU5E/Ciae1VTgBdjWta1GtRGtamjWpwRETwQDkABh1cNh6lh1mrRr17D9euaKJjHu156uaiKoEKzXYbtpmc+/N3sc51mV3qTwxyvjhkevNzmtVFTXx6VQCcY3GY/GUYaGOrR1KUDemGvC1GManuagEF735jeeP2k2HadaeXIX5kryWKzHPlhY5qrqxGorkc5eCO8ANB2P7KybWR24dxtZPuSwn8hir1/Ssci9Xm1VFlfr5lTknBPEC4gAAAAAAAAAAAAAAAAAAAAAPC5Qo3YvRu1orMX/bmY2Rv3ORUA0F3tj26uRLHY21jXNVNNW1YmO+xzGtVAKKzHZ3Yex9yzf8AleIW9snKyIlLMxvlbLj5nLokc6RuTWNfzaf4gWNW/bf2mWKOWnXtNie1HMkhuzdL2qmqO6mu46ovgAd+2ftW5qNfWuOROSLcmVE+HEDh/wC2btW9/XLXuSLyRH25V0T79QMhn7bOzqKiuwjn6fmtWePx0kQDKh/b32ehdq3bkS+501hyfcsgHvP2f7OYupPes7dx8VWux0s807Ve1rGJq5y9au8AKFjbie427Za20Nn1n4bHKradSKNtSs53Vp9TkrDEa5WcNWQsdqv3gXJsr9v20sNIt/NxRZrKSI7yPiaylAj+LmV6/FqIng5ePwArn9x+Qw2x9u1Nk7Rrx4puYc63lGVkVHSR69LGveqq5Uc5F5r4ewB2OhynbLaGY3huyxLQwlyJiYzDTL0SWbGnV6rYl+VzkRGtXxbxXgiATDs/vLcV7D57uTvfKupbftPWPF0JHaV4ooXKjnsZpqqq7+W3Ti5UXXXgB2t1M9veld3xm6ssGAxME1vau3XJ5p5YWK6O3bYvzdXT5Gez3fMFcdm+5NnGbesYLauIly3cHPXpZrk8selaBmqMjkle3isbE1dpwRFVePgoXv267cv286zms5b/AFfeOU82Tyjk1Rqa6pBX1RFZE3hw8dPDgiBW3abdm29ubv7l3N0ZOvjb0mSRzm2JEa98Ub5tPTYvmfor/wAKKoG7XdO6u7Nl2M2xHawOxvM3Ibke307NpERU9Gq1eSOXm72c9PlULR2xtrEbZwVTCYmL0aNNnTGirq5yqurnvd+J73KrnL7QNoBTvdKe5szuRgu4v08lrCfTuxWbSFvW+KN6q5kiJ/mVF/4dPEBme7l7e1pu1+1jnTW7DVW/uKaKSKtSh04q3rajvUXknl58vagTjt32+xuy8O+rBK+5krj/AF8tlJuM1mdU4ucq66NTVeluvD4qqqEqAqDeeI3XtDuV/wD0Db2LlzeOyVZKmdxtbjOiMRvRJG3xXyNXl7U4a6gcadzO57kr3ak+ydna6Wonqv6lcRNNY+KM9Njk8dP+bwC0sHhMXg8VWxOLrtrUajEjhib4InNVXmrnLxcq8VXiBnAeUdOpFPJPHBGyeX+rK1rUe7T8zkTVQPUABwrWrpqiLpxTXwUDkAAAAAHEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8btKpeqS1LkLLFWdqsmhkRHNc1eaKigQq5Vyvb/EQR7XxMuZwMMks12itlzrNeJUTRtNj0VHMbxXoV3wAkW1N3YLdOKbksPOskPUsc0UjVjmhkb80csbvM1yf701QDcgAMbJZLH4yjNfyFiOrSrtV89iVyNY1qeKqoHzt3s3puzdzMLt3F1JsRtncd6GpUvWWujmu9UjW+p6a6OZC1Xo5EcmruCgX7tjbGE2zha2Gw1ZlWlWYjWtaiIrlRNFe9U+Z7ubnLzAwt5792/tKmyXJSOkt2F6KONrt9W1ZkVdEbFEnFePjyA+Ye693cEPdvB7i3lt5LEckccsG3Yl9ZHRtVzYoZH6Oa+Tr4vREVPADB76V+49rH4fcO83Phkyj5HUsHGjmxUoWdOjHJy9V6O468eHH3Bavb7aGZ7i1sTlt0UP0XZWGSJNu7Uj6vTm9JqIk9hXeZ7NU8vUnm+C+YL50TTTThy0AwcbgsJi1mXGY+tRWw7qnWvEyJXu9ruhE1+0DOAieb7U9vM7mUzWWwde3kuHVM/r0d08utiORj/8AiRQJTBBBXhZBBG2KGJqMjiYiNa1qJojWtTgiIB3AAdZYo5Y3RysSSN6K17HIitVF5oqKB5UqFGjD6FKtFVh119KFjY26r49LURAPcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEt1bBTIzfquAuvwG42OR7cjWanTNomiMtRfLMz/NyAxtt9wbD82u190UX4rcDE0hsaL9De05uqyr4rz9N3H2a6KBut2bww+2aTJ7zny2bDvSoY+u31LNmVeUcMacXLx4ryTxAjuM2dmNzXIM7vxGKyJyTYva8a9VWqvNr7K8rE6J7fI3joBj969g5rdOHx13b8rYtwYC029jUcuiPc1Wu6dV4a9TGqmoGog3t3yz1aPF0NnM29klb03s3kJUdVjdyV8ESIrn682/Np468wJbsvtri9vWZMtcsS5rdFlvTcztxeqZyKuvRE3VWxRp4NaBK5alSWaKeWGOSaDX0ZXNRXM159LlTVNfcB1t0KNxrG3K0VlsbkfG2ZjXo1ycnJ1Iuip7QPcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACCd1MjirWKftRtP9W3Dl4nJjMaz5o15JakfyhjidxVyrx5JqBEdo/U7M3/AFMbvrqymYy0TYMJuqR7nxM8qItFjX/0/Nr5/meq+bmBdIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAInuneFqLIJtvbMTL+6ZmI9zH6/T0oXf8A5FtzflT8rPmf4AZe0dl0dvRzWHyvyGdvdLsrmZ+M1h6fFVSONvJkbeCJ94GVuvbVDceDtYq5G1yTMd6MrkRXRS6eSRi+Dmrx4ARftDvHK5nHZLCbgVv/AJNtqytHIub/AKrOPo2NPDrRqovtVuviBPwAAAAAAcMVzmIrm9Ll5t56fcByAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQrdm8MpLl02jtFrJ9xyNR967I1XVsbXd/rTacHSL/AKcfjzXgBudpbRxe2aD69Tqnt2XetkcjMvVYtTr80sr+arqvBOSeAG8AAVFRauK/cteghf0wZ3CJYniRODpoXsa1fijWO+8C3QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6M9ZJXIqN9HROhdVV2vHXXX7AO4AAAAAAAAABDN47rybsjHtLavRJuW0xH2bTk64cdWdwWxMni//ALcf4l93MNztPaWK2xjVpUUfJLM9Z712Z3XPZnd880r15ud9yckA3QAABUuORMt+5PJ2WJ1wbfwjKz5G66JPYka5Gqvt6HP4AW0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEauVjVenS5UTqbrrovs1A5AAAAAAAAARXe27bGN9DCYRjbW68qitx1ZU1ZE3k63Y0+WGLmvtXggGVsvZtLbOPkjbK+5lLr/Xy2Vm4zWrC83uXwamujGpwan2gSAAAA0+7ty0ds7byGcuuRIaMLpOleHW/kxie9ztEAhnYbC34dp2Ny5ZumZ3ZZflbKr8yRSf8A12cfDo8yJ/EBZYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEf3vu+ptbCOvSRrZuzPbXxmPj/q2bUi9McTETVeK818EAwtg7SuYqGxmc7IlrdeYVJcpZRdWxN5x1YeK9MUKLpw5rx9mgS0AAAAUpuSZ/djuAzalKTXZW2pm2Nw2G69Nuy1V6KzFTmiKmjvdqvsAupjWsajGIjWtREa1OCIieAHIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPK3arVKs1qzI2GtXY6WaVy6NaxidTnKvsREAr/Y9WXeGbd3BysTm1GdcG0qUicIqvJ9xzf+7Ouunsb8QLFAAAAFX90975exkIO32y3JLunLJpctN4sx9R3CSaVU+V3SvDx9nFUAl+x9j4bZ+2oMDjGu9KPz2bCrpLPO7T1Jnqn4nKn2JwAkIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFa9wp7G6d0Y7t9S61x69N3dc0TunoqN4xV1dqmnrOTinPQCxq9eCtBHXgYkUELUZFG1NGta1NERE9iIB6AAGqa6eIFfdx+4l3H24dpbShTI73ybdK8CaelTiVPNZsu5NRqcURef3ahsO2nbmnszFzJJOuRz2Rf6+ay8mqyTzLx0TXVUY3VelPt5qBMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABNdOPAAAAAAMbJ5CtjcdayFp3RWqRPnmd7GRtVzv7EAgfZCKe9tq3u247rv7puTXpHK1GuZCxywwRa6rq1rY9W/wCYCxQAACsN5b/yWRzq7W7fMdkdyMY6O9kUev6fjmSKiK+deLHypp5W6KqfegEj7fdu8bs+jOqTPyOavv8AWy2ZseaexKvHiq6q1ifhbr/aBLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAED76Mtv7U7gSrr6qQscvTz6GzMV/2dOuoGp/bRk6t3s9hoYZEdNRWxWtR66uZIk73o13s1Y9qoBaQGk3TvXa+1aLrmeyMNKNEVWRvdrLJ7o4k1e9fggFevu9xe57mx49ljZ2x5E/nXZU6cndjX8MTf9Fjm/i/9XICxtrbTwG1sRHicJUbVqR8VRNXPe9fmfI9dXPcvtUDbgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHhepVb9KelbjSarZjdFPE7k5j00ci/YB89ZL9ue/tt5qzk+2u5XY+G07V9V8jo16dVVGvTR0cnTrwVwEwxW0f3CXaTamd3nSoRaI2SejUZJbc3Tj51bExjve0Df7V7LbNwWQXLWknz2dcqOXK5eT6qZHJ4sRydLfjpr7wJ6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB0YxySPcr1cjtOli6aN0Tw0TXj7wO4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//Z",
+ "actualDateTime": "60a68c99-c899-4d3e-a83b-63245406435f:string:2020-05-29T09:46:34Z",
+ "statement": "e7efc705-2b39-4f48-90cc-87859e2c6e0c:string:The undersigned hereby declares that the above-stated information is correct and that the goods exported to [importer] comply with the origin requirements specified in the China-Australia Free Trade Agreement."
+ },
+ "issueLocation": {
+ "iD": "b0e019da-f63c-41f2-8dea-742f7fc16075:string:unece.un.org:locode:AUADL",
+ "name": "52c39e08-bac5-4cdf-9cd7-5eab815e46a2:string:Adelaide"
+ },
+ "issuer": {
+ "iD": "bffd6a1a-a532-4045-b6c0-cc1b0e180a74:string:id:wfa.org.au",
+ "name": "f8618138-61d7-4cdb-b2cd-c6f31237051e:string:Australian Grape and Wine Incorporated",
+ "postalAddress": {
+ "line1": "e8c47018-dba7-4da1-ac18-5a450354c323:string:Level 1, Industry Offcies",
+ "line2": "a1f12974-bc7f-40dd-afef-4b0cb01ca4db:string:Botanic Road",
+ "cityName": "ac291751-3d55-4fae-94b0-2cb78e08fbae:string:Adelaide",
+ "postcode": "0f259d18-9f53-4e15-89fe-f9799b32f886:string:5000",
+ "countrySubDivisionName": "9514b11b-ed05-4ef6-8ee3-05b88f30137f:string:SA",
+ "countryCode": "6451c62b-6df0-48d3-b529-957e91f9d710:string:AU"
+ }
+ },
+ "status": "61b95c57-6552-4f1c-aa3c-2d212e1a87f2:string:issued",
+ "isPreferential": "cba0b32d-a2b4-4e74-9b3d-fd66fe6e59aa:boolean:true",
+ "freeTradeAgreement": "7caf8294-3602-40aa-96b5-d6eac8c38900:string:CHAFTA",
+ "iD": "b46da2a8-06d0-4389-919b-7a663f0205f7:string:111",
+ "links": {
+ "self": {
+ "href": "a9d40b34-fef3-4d95-ba49-c2bd0256ea37:string:https://actions.tradetrust.io?q=%7B%22type%22%3A%22DOCUMENT%22%2C%22payload%22%3A%7B%22uri%22%3A%22https%3A%2F%2Fgallery.openattestation.com%2Fstatic%2Fdocuments%2Ftradetrust%2Fv2%2Fchafta-coo-stability.json%22%2C%22permittedActions%22%3A%5B%22VIEW%22%5D%2C%22redirect%22%3A%22https%3A%2F%2Fref.tradetrust.io%22%2C%22chainId%22%3A%22101010%22%7D%7D%0A"
+ }
+ },
+ "network": {
+ "chain": "193d78e7-2cb8-4e00-b86f-45e09654aa54:string:FREE",
+ "chainId": "60296159-6d5e-4016-b04e-e6bcec325d36:string:101010"
+ }
+ },
+ "signature": {
+ "type": "SHA3MerkleProof",
+ "targetHash": "73e4e9ae2269686ea0a0bed873f042fb690c52370565ee7db3c6e822773917fb",
+ "proof": [],
+ "merkleRoot": "73e4e9ae2269686ea0a0bed873f042fb690c52370565ee7db3c6e822773917fb"
+ }
+}
diff --git a/src/__tests__/__fixtures__/oa/2.0/signed_wrapped_oa_dns_txt_token_registry_v2.json b/src/__tests__/__fixtures__/oa/2.0/signed_wrapped_oa_dns_txt_token_registry_v2.json
new file mode 100644
index 0000000..509e97c
--- /dev/null
+++ b/src/__tests__/__fixtures__/oa/2.0/signed_wrapped_oa_dns_txt_token_registry_v2.json
@@ -0,0 +1,56 @@
+{
+ "version": "https://schema.openattestation.com/2.0/schema.json",
+ "data": {
+ "name": "6c6ff1b0-1f76-44f6-acb4-1f4480adb40d:string:TradeTrust Bill of Lading v2",
+ "shipper": {
+ "address": {
+ "street": "c9f4a183-0607-409b-ab2b-ec4247d45a74:string:One North",
+ "country": "636102b0-b894-4315-a7e8-b51415348a94:string:Singapore"
+ },
+ "name": "a9f78f2e-c6a8-4e5b-bae3-5482bd21b456:string:Demo Shipper"
+ },
+ "consignee": {
+ "name": "2cede8d9-9c02-483b-b504-bf8618a99bc4:string:Demo Consignee"
+ },
+ "notifyParty": {
+ "name": "0304dc0c-96d4-4d7a-b981-148132e290fb:string:Demo Notify"
+ },
+ "$template": {
+ "type": "ae7232e7-fd0c-4f9a-8f42-52980d469656:string:EMBEDDED_RENDERER",
+ "name": "c25a962c-557b-42cf-9ca1-d44ec85cca25:string:BILL_OF_LADING",
+ "url": "f8aec37c-331b-4025-a9b8-11ef2f60a8e5:string:https://generic-templates.tradetrust.io"
+ },
+ "issuers": [
+ {
+ "identityProof": {
+ "type": "a8b5bfdf-0e2c-4790-9148-232f50b04d1e:string:DNS-TXT",
+ "location": "fbd510ad-698c-4c5e-9864-ef6738b80201:string:example.tradetrust.io"
+ },
+ "name": "06940f1d-9c4b-42e9-8d75-92b504a74487:string:Demo token registry",
+ "tokenRegistry": "01a0a2c3-b521-41c0-90e8-522e593f3913:string:0x71D28767662cB233F887aD2Bb65d048d760bA694"
+ }
+ ],
+ "blNumber": "10a9e9c4-fb9c-402c-b51f-2391a71f64db:string:123",
+ "vessel": "98545c1c-f5fb-4fcb-90b7-e6590cf6ccf3:string:1",
+ "voyageNo": "a6821d2a-6ebf-4cd2-9708-78b41bfebe75:string:100",
+ "portOfLoading": "8d8b6c03-53dd-4409-8bde-4cacc1fb614c:string:Singapore Port",
+ "portOfDischarge": "ad8027a1-f4db-46ad-9f59-513f1f17a95f:string:China Port",
+ "placeOfReceipt": "a7222399-b500-4bf9-9ab8-51cb6f89a422:string:Beijing",
+ "placeOfDelivery": "46a36db7-9485-4c0c-8c23-42b3a3e627eb:string:Singapore",
+ "links": {
+ "self": {
+ "href": "4aab0503-088d-49ca-a2e6-917c910c9ca3:string:https://actions.tradetrust.io?q=%7B%22type%22%3A%22DOCUMENT%22%2C%22payload%22%3A%7B%22uri%22%3A%22https%3A%2F%2Fgallery.openattestation.com%2Fstatic%2Fdocuments%2Ftradetrust%2Fv2%2Febl-stability.json%22%2C%22permittedActions%22%3A%5B%22VIEW%22%5D%2C%22redirect%22%3A%22https%3A%2F%2Fref.tradetrust.io%22%2C%22chainId%22%3A%20%22101010%22%7D%7D"
+ }
+ },
+ "network": {
+ "chain": "6b1ed020-7e1f-4969-9d9d-3360aaed3e9c:string:FREE",
+ "chainId": "30e1af7c-40f0-4921-8702-bae3154977f8:string:101010"
+ }
+ },
+ "signature": {
+ "type": "SHA3MerkleProof",
+ "targetHash": "45c4f4dde4e8da7b0b3eb2ac99fc05a1226b513773a4ef1e0c6389bf30de7a3f",
+ "proof": [],
+ "merkleRoot": "45c4f4dde4e8da7b0b3eb2ac99fc05a1226b513773a4ef1e0c6389bf30de7a3f"
+ }
+}
diff --git a/src/__tests__/__fixtures__/oa/3.0/oa_dns_txt_token_registry_no_network_field_stability_v3.json b/src/__tests__/__fixtures__/oa/3.0/oa_dns_txt_token_registry_no_network_field_stability_v3.json
new file mode 100644
index 0000000..052af19
--- /dev/null
+++ b/src/__tests__/__fixtures__/oa/3.0/oa_dns_txt_token_registry_no_network_field_stability_v3.json
@@ -0,0 +1,79 @@
+{
+ "version": "https://schema.openattestation.com/3.0/schema.json",
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "https://schemata.openattestation.com/com/openattestation/1.0/OpenAttestation.v3.json",
+ "https://schemata.tradetrust.io/io/tradetrust/bill-of-lading/1.0/bill-of-lading-context.json"
+ ],
+ "type": ["VerifiableCredential", "OpenAttestationCredential"],
+ "issuer": {
+ "id": "https://example.com",
+ "name": "Demo token registry",
+ "type": "OpenAttestationIssuer"
+ },
+ "issuanceDate": "2010-01-01T19:23:24Z",
+ "openAttestationMetadata": {
+ "template": {
+ "type": "EMBEDDED_RENDERER",
+ "name": "BILL_OF_LADING",
+ "url": "https://generic-templates.tradetrust.io"
+ },
+ "proof": {
+ "type": "OpenAttestationProofMethod",
+ "method": "TOKEN_REGISTRY",
+ "value": "0x71D28767662cB233F887aD2Bb65d048d760bA694"
+ },
+ "identityProof": {
+ "type": "DNS-TXT",
+ "identifier": "example.tradetrust.io"
+ }
+ },
+ "credentialSubject": {
+ "name": "TradeTrust Bill of Lading v3",
+ "blNumber": "123",
+ "scac": "DEMO",
+ "carrierName": "Demo Carrier",
+ "shipper": {
+ "name": "Demo Shipper",
+ "address": {
+ "street": "One North",
+ "country": "Singapore"
+ }
+ },
+ "consignee": {
+ "name": "Demo Consignee"
+ },
+ "notifyParty": {
+ "name": "Demo Notify"
+ },
+ "vessel": "1",
+ "voyageNo": "100",
+ "portOfLoading": "Singapore Port",
+ "portOfDischarge": "China Port",
+ "placeOfReceipt": "Beijing",
+ "placeOfDelivery": "Singapore",
+ "packages": [
+ {
+ "description": "Green Apples",
+ "weight": "20",
+ "measurement": "100"
+ }
+ ],
+ "links": {
+ "self": {
+ "href": "https://actions.tradetrust.io?q=%7B%22type%22%3A%22DOCUMENT%22%2C%22payload%22%3A%7B%22uri%22%3A%22https%3A%2F%2Fgallery.openattestation.com%2Fstatic%2Fdocuments%2Ftradetrust%2Fv3%2Febl-stability.json%22%2C%22permittedActions%22%3A%5B%22VIEW%22%5D%2C%22redirect%22%3A%22https%3A%2F%2Fref.tradetrust.io%22%2C%20%22chainId%22%3A%20%22101010%22%7D%7D"
+ }
+ }
+ },
+ "proof": {
+ "type": "OpenAttestationMerkleProofSignature2018",
+ "proofPurpose": "assertionMethod",
+ "targetHash": "97cf312985330060a303713aa19af47b31d30f57405010c8a667665fd447f7e6",
+ "proofs": [],
+ "merkleRoot": "97cf312985330060a303713aa19af47b31d30f57405010c8a667665fd447f7e6",
+ "salts": "W3sidmFsdWUiOiI0NWQxZTQzMWY3OGQ0YWY2NGE2NzI5OTE5ZmRkMmUwNDMxODZhYWMzNzZjODI5ODNjODQ1ZmQyZjM1ZjdmOGIyIiwicGF0aCI6InZlcnNpb24ifSx7InZhbHVlIjoiY2Y4ZGJhNWE1MWUwZGM2YmEyMjU4MzFkN2JiNGM0NzU4YWY4MzM5NmEzOTMwZWI0MjJkY2U1YWI0ZWFiMjQ4YSIsInBhdGgiOiJAY29udGV4dFswXSJ9LHsidmFsdWUiOiJmYTQxOGRhMDcyMjRhNjhhOGM5OTE4ZTcyMGIwYjY2MTIwYWZmZWRjOTQyMjc1NTFhMjNjYzJjODA4NTYyZjRlIiwicGF0aCI6IkBjb250ZXh0WzFdIn0seyJ2YWx1ZSI6IjNlNWQ4ZGU2YzFmOTU3YjllNDY0NGFiZDhlNzZkM2M5ZjJjY2I2NzJjZGNhNTYxYjY2MTM5MzM4YTg1OWNiN2IiLCJwYXRoIjoiQGNvbnRleHRbMl0ifSx7InZhbHVlIjoiNTc1MmE0ZDMxYjFjZGM4NWNiYWZhODUyN2MzOTQwZTM2NjY3ZjIyMTdhZDFiZWY4ZDZmMmZjYzMwYTkxY2JkNiIsInBhdGgiOiJ0eXBlWzBdIn0seyJ2YWx1ZSI6IjhkZTU4MDEwYjU1MDcxOTk1OWMyMjNkODFkYTgyYjIwZjVlOGE3NDQwNmQ3MDZiOWFmMjJjOTA0YTRjMTU5MTQiLCJwYXRoIjoidHlwZVsxXSJ9LHsidmFsdWUiOiJhZTM0NjMyMzM0ZjkwOTY0MjgxYzk1OTdjZTM3ZTFlZWY3MDA5OWMyNDY4OTM1MjVlY2QxMWNkMzIxNGJjMjllIiwicGF0aCI6Imlzc3Vlci5pZCJ9LHsidmFsdWUiOiJhYWE1M2Q5MWQ5ODQxOTI2NzFkOGViYjA2N2JlY2EyOTRiMjA5ZWZiN2I1MjY2ZjZiOGUzYWU2Y2Q3ZTE5YzEwIiwicGF0aCI6Imlzc3Vlci5uYW1lIn0seyJ2YWx1ZSI6ImUxZDkxYWZhZDU3MmVhMzA2YWVmZTU1NzMzNzM5M2VjOTYwZjUxZjIzN2E5Mjc2YmZjYTg4YzE0NTFiNDM2NzgiLCJwYXRoIjoiaXNzdWVyLnR5cGUifSx7InZhbHVlIjoiOTRkYjNjMDNiOTcxY2QwYzAwYTkxZjAzZGY0M2ZkYzBiZTg0NjRhMTY2Y2U5NGE4M2YyZWIyMWQ3YWE0NTRhNCIsInBhdGgiOiJpc3N1YW5jZURhdGUifSx7InZhbHVlIjoiMDk5NDZhYWJlMWZiODBmYzE3MGFmNjkzNzY1NGExNTc4ZGFiN2Y3ZDhjNWQzYTVhMDU2NDU5NDc1NjhiNmQ3YSIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS50ZW1wbGF0ZS50eXBlIn0seyJ2YWx1ZSI6ImQ1YTk4YTQwYWNmOGQxZDNmZjY1Y2M1YjNjYmEzNmJjZWJmZjJlNzQ0YTRmYjU2ODhhNjRmMzI4MThhMDQ3YmYiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEudGVtcGxhdGUubmFtZSJ9LHsidmFsdWUiOiIxZTMxNmQzYTQ3OTRlNTljMzJhZTcxNTZhZDQyYjJjMDNiODJiZWViNjIyMDUxMDI4YjlkNDdiZTI1Yjc2MDZmIiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLnRlbXBsYXRlLnVybCJ9LHsidmFsdWUiOiIxYTdlZmY2ODM5NjA1ZDgyNGMwY2FjZjg2OWQ3MmUwZTlmYjY5NzBkOGQxZDVhZmQxNWNhOWU2NmUxZWYwNWJjIiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLnByb29mLnR5cGUifSx7InZhbHVlIjoiYjllYWVhYTQzN2RiOGY5ZTY2YzAwN2FmZWIyNjlhZDM3Njk3MjM2NTI5NWZjYTVlZWI4N2ExMjhhMzlhNTg4OSIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi5tZXRob2QifSx7InZhbHVlIjoiZDBlNDFiMzM1ZGY3ZGFiOTA2YTk2MDE4OWY1NjkxZTc2MzJjODBkNjI4N2Q0ODMxNjFmODYwMzQ1ZWZhMWIzZSIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi52YWx1ZSJ9LHsidmFsdWUiOiI2OGM2OTYwMzU2YTk0ZDdlNDc3NDcyNjY1NjE0Nzc3MjBkZTMyZDE2YTgwMTBhNzA2ZmFlMTYyMTdmOGE1NmZiIiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLmlkZW50aXR5UHJvb2YudHlwZSJ9LHsidmFsdWUiOiIzMGU3M2ZiOWNkYTI2YjU4M2MwZTY2NDA1OWM5ZDU3ZjU0NjRmMjAyYWM1YjFmY2EzNDNkYTM5MTk2Y2MyY2UwIiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLmlkZW50aXR5UHJvb2YuaWRlbnRpZmllciJ9LHsidmFsdWUiOiJlZmZlNDZjOGUwMGQ3ZDgyNzJhYmNmMjNiYWIzMmU3NTBkYzg1MjVmMmE4ZDNmNmE0YTVlYjMzNTU0YmJhMWIyIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0Lm5hbWUifSx7InZhbHVlIjoiMmIwZTc0ZmY5N2RiMDkwYzFmMDk2ZmM2YjIzMGE3MTczOGQ1ZGYyODZjODMyYjE4NWY5OTFlMTNkNDhjNDIyZCIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5ibE51bWJlciJ9LHsidmFsdWUiOiIzNzNlNmNjYjMwNTkzOTdhOWE0ZjM5YmMyM2ZjYmVhNGU3NzI5NTg5ZmU0M2JjNzU4MmMxYzdlY2E2MzRlOTlmIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LnNjYWMifSx7InZhbHVlIjoiMzk5MDkxMGU2ZDgxOTRiN2Y3YmEyMzUzZWFhMDdkODNmOTBmODM5NWEyZTZkOTY0ZTU2ZDg3YmRlNzk4ZTJiNyIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jYXJyaWVyTmFtZSJ9LHsidmFsdWUiOiI4MmNlN2U1Nzk2ODllN2MxMzkxZGYzNzhiMjc3ZDNlOGE0N2RkMDljMDQyOGZkMTQ5ZjdjZWI4MzlkN2MwZWE2IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LnNoaXBwZXIubmFtZSJ9LHsidmFsdWUiOiJmYjg2ODAwODhlNmIyMjBhOGNjODJlNTVlMGUzNzVjMzEzMjhlMWMxZjMyYjMxOTliN2U5YzdkZGE3ZGNhZDI4IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LnNoaXBwZXIuYWRkcmVzcy5zdHJlZXQifSx7InZhbHVlIjoiYTU4NTY4OGVlNzIxNWU5ZTRkZjY5OWFkY2ZjMzZkOWI4ZDIwOTJlNjU0OThhZmU2ZjQ2YWI4MjM3ODYxYjY5MCIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5zaGlwcGVyLmFkZHJlc3MuY291bnRyeSJ9LHsidmFsdWUiOiJlOWZjMzk1NGI0ZTNkZDUzZDQ1ZjQzMmI5MTc5ZWM0N2IyNTY0OWNlMDFkMDNmNTU5MmZiYzkyZTgzNTQ2ZGE4IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmNvbnNpZ25lZS5uYW1lIn0seyJ2YWx1ZSI6IjljZTA0Nzk2N2ExYzVhNmFjYTM5YmJiNGVjYjIyMTQ1Yzg3ZGI1ODVjNWRiY2E3ZjgzY2M4ZjAwZTZiYjJiZmUiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3Qubm90aWZ5UGFydHkubmFtZSJ9LHsidmFsdWUiOiIzZmU1OGJkNzVlNDEwMGNkNmI5YzAxMDA2ZTFlYTU5NTM4N2ViMzU0ZWFmZjgwNWVkMTQ2NjkxNzBmMTg2NjM1IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LnZlc3NlbCJ9LHsidmFsdWUiOiJmNzZhMjE3MDg5ZGIzODA0MGMyM2U1MTNiYThhMTZjMjZhNDJiYzQyZDM5Y2M0YjQyM2YxYzQzMzk3YjNhOWVkIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LnZveWFnZU5vIn0seyJ2YWx1ZSI6IjY4OWU5NzA3MTJkM2E3ZTczOTFkYWY1MTlmYzEzNzA0NGI4ODk0ZDA1NmIzZjIyMzkzMDg5MzI5MmY2YWZlYWUiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QucG9ydE9mTG9hZGluZyJ9LHsidmFsdWUiOiI3MmE5MzYyYTVlOWM3MDc1YWYyMmYzMDY0OGJlZGI5ZDIyOGE2MTA1MGNiODdjMWZjNDY5YzUyZDFmZDYzNTJjIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LnBvcnRPZkRpc2NoYXJnZSJ9LHsidmFsdWUiOiJlMmVmNWIxZDA4MTQyNmI2OWU0ZjJmYWRlZWU4NjU0NzZjZDhmN2U1NGRlNDA0ZTczNDk0MjU5OTBkOTI5NjE1IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LnBsYWNlT2ZSZWNlaXB0In0seyJ2YWx1ZSI6Ijk5MGUzNGRiYjIzYzBlZWUyNTMwNDQwNTdkNDQ5NDZhNzY5Y2M2OTYwNWM3MDU4MTFjODdlMWYyYjA1YmY2ZjIiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QucGxhY2VPZkRlbGl2ZXJ5In0seyJ2YWx1ZSI6IjdjODIxMjExZTVmN2FjYmM0MTdmZTIwOWY2MGFkODRlMWE5YzM3NzdjNGJkMTM4MDYyZTA2YjlmZmU0ODg2NmYiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QucGFja2FnZXNbMF0uZGVzY3JpcHRpb24ifSx7InZhbHVlIjoiNDQ3YWQ5YzQzZjMzMTcwNWRkMmY4ZmViYzBkNzBiYWVkZTI1YjViOWE5MWU3MDdjMjZhNjBlYTQxM2ZlZDBhYyIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5wYWNrYWdlc1swXS53ZWlnaHQifSx7InZhbHVlIjoiNmZkZmM1NWMwMTdlOWE3ZDU0NGRkOTk4MjEzNjIwZGMxYzhmOTIwOTBmYjFjODgxYWU3MDIwYzc2YjU2ZjI1MiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5wYWNrYWdlc1swXS5tZWFzdXJlbWVudCJ9LHsidmFsdWUiOiI4NmI5ZjA2YzE5OTQzNGUwZjc2YTBhZjMwMTc5MzU5NTg1MmVjNGU3OTBlZmFlYzk2OTIyMTY5NDdhMTRjYzQ2IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmxpbmtzLnNlbGYuaHJlZiJ9XQ==",
+ "privacy": {
+ "obfuscated": []
+ }
+ }
+}
diff --git a/src/__tests__/__fixtures__/oa/3.0/signed_wrapped_oa_dns_did_v3.json b/src/__tests__/__fixtures__/oa/3.0/signed_wrapped_oa_dns_did_v3.json
new file mode 100644
index 0000000..9095c77
--- /dev/null
+++ b/src/__tests__/__fixtures__/oa/3.0/signed_wrapped_oa_dns_did_v3.json
@@ -0,0 +1,72 @@
+{
+ "version": "https://schema.openattestation.com/3.0/schema.json",
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "https://schemata.openattestation.com/com/openattestation/1.0/OpenAttestation.v3.json",
+ "https://schemata.openattestation.com/io/tradetrust/bill-of-lading/1.0/bill-of-lading-context.json"
+ ],
+ "credentialSubject": {
+ "id": "urn:uuid:a013fb9d-bb03-4056-b696-05575eceaf42",
+ "shipper": {
+ "address": {
+ "street": "456 Orchard Road",
+ "country": "SG"
+ }
+ },
+ "consignee": {
+ "name": "TradeTrust"
+ },
+ "notifyParty": {
+ "name": "TrustVC"
+ },
+ "packages": [
+ {
+ "description": "1 Pallet",
+ "weight": "1",
+ "measurement": "KG"
+ }
+ ],
+ "blNumber": "20240315",
+ "scac": "20240315"
+ },
+ "openAttestationMetadata": {
+ "template": {
+ "type": "EMBEDDED_RENDERER",
+ "name": "BILL_OF_LADING",
+ "url": "https://generic-templates.tradetrust.io"
+ },
+ "proof": {
+ "type": "OpenAttestationProofMethod",
+ "method": "DID",
+ "value": "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90#controller",
+ "revocation": {
+ "type": "NONE"
+ }
+ },
+ "identityProof": {
+ "type": "DNS-DID",
+ "identifier": "example.tradetrust.io"
+ }
+ },
+ "issuanceDate": "2021-12-03T12:19:52Z",
+ "expirationDate": "2029-12-03T12:19:52Z",
+ "issuer": {
+ "id": "https://example.tradetrust.io",
+ "name": "DEMO TOKEN REGISTRY",
+ "type": "OpenAttestationIssuer"
+ },
+ "type": ["VerifiableCredential", "OpenAttestationCredential"],
+ "proof": {
+ "type": "OpenAttestationMerkleProofSignature2018",
+ "proofPurpose": "assertionMethod",
+ "targetHash": "8f832ec1d27e09b2530cd051c9acea960971c238a3627369f33cdc58af9548cd",
+ "proofs": [],
+ "merkleRoot": "8f832ec1d27e09b2530cd051c9acea960971c238a3627369f33cdc58af9548cd",
+ "salts": "W3sidmFsdWUiOiI2MmZjMzg5NWVmZjg1ODI5Mjc1YmY5MzQxMzI4N2QwY2NjNDliYTcyY2VhOWM1NTA2NjFjYzk4YTA1YTczNjU0IiwicGF0aCI6InZlcnNpb24ifSx7InZhbHVlIjoiYzI1NWZhZmFkNWQ2YmFlODE3YWJmNDExOGVmZDMwODRiNDMwOTIyZjE4MDU2OGE2NmY4ZDFjZWUxMTFjZDA3NyIsInBhdGgiOiJAY29udGV4dFswXSJ9LHsidmFsdWUiOiIwZWZkZDkxOGFjOGZmYWU1ODQ0ZGE4M2U3YTYyNWJhMGYyOGUyYjJlMTVlMWFlNjYzODFmZDAyYmEwZmYwOWQxIiwicGF0aCI6IkBjb250ZXh0WzFdIn0seyJ2YWx1ZSI6ImE4YjY2ZDEzNmRlYzYxOGM3ODI1ZmVjOTg3ZTM2NWUzYzlmZjMwNzg3NmI0MDc2NWUwZGI2MjdmZjA1NTAxNGIiLCJwYXRoIjoiQGNvbnRleHRbMl0ifSx7InZhbHVlIjoiMGQyMDkyMDU2MjBmZjg1NGU5MjZhNDI1YTZmYTk3ZDdkZWM0YjNjODE4N2YzNmM5YTZjZGY0OGYxMjMzNzgwNyIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5pZCJ9LHsidmFsdWUiOiI3MTdmNDg1YjFiMGNjMTFjZjExODNkMzkzYWE1MDc5ZDljNzYzZjY0NmMxNzg1MmJjZTY1OTNmOGJjZGRmM2IyIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LnNoaXBwZXIuYWRkcmVzcy5zdHJlZXQifSx7InZhbHVlIjoiNzUzM2M0ZDQxZmQ5Yjk2NjlkZmUyOWMxMmUyYTc1MDA1MzEyYjdjNmY0OWEzZDI2Yjk3Yjk3MTY3ODMxYmM4YyIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5zaGlwcGVyLmFkZHJlc3MuY291bnRyeSJ9LHsidmFsdWUiOiJkNDc2NTM1NzNlZTAxNzg5ODljZWU1ZmU2NjBiZjA4MzZmZDQzZTU1MmQ0M2JkMTM0MTg2ZGY3MTBmNWFkZTBhIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmNvbnNpZ25lZS5uYW1lIn0seyJ2YWx1ZSI6ImFjZGIyY2U5Y2YyMzlmZWYyMjE1MTNiZDRiZTAxNTk0OTc4ZmRlYjQ4ZjQ0NTk1NTkwOGZkYzc1ZTQxYmEzZWEiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3Qubm90aWZ5UGFydHkubmFtZSJ9LHsidmFsdWUiOiJkYTkxODQzNzIxZjU2MDljOGM3ZTE1MjgzNzBmZDdkMTA0ZGFmZGI3OWEzZDViMjMxZDI0MTM3NTZmMmRjNzZkIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LnBhY2thZ2VzWzBdLmRlc2NyaXB0aW9uIn0seyJ2YWx1ZSI6IjE4Y2JjNTQxZmM1YzZmZDI5NzFlMjBiNGU5ZmQ1MDdmMDA4MzZhMTRkNWZmYjY3ZGEzNDYwMTFmYzk1MDllMjAiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QucGFja2FnZXNbMF0ud2VpZ2h0In0seyJ2YWx1ZSI6IjJlM2I4YzRiNzI5YjAxMjY2MGNkOTU2MTE1NGFmZGZhOGM0MmRmMDcxZDBlZjBhNjZhZTViZjNkMmZkYmU0YTciLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QucGFja2FnZXNbMF0ubWVhc3VyZW1lbnQifSx7InZhbHVlIjoiMzI1NTVmNjkyNDEyM2JhMDFjOGU2MWFhN2U3MGE1MGY5YWI1NzdlYmY2ODJmYTk3MTVkNWEyZTU5M2FlMWFlMiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5ibE51bWJlciJ9LHsidmFsdWUiOiIxYjhhMTVhYzgzZmQ5MjUxNzVlNTRlODc4MGI2YWQzZjUxYzQwYjlhOGJlYTA3NGQzZGY1Y2U4MDI0MjAyMWNjIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LnNjYWMifSx7InZhbHVlIjoiNTVlZGMxNjRiMWE5ODFjYWMzYTBiNGFlNDlmYzg0Y2Q0ZTY3YTBkNjZkODE4YjVhODcwOTUyMDgzMWI3MzA1NiIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS50ZW1wbGF0ZS50eXBlIn0seyJ2YWx1ZSI6ImMxYjI4OWZjYjY0OGY4NTU0Zjc4NmIxNTM1MmY3ZGVmYmI4Mzg3ZDBmMWI0NzFmYTM4M2I3YWMzYWQzY2E1OTYiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEudGVtcGxhdGUubmFtZSJ9LHsidmFsdWUiOiI0MDcxMTVmNjI0M2Q5NGJiNmQxYjUwMDU5YWM2MjI2ZGQ4NTQ5NTdlNTRmMzBhODI3ZjA2ZWM1YTFmODA4N2VkIiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLnRlbXBsYXRlLnVybCJ9LHsidmFsdWUiOiI3YTM3NWY2MDkzMzA2MDFkYTQxODQwNzQ2ZGQyYjQyMTEwMDY3ZTMwOWQxMWY5MGJiODc3MmQ2N2U5NjMyNzdhIiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLnByb29mLnR5cGUifSx7InZhbHVlIjoiMWU1YzJhYzRmYTNjN2U1NjQxYTJhMGQ3OGU1MTJjOTg1OGMzODI2NGJmMDMxNmI2ZGY2MDRiOTVkYzUyMmUyOSIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi5tZXRob2QifSx7InZhbHVlIjoiYzIxNjg5M2JhOWY5MjAzNmMyNGFlMGQ3MTQ4NjlkMzhmZjM3ZjgyZDhkYTc2YjBjZmNjYzRlM2RkZjY1YmQ5MSIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi52YWx1ZSJ9LHsidmFsdWUiOiI1ODk2MjA2MGZmZmY4ZDQyMGVjYjA1YjJjYTNiYzc5YWJiNDU4YTRlNzc2OGZkY2ZiYjM2ZmRmOWUyNDJlZDg0IiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLnByb29mLnJldm9jYXRpb24udHlwZSJ9LHsidmFsdWUiOiIxYmNmN2M4NWJkODQyNzI1OTEzNzZmMjk1OWUwMjk5MDdmZmM4N2M4MmM2NzE1NGJjMGQ2ZWE2MTAzMmJkZjE2IiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLmlkZW50aXR5UHJvb2YudHlwZSJ9LHsidmFsdWUiOiIzMzVkYjA4MzdlNDFiNDg0YWI1ZjYxYTI4MTA0M2FhODVmMWM5NzMwNTU4YmUwOGZkZTAwNmI3YTIwMjljMjJmIiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLmlkZW50aXR5UHJvb2YuaWRlbnRpZmllciJ9LHsidmFsdWUiOiI2OWIxMThkZjM0NjQ3YjA1ODhkOTc1OWYzYzM2MzllZDExZDIzNWJhYWUyMzAwMGRjN2M3Y2ZlYjA5Yjc0YmU2IiwicGF0aCI6Imlzc3VhbmNlRGF0ZSJ9LHsidmFsdWUiOiJjNDc4MDVkMmIwNGEzNGQ3Y2UzOGVjMDAxZDI4Y2MxYjk3MzNmODgzYTRlYTJjNGQzYjBlYTRiMWZhOGFjYjkxIiwicGF0aCI6ImV4cGlyYXRpb25EYXRlIn0seyJ2YWx1ZSI6ImQyYWNiZjYwYzEwNDc2ZmNiOTQ0MDg2YTAwODRkMjIzZWJhMjdhNzQyYzNmN2JhNWU5ZWE1YjQ4MTE0NDljN2IiLCJwYXRoIjoiaXNzdWVyLmlkIn0seyJ2YWx1ZSI6IjBlNWVkOGNiMDFiZTA0ZGY2OTg0MzlhYTMyNjZjNTY0MGMxNjRlN2VmMTBjYTJjNGNmNWRiZmQzMWQzYjAxZTEiLCJwYXRoIjoiaXNzdWVyLm5hbWUifSx7InZhbHVlIjoiZTgyMTFhZTc2ZjYyMjI4N2Q2ZWM1MzkyNzg4ZDY1OTk1MGRlZWQ5MTg0MjcxZjRjZTFiZTFmNGU4ZWE0YmJjNCIsInBhdGgiOiJpc3N1ZXIudHlwZSJ9LHsidmFsdWUiOiI0MGE0ZTAwYjY0YjEzMWYwYTM2NTM2MDAyYjNjNjJkY2ZmNTI1ZDUyOGNiZGYzZTAxYTQ5ZDcwMzBhMTQ4MjhlIiwicGF0aCI6InR5cGVbMF0ifSx7InZhbHVlIjoiYWZlOTc0OGZkM2U0MGFmZGQyNWI4NmNlZTA5YTJhNjE3N2MzNDZhMDY4ZjJhNmZkMzk4OTNiN2Q2MTJkZWI0MyIsInBhdGgiOiJ0eXBlWzFdIn1d",
+ "privacy": {
+ "obfuscated": []
+ },
+ "key": "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90#controller",
+ "signature": "0x836a2547654da43f01641b3a0efff6797adc7e8b806d65cb9c67e25b119c70c34aa4c73a14d8138f52c05f6f7e1048ead225c85eb981fac8c2207895e48f14a91c"
+ }
+}
diff --git a/src/__tests__/__fixtures__/oa/3.0/signed_wrapped_oa_dns_txt_docstore_v3.json b/src/__tests__/__fixtures__/oa/3.0/signed_wrapped_oa_dns_txt_docstore_v3.json
new file mode 100644
index 0000000..adecdfe
--- /dev/null
+++ b/src/__tests__/__fixtures__/oa/3.0/signed_wrapped_oa_dns_txt_docstore_v3.json
@@ -0,0 +1,100 @@
+{
+ "version": "https://schema.openattestation.com/3.0/schema.json",
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "https://schemata.tradetrust.io/io/tradetrust/Invoice/1.0/invoice-context.json",
+ "https://schemata.openattestation.com/com/openattestation/1.0/OpenAttestation.v3.json"
+ ],
+ "type": ["VerifiableCredential", "OpenAttestationCredential"],
+ "issuanceDate": "2010-01-01T19:23:24Z",
+ "issuer": {
+ "id": "https://example.com",
+ "name": "DEMO STORE",
+ "type": "OpenAttestationIssuer"
+ },
+ "openAttestationMetadata": {
+ "template": {
+ "type": "EMBEDDED_RENDERER",
+ "name": "INVOICE",
+ "url": "https://generic-templates.tradetrust.io"
+ },
+ "proof": {
+ "type": "OpenAttestationProofMethod",
+ "method": "DOCUMENT_STORE",
+ "value": "0xA594f6e10564e87888425c7CC3910FE1c800aB0B"
+ },
+ "identityProof": {
+ "type": "DNS-TXT",
+ "identifier": "example.tradetrust.io"
+ }
+ },
+ "credentialSubject": {
+ "name": "TradeTrust Invoice v3",
+ "id": "1111",
+ "date": "2018-02-21",
+ "customerId": "564",
+ "terms": "Due Upon Receipt",
+ "billFrom": {
+ "name": "ABC Company",
+ "streetAddress": "Level 1, Industry Offices",
+ "city": "Singapore",
+ "postalCode": "123456",
+ "phoneNumber": "60305029"
+ },
+ "billTo": {
+ "company": {
+ "name": "DEF Company",
+ "streetAddress": "Level 2, Industry Offices",
+ "city": "Singapore",
+ "postalCode": "612345",
+ "phoneNumber": "61204028"
+ },
+ "name": "James Lee",
+ "email": "def@company.com"
+ },
+ "billableItems": [
+ {
+ "description": "Service Fee",
+ "quantity": "1",
+ "unitPrice": "200",
+ "amount": "200"
+ },
+ {
+ "description": "Labor: 5 hours at $75/hr",
+ "quantity": "5",
+ "unitPrice": "75",
+ "amount": "375"
+ },
+ {
+ "description": "New client discount",
+ "quantity": "1",
+ "unitPrice": "50",
+ "amount": "50"
+ }
+ ],
+ "subtotal": "625",
+ "tax": "0",
+ "taxTotal": "0",
+ "total": "625",
+ "links": {
+ "self": {
+ "href": "https://actions.tradetrust.io?q=%7B%22type%22%3A%22DOCUMENT%22%2C%22payload%22%3A%7B%22uri%22%3A%22https%3A%2F%2Fgallery.openattestation.com%2Fstatic%2Fdocuments%2Ftradetrust%2Fv3%2Finvoice-stability.json%22%2C%22permittedActions%22%3A%5B%22VIEW%22%5D%2C%22redirect%22%3A%22https%3A%2F%2Fref.tradetrust.io%22%2C%20%22chainId%22%3A%20%22101010%22%20%7D%7D"
+ }
+ }
+ },
+ "network": {
+ "chain": "FREE",
+ "chainId": "101010"
+ },
+ "proof": {
+ "type": "OpenAttestationMerkleProofSignature2018",
+ "proofPurpose": "assertionMethod",
+ "targetHash": "6aced56cddb6d7c374d5a27a76a3c575f62c593759ab3ca95c6b3b0e0e67c0d9",
+ "proofs": [],
+ "merkleRoot": "6aced56cddb6d7c374d5a27a76a3c575f62c593759ab3ca95c6b3b0e0e67c0d9",
+ "salts": "W3sidmFsdWUiOiI2MzkwNDIwOTMwZWM5MWIwMzExOGZjZWQ4MjdhNmIxYzdkZTJjZjc0NGRkMGYwZmY1ZTI5NjZhNGQ2YzdkNGY1IiwicGF0aCI6InZlcnNpb24ifSx7InZhbHVlIjoiZWE4ZWY1NzVlMDk1YjQwMjIzMWU2ZTI3MzgyNzE2YjY2ZDlhZjAzY2ZjNGY0Y2Y3NDAzMmYyYzZjNWFhOTkwNSIsInBhdGgiOiJAY29udGV4dFswXSJ9LHsidmFsdWUiOiI5NGE2NmJmNDljNjcyZDViMmJiZDM1MTA3NDMwYjYwODIwNjMyMWRkMGU4Zjk0YTc5YmM5NTkwYTUxNzk1N2UzIiwicGF0aCI6IkBjb250ZXh0WzFdIn0seyJ2YWx1ZSI6IjI1NjQ4YjI5ZjA1NjFkNWEzZGJhNTNiZGUwNmE2MDc3NmNiY2VhNGJkN2FmOThkZjliZDhmZmNjMWIxYmUwYjQiLCJwYXRoIjoiQGNvbnRleHRbMl0ifSx7InZhbHVlIjoiNmIyMDg2NGM4Yzk3MTYzZDNmMTVlYjU1ODdiNmI3OGRjNTNhOGViODYyNTFkMGVjYzc3MTZhZGVhMTE4Nzg5MiIsInBhdGgiOiJ0eXBlWzBdIn0seyJ2YWx1ZSI6ImM2MzhlYzNkODY1ODNhNzY2MjIwMWI1YWIwZWYxZTc2YjM2MzlhNzM2ZmI3OTZjMmVkOGI0OTY3ZWQxNGYxYWIiLCJwYXRoIjoidHlwZVsxXSJ9LHsidmFsdWUiOiI4YjQ3NzcwMjFjY2E3N2JjYWUxN2IwNWRlOWZiODg5NmFlNjY5ZTVlYjAwYjY0ODBmNTRlOWQ1YTBiNzZlMTFlIiwicGF0aCI6Imlzc3VhbmNlRGF0ZSJ9LHsidmFsdWUiOiI5NjlkYzYwOTQyMWI5MGQ0YWQwOTE1OWEyYjZlNWEzYmQ0ZjQ4MmMzY2ZkYWIyNjc2YzE3MjhhYmRkNTQyOTE0IiwicGF0aCI6Imlzc3Vlci5pZCJ9LHsidmFsdWUiOiI3YTMwMzg5ZDM2MGNiMjZjZGU2ODM2N2ExZmU1NGY3NjZmMjc1NWI2YmRkNzVkMzc2NTdkYmRhNWQwZjEzMGYwIiwicGF0aCI6Imlzc3Vlci5uYW1lIn0seyJ2YWx1ZSI6IjI3YjJmYTA0YWRlMzU3ODI0NWNkZmFiNTEyNTAxNzZkMGFiZWFkMjFmZWVkYTMzNTkxODFmYTg4ODUzMjQ1YTMiLCJwYXRoIjoiaXNzdWVyLnR5cGUifSx7InZhbHVlIjoiYTE0NDMxODcxODA3NTEwZjJhZWI3YjY5NDZhMWYyNmU4N2M0ODVlNjRiOTVmMzUxNDVmYzk0ZDhjNGQ2YzBlMyIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS50ZW1wbGF0ZS50eXBlIn0seyJ2YWx1ZSI6IjczZTY3NzEwNWI2NGZjMzc4NTYyMzU0NDNiOTMzNDkzMzMxMWY1OWIxMmE5ZjZhNDdhODY3NDBkODc2MmRjNDgiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEudGVtcGxhdGUubmFtZSJ9LHsidmFsdWUiOiI4NGE0MWQ0MTk2OGIzMjNjZTRiY2I3NWZlNTdhZTRkNDY4MmNkZGEzMDVkOTc3YjBmMzI1NzhjMmI0NzE2OTM4IiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLnRlbXBsYXRlLnVybCJ9LHsidmFsdWUiOiI4ZjBiMDE2ODc5YTYxYzQxNThlNjM1YzlmYzI4ZjFjYzU5YzAwNDU1ZGEyNjAwNDIwNGJhN2YyODc5MmU5YjMxIiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLnByb29mLnR5cGUifSx7InZhbHVlIjoiNDJiMzcyYWI0MGMyNGQzNTg4ZGJjZjAyZjhiNmU0NTJjMzJmZmVjODc0OWUyYWQyNDYzMzA1YmMzNzg1NzUwNyIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi5tZXRob2QifSx7InZhbHVlIjoiZGJkY2Q2YmU1NTg4MjQxNDY5OWE5OTY1NGYxMGJjODg0NzBhMGNjNTZiOGZhNTU1ODg2MTI5Njc5ZjlkOTJjNyIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi52YWx1ZSJ9LHsidmFsdWUiOiI2MTA5MDgzMzVkYmQzYmQ5MzJmZjMyMTgzYzc5MjU0YjViYmQyOGE2OGYzNWY5YjRhMWRjN2QyNGRlZDQ4ZWY3IiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLmlkZW50aXR5UHJvb2YudHlwZSJ9LHsidmFsdWUiOiI1NmNiZWY1YzU3NzIzZjYwNjk2NDJlYTUxMDg4YjRlYTFiMGNlMjI0MmE5N2ViNGIzMGNkZGZjNmVlZjkzNDExIiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLmlkZW50aXR5UHJvb2YuaWRlbnRpZmllciJ9LHsidmFsdWUiOiIyODdhMWQ5Yzc3ZDk1ZGE5ZTc5ZWI2N2VlZmY2MDJiZWMxMTgyN2I1ZWRmZGUxZjIzYzIwNTBiOTIxMGE5ZjdmIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0Lm5hbWUifSx7InZhbHVlIjoiZDhjNGY0ZTMzMTJjMTM3ODgxMTUxYTM3MjVkZGJkODMxMDU5MjM2ZmQ0N2ZjNTdkNzRkYmY5NWVhZGU1MzY2NyIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5pZCJ9LHsidmFsdWUiOiI3MzIyZTc5MmU3YzljMTY3OWIyZjc2ZDgwMmU0YzY5ZDRhMzY4NTY0OTUxZWFlNGRiNzA5MzIwYmY4MmYyMmFlIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmRhdGUifSx7InZhbHVlIjoiNTA3ZTU5MDZlNjc2ZjZhZmM5ODMzMzc5NzdiMTQ2YjdlNmUyYjM2ZWRhMWEyZTc5MjE0YzJiYTA2MDBiZmFhMiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jdXN0b21lcklkIn0seyJ2YWx1ZSI6ImZmZjVjMWE0NmExOTMwNzQwNGE0OTJjMTU0MTNmMjEwOWM1NmI0NzVmMDNlZmIzYThiY2I0YzFjN2E3ZTEyYmEiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QudGVybXMifSx7InZhbHVlIjoiZDViOWRmNzkzYzlmMzYxMjFhOTZjMzdkYzEwZDc4ZTQ4MzAxNjI1NGEyODM1YTc3MzM3ZjQxNWQ0OWQxOGRjMiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5iaWxsRnJvbS5uYW1lIn0seyJ2YWx1ZSI6IjRiZTI1NGFmZmJjYmVlZDc1ZGNmZWMxYTEzYTFkNWZiZGNkZWI4Mjk5OTE3ZjJhZWE4MGJlZWU2ZGVjZjBlMDgiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuYmlsbEZyb20uc3RyZWV0QWRkcmVzcyJ9LHsidmFsdWUiOiJiZTM0ZWYwMjhkMjE1MDFlZTE0MjFhOWFjMmVkMzlkMWJkZGMyOTJjMjk1NDY1NjNlZGFjZWZjNDdlOGNlZGQ0IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmJpbGxGcm9tLmNpdHkifSx7InZhbHVlIjoiMzA4MDI2MTk2ZTgxMjQ5NDQ3N2E2MmQyYjdmNGFiYTU2YjE3MGE5ZWQ1MDY0NzBmNTVjMGNlMjA2MGY4NjZkZCIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5iaWxsRnJvbS5wb3N0YWxDb2RlIn0seyJ2YWx1ZSI6IjE2MmVkZmY3NzE5ZTRkZDA2MTAyNzAyZmNhZDUxMGVmM2YwNGM4ZjRjY2U5OTg4OTdiNzRkZjBhYmYxZmJiNjQiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuYmlsbEZyb20ucGhvbmVOdW1iZXIifSx7InZhbHVlIjoiZTM0MTMwMzQwY2I3NDFmOWZkMDdiZmJhNmNhY2QxOTkxMDRiMTM4YzlhMThmMGE2ZTQ0OWQ0YWI5OWEyNDQxNiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5iaWxsVG8uY29tcGFueS5uYW1lIn0seyJ2YWx1ZSI6Ijg0MzQ4NGNlZDViNDdhNGNkMzFmMTEwM2MwYmEyYjc5YjQ4MzE0ZGM0OGQwOTZkMzEzNDc2NTBlM2M2ODM5MzUiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuYmlsbFRvLmNvbXBhbnkuc3RyZWV0QWRkcmVzcyJ9LHsidmFsdWUiOiIxMGEyN2RjYmEyZDE5YTBkMGFkZmZlYzMwOWJmZjQ3MzQ5MThjZmI4NGFjZmRhZWZkN2E1OTg2ODQyZjJkN2M3IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmJpbGxUby5jb21wYW55LmNpdHkifSx7InZhbHVlIjoiMmQzYjA3ZWU5OGM5MDNjYjJjOTRjYWEyYzQ1MDYzNjE0NzllY2M0ZDgyMzIzYjllNjdkYjZjNjY0MGYxMmMyNSIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5iaWxsVG8uY29tcGFueS5wb3N0YWxDb2RlIn0seyJ2YWx1ZSI6IjY3MWZjNWUxNDg5OWQxYjU2MTkzM2IzYTMzNjkwMDk0MjFmYjhhZmEwMTRlZGU1YjNiMjdkN2YwMjJkYjc3ZjgiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuYmlsbFRvLmNvbXBhbnkucGhvbmVOdW1iZXIifSx7InZhbHVlIjoiNmRmMzIyYzhkODhjOTQzYzYzZjlmMDJiYjczMDNiYTExMWVlMzkxZjMxNjMxMDYxYzRhODMxM2I0OTg5YmUzNSIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5iaWxsVG8ubmFtZSJ9LHsidmFsdWUiOiI2ZWJjZDAzZTI1M2YyYTljZjcyN2M5YTYyYWI2NDcwZDZmM2FhOTY0NjIyODViZGJhZWIxODQzMzJhNGQ1OWVhIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmJpbGxUby5lbWFpbCJ9LHsidmFsdWUiOiIyNjM2MDAzYWE1YzZlNmFjZjU5ZjRlODgxZTdkN2QzMDllNWYwNDY5MTAyODZjN2I3NzJmOWM2MzU2MjQ5M2ZlIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmJpbGxhYmxlSXRlbXNbMF0uZGVzY3JpcHRpb24ifSx7InZhbHVlIjoiMjkxNmYzYWM1YzYxYWVhMDJlOWVkMTI3M2Q3ZjJhMGYwYjFjNWI1ODY3ZDViNDE3OWZjNzUyOTZhNDllYjlkNyIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5iaWxsYWJsZUl0ZW1zWzBdLnF1YW50aXR5In0seyJ2YWx1ZSI6ImVkOTkyYTc1NTNjMzY2MGYwMjhjYjY1ODU4MzViMzJjYmU2MGI4NWQxZDllOTZlYjNjMjJlYmQxMmM2YWM5NzUiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuYmlsbGFibGVJdGVtc1swXS51bml0UHJpY2UifSx7InZhbHVlIjoiMDgzMzgzODBhM2FjYjVlMDEzZmJiZDE0ODJkNzdmMzFjOTRkOGY3NzYzMjU3OGNkZjRjZjFmYmZlNjY0YTdmZCIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5iaWxsYWJsZUl0ZW1zWzBdLmFtb3VudCJ9LHsidmFsdWUiOiJiY2FhYzkzYzA4NGFlOWQyZDRjM2I5MTIzMDE3YTVkMTc4NTUyZGYwNmM5N2I4ZDBkZmMxZWY3YWIzZWU2NzZiIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmJpbGxhYmxlSXRlbXNbMV0uZGVzY3JpcHRpb24ifSx7InZhbHVlIjoiM2I3ZmVlYmJhYzA0MGU2ODdlOTY5NGNhM2ExZWIzNmJjMmEzNTAzMGI2M2RiZjYzMjAxOWQyYWVhNDRjZjc3YiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5iaWxsYWJsZUl0ZW1zWzFdLnF1YW50aXR5In0seyJ2YWx1ZSI6IjAwYjJjZDZmNTVkMmQ2YWQyNTRiZWM1ZjAxNjlkOThmZTg2OTFmMWY4MTU2N2YwOTZmNzdhMzQ0ZjdjNzQzN2EiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuYmlsbGFibGVJdGVtc1sxXS51bml0UHJpY2UifSx7InZhbHVlIjoiOTZhNjQyYzM1NTg3Y2FhNmJmNGE1NjI2ZjhmYTEzZjc1YTM3MDAxOTMxOTBkYmM1MDcwMTA1NzZiMGNjYjEwNSIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5iaWxsYWJsZUl0ZW1zWzFdLmFtb3VudCJ9LHsidmFsdWUiOiIzYzU4ODBlZmYwOGY2NjVhYjkxMGU5MmYwMzRlYzhkYzQ5NTk3NWZlMzZmMTdjMzk1YzJkNjUxZjk1ZTdlNGVlIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmJpbGxhYmxlSXRlbXNbMl0uZGVzY3JpcHRpb24ifSx7InZhbHVlIjoiYWZhODhmMmQyMWNhZDk3NWU0YzYxM2U5MTRmMzBiNjg0ZGVhNDJhNmM5MDgwODZkNDk5YzZjMjRhOTYzOTkxZiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5iaWxsYWJsZUl0ZW1zWzJdLnF1YW50aXR5In0seyJ2YWx1ZSI6ImZlZjgzMzZmYWZkYzNjYTc0NzU3NmZhOTA3MTg0Yjc1ZjhmMjM1ZWJjODE0NDg2NmE1MDQ3ZjM5ZjgyYTA1ZWUiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuYmlsbGFibGVJdGVtc1syXS51bml0UHJpY2UifSx7InZhbHVlIjoiMDEyNzVhMmY5OTA4Y2I1ZGRmMzc3ZWRmNjI4ZWQ5YTFiMTAzZjI4ODc4OGM2MGU5NmQ1NmU3ZDA4MGI4NDBjNCIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5iaWxsYWJsZUl0ZW1zWzJdLmFtb3VudCJ9LHsidmFsdWUiOiJmYmVhMzIzMzEzYjg4ZGQ2NjU5NDBhYWVhNDA3MDMxOWEyZjM3MDg1NTg1ZmY4NzViMDRlYTIwZTI1YjRiNWQ5IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LnN1YnRvdGFsIn0seyJ2YWx1ZSI6ImYyNWVhYmE4ZjJmYTFmOWUyMTQ4ZDk4NTkyNmNmMGY0NjVmMjE0M2EyMjI3M2U3ZjAzNDQ1YWM0YTZlOWIzMjMiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QudGF4In0seyJ2YWx1ZSI6ImViNGNiNjlmYTU0NjAyZjA0NmY2ZDI2ZjQ0ZmI4ZTUyNzk0ZmZmMzE0Y2FkYWU0MmFiY2I1NDg5MmIwNTliZTEiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QudGF4VG90YWwifSx7InZhbHVlIjoiNmIyNDk0M2EyNjdlYjEzMWJhZGM0M2UzZTc1ZjU5YWIyNWFhYTJlNjJjZDJkYjNmODZjYmJkNDhjMTZiYWVmNSIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC50b3RhbCJ9LHsidmFsdWUiOiIzNzI3OGExYjE0ZDhkOGI3Y2IyOWQwYTY4ZWVmNDYxYzJiMjBkYTA3ZWU0NTgxMmU5YjU2YjBmNDRkODFhNWJmIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmxpbmtzLnNlbGYuaHJlZiJ9LHsidmFsdWUiOiI5NTkwMTdmNGYyNDk0YjZmMTg3ZDc5MTE5OGM1MzdmNGI2MTdkY2Q5ODUwNzZmMzQ0MTI5ZjFlOWI3NGRhMzc3IiwicGF0aCI6Im5ldHdvcmsuY2hhaW4ifSx7InZhbHVlIjoiOWFlMjY2ZWNlYzMxMjkzOTU5NGQ4YWYwNGVjMzgyNWE0NzllODBhN2JiMTE2YTI5MzhhODU2OTljZjRiZjVkOCIsInBhdGgiOiJuZXR3b3JrLmNoYWluSWQifV0=",
+ "privacy": {
+ "obfuscated": []
+ }
+ }
+}
diff --git a/src/__tests__/__fixtures__/oa/3.0/signed_wrapped_oa_dns_txt_token_registry_v3.json b/src/__tests__/__fixtures__/oa/3.0/signed_wrapped_oa_dns_txt_token_registry_v3.json
new file mode 100644
index 0000000..fc52eb4
--- /dev/null
+++ b/src/__tests__/__fixtures__/oa/3.0/signed_wrapped_oa_dns_txt_token_registry_v3.json
@@ -0,0 +1,83 @@
+{
+ "version": "https://schema.openattestation.com/3.0/schema.json",
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "https://schemata.openattestation.com/com/openattestation/1.0/OpenAttestation.v3.json",
+ "https://schemata.tradetrust.io/io/tradetrust/bill-of-lading/1.0/bill-of-lading-context.json"
+ ],
+ "type": ["VerifiableCredential", "OpenAttestationCredential"],
+ "issuer": {
+ "id": "https://example.com",
+ "name": "Demo token registry",
+ "type": "OpenAttestationIssuer"
+ },
+ "issuanceDate": "2010-01-01T19:23:24Z",
+ "openAttestationMetadata": {
+ "template": {
+ "type": "EMBEDDED_RENDERER",
+ "name": "BILL_OF_LADING",
+ "url": "https://generic-templates.tradetrust.io"
+ },
+ "proof": {
+ "type": "OpenAttestationProofMethod",
+ "method": "TOKEN_REGISTRY",
+ "value": "0x71D28767662cB233F887aD2Bb65d048d760bA694"
+ },
+ "identityProof": {
+ "type": "DNS-TXT",
+ "identifier": "example.tradetrust.io"
+ }
+ },
+ "credentialSubject": {
+ "name": "TradeTrust Bill of Lading v3",
+ "blNumber": "123",
+ "scac": "DEMO",
+ "carrierName": "Demo Carrier",
+ "shipper": {
+ "name": "Demo Shipper",
+ "address": {
+ "street": "One North",
+ "country": "Singapore"
+ }
+ },
+ "consignee": {
+ "name": "Demo Consignee"
+ },
+ "notifyParty": {
+ "name": "Demo Notify"
+ },
+ "vessel": "1",
+ "voyageNo": "100",
+ "portOfLoading": "Singapore Port",
+ "portOfDischarge": "China Port",
+ "placeOfReceipt": "Beijing",
+ "placeOfDelivery": "Singapore",
+ "packages": [
+ {
+ "description": "Green Apples",
+ "weight": "20",
+ "measurement": "100"
+ }
+ ],
+ "links": {
+ "self": {
+ "href": "https://actions.tradetrust.io?q=%7B%22type%22%3A%22DOCUMENT%22%2C%22payload%22%3A%7B%22uri%22%3A%22https%3A%2F%2Fgallery.openattestation.com%2Fstatic%2Fdocuments%2Ftradetrust%2Fv3%2Febl-stability.json%22%2C%22permittedActions%22%3A%5B%22VIEW%22%5D%2C%22redirect%22%3A%22https%3A%2F%2Fref.tradetrust.io%22%2C%20%22chainId%22%3A%20%22101010%22%7D%7D"
+ }
+ }
+ },
+ "network": {
+ "chain": "FREE",
+ "chainId": "101010"
+ },
+ "proof": {
+ "type": "OpenAttestationMerkleProofSignature2018",
+ "proofPurpose": "assertionMethod",
+ "targetHash": "3569b4d361b452af81fcff075f6df202ed6ffa3e58223f3bb43cc55a54968505",
+ "proofs": [],
+ "merkleRoot": "3569b4d361b452af81fcff075f6df202ed6ffa3e58223f3bb43cc55a54968505",
+ "salts": "W3sidmFsdWUiOiI5MWQxYmMwNzc4NjhmODEyMTYwMGRlMjFjMDEzYWIwNTA0YWM5NGM5MmQ4OTQ0YThmN2ViYzcxNTYyMjkwZGRhIiwicGF0aCI6InZlcnNpb24ifSx7InZhbHVlIjoiM2U1ODUyMzQ0NDA4YWVhYTE1MmI5Y2I2NjA3YmRjNDFhN2YzNDk4NTI5MDQ3YzA5MmI1ZDZlZDQwMzUxZmQyNyIsInBhdGgiOiJAY29udGV4dFswXSJ9LHsidmFsdWUiOiJiNjA3MWFhYjM1ZTMxZTBjMmI2ZjM1ODI5ZWMyMDBhNzVlYjc4ZDdiNDlmMjQ5YzAyMDIyM2VlODIxN2VhOWUzIiwicGF0aCI6IkBjb250ZXh0WzFdIn0seyJ2YWx1ZSI6ImVkNTlkMTNiOTdhZWJiNTViNGRkY2YxNDM2ZGU3MjIwNGU3ZGE4NjdjY2NmZDgxYmIwMjgyODZhYWNhMmI0ZDkiLCJwYXRoIjoiQGNvbnRleHRbMl0ifSx7InZhbHVlIjoiNWUzNzVjMzhkNmI3ZDhhNGI2NmM3ZDAzNGJhMjM4Y2U1ODBjZTA2MzZmMWJiNmVlMWRiNDkyNTMyODNjNDQwNyIsInBhdGgiOiJ0eXBlWzBdIn0seyJ2YWx1ZSI6IjFkN2MxMTMyZDE5YTViZTFhMGQ4MDU1M2U5NDA1YzNmZWQ3YWZkMjkzM2Y0NjI5OGJmNDJmODgwYzgyMjg4NTEiLCJwYXRoIjoidHlwZVsxXSJ9LHsidmFsdWUiOiJiMmMxOTFlNDFiMzNlMDViM2FmMTU0N2JkODZhYTlmNGFlZGI3NjViMjk2YWVlZDZmY2I5ZTBlNzhmY2JmYmMwIiwicGF0aCI6Imlzc3Vlci5pZCJ9LHsidmFsdWUiOiJjYmM0YmEwYWYwYzMyYmJmNzdiNzJjN2Y1Njc3NTBhZTYxOWJhZjRlY2I5ZTZlYzE0ZTkwYTFhYTIxZmIxMmY4IiwicGF0aCI6Imlzc3Vlci5uYW1lIn0seyJ2YWx1ZSI6ImJkMzZkNDc0OGM3MTRiNWNlOWZkOTllMzgyNTljMWIyZDI3YWMxNjg2YmM5YTQxNGNhYWE5YTRhMDE4OGUyYWYiLCJwYXRoIjoiaXNzdWVyLnR5cGUifSx7InZhbHVlIjoiN2M4OWVlOTJhYmYyNzEyOWE4MmY2ZTU3NDczYTU1YWI5MjAwOWQ4YzM2NmM2NTQ1NjU5ZWNjODBmMGMxMDQ4NSIsInBhdGgiOiJpc3N1YW5jZURhdGUifSx7InZhbHVlIjoiYWY4ZGQ1YWY1MTEzMDg3YzgzNzMyZDNkMTliYjIxYzJjYjdkMTQ5M2Q0MjRjYjBmOGE3ZDhmODAyOGE3MWE4OCIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS50ZW1wbGF0ZS50eXBlIn0seyJ2YWx1ZSI6IjJmOTRlMjBkMDExNmNmMDUxOWM4YjM1OWYzYTYxYzAzOWMwNzc1ZWMwNzdhZTE2NGUzNWRlOTY2MTAzYTA4MGMiLCJwYXRoIjoib3BlbkF0dGVzdGF0aW9uTWV0YWRhdGEudGVtcGxhdGUubmFtZSJ9LHsidmFsdWUiOiJjYjliYzQxYzI4NDM1ZTU0ZWZlMzRiNjIzODRhNzkzMGU0ZGJhNThjMTYwNmU3MWFhM2QxYzUzODIzZjU0NWNhIiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLnRlbXBsYXRlLnVybCJ9LHsidmFsdWUiOiJhNzc0MDg2YjAyMWMyYTk1Mzc3NDg5ZmQxYjhkMjc4ZmE5NWRmNDc4ODM1OTY4ODVkYzhmYWI4MzA4YmQ4ZGNmIiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLnByb29mLnR5cGUifSx7InZhbHVlIjoiNTVlYThlZDI1MDg5N2E3MDRjYzMyZTk2N2MwMTZiZjE0M2ZmYWNiYjUwOWRiOTQzMWRiNTQxYWYyMDk4ZTFhMyIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi5tZXRob2QifSx7InZhbHVlIjoiOWYyMjZiNjhlZTMwNjc5ODU0YjU0OWVkZTFlMTFjMzE1OGUzNDljOWE5YTlkZTJmZDhkZTAwZWQ4ZGEyY2ZkZSIsInBhdGgiOiJvcGVuQXR0ZXN0YXRpb25NZXRhZGF0YS5wcm9vZi52YWx1ZSJ9LHsidmFsdWUiOiJmMzhkZTI1MzFkMDhhNDA5MDEyYzU3ZDUwMDc0NWNkNTQwNmU5YjY4MDg4ZTQwZmY3MDIyZTc0NWUzOTc2MWJiIiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLmlkZW50aXR5UHJvb2YudHlwZSJ9LHsidmFsdWUiOiJiM2Y5NjE4NGYyNTFmNzg2Y2Q5OTNlOGYwNzZiZWY2NWI5OTQxODlmNmJmNWYwZTdkNWZhZTA1ODM3NGUyZjk4IiwicGF0aCI6Im9wZW5BdHRlc3RhdGlvbk1ldGFkYXRhLmlkZW50aXR5UHJvb2YuaWRlbnRpZmllciJ9LHsidmFsdWUiOiI5MDI0MDFjM2IyNTgwZjRhYzhkYTc0MTdkNGY5YmU4MTUyZjZjMGM3OGIzNjU4NjU1YjFjYTFhNGNiODZhOGRkIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0Lm5hbWUifSx7InZhbHVlIjoiMzIwMzQzMjE5Yzk5ODg2NWQ4MjA2MjI2MTE4MWQ0ODE3ZDk3M2NlNmZlZDNhMzE2NGNjOWYxYTQwY2MyZTljOSIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5ibE51bWJlciJ9LHsidmFsdWUiOiI1NzU4MTZjMDlhOWE3NzJiNjMyZjJiMDU1MmYyYjc0NmFmYmJkY2JhZDQ1ZmRhNjY2YjMxMjQzYzBlYjY2YjgyIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LnNjYWMifSx7InZhbHVlIjoiYWVjMDg0Mjc2MmRkZGQ0OThjMDAxN2ViYWE2NmQ2N2VlNDY2Y2Y5OTZlZTI4YTkwZDE5NmNlMGYwM2NiOGQyNCIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5jYXJyaWVyTmFtZSJ9LHsidmFsdWUiOiJjNTQ3MzE2M2JjM2I5NWUxM2NkMGY2MzdmNGNjN2U2ZmY3YTY5M2NkOGE5ZGRlMDlhYzI0MTljODJiYTk1YmZhIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LnNoaXBwZXIubmFtZSJ9LHsidmFsdWUiOiIwZWQ0NjJjZDBkMDM2YWVhZjk3N2Q1ZTRlMWViZDM0ZGIzYjdjYTE0YzU0MWE5ODMxOTYwNGMxMWI0MzYwNTgxIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LnNoaXBwZXIuYWRkcmVzcy5zdHJlZXQifSx7InZhbHVlIjoiODU5ZDYyYzdkNDU4NzA3MGFhYzIwYjA4NDBhMzczZmQxOTRmODdmNmM5M2I5NmQ1OGY0M2E2Nzg5N2U3NGVlMiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5zaGlwcGVyLmFkZHJlc3MuY291bnRyeSJ9LHsidmFsdWUiOiI0MTZiOWRlM2E0M2Y4YTEzMjEwYjJlNDMzYjMwOTRjODIyMTcwYmNmNzZiMzg5NzRhMTE2YjJkYmNkNDM2ZDg1IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmNvbnNpZ25lZS5uYW1lIn0seyJ2YWx1ZSI6ImE2Y2UxZGZkZmNlNmNmZjExNzc2ZjYyMzcyZTJlYzA2NWJkM2YxZTIyNjJkMTFlYTQ4NDFkMjY2YWZiNTI5MTMiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3Qubm90aWZ5UGFydHkubmFtZSJ9LHsidmFsdWUiOiI3YjI5MjE0ODczMzA2NzZiNzk2ZmRmYWE2Y2UwY2EyMThmNjI0YzAwNmI0OTMwZjIyYzIxOWNhMDBlM2Q2OTc4IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LnZlc3NlbCJ9LHsidmFsdWUiOiI5MDg5NjY2OTMxZGJhZjE3MzAxYjg1MGUzM2I5YTEwODViYzk5MzY5MzJiN2IxMzQ0OGFiYzc3YTU3N2VlM2RmIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LnZveWFnZU5vIn0seyJ2YWx1ZSI6IjE2ZDhhNjA4ZDNjZTA2YzcyZmJmMWYxNWJkNTRiODhhZjk5ZjY0N2M3MTkxMGE0NzgxZWRkZjc2M2ExZDRlNzgiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QucG9ydE9mTG9hZGluZyJ9LHsidmFsdWUiOiIzZjM4OGI3NmNkNTFhNGJhMTA2MGVlZGQyZDM0N2I5YWQ5YWFkY2E4YTUxMDA4NDMzNzhiNDc5MjhlYjc4MmMxIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LnBvcnRPZkRpc2NoYXJnZSJ9LHsidmFsdWUiOiJiODcwNjFmY2M1MTQ0NmM3OWE2MzFjOTU5YjQzZWM1ZTM4MjMyNTcxYjRmZTVhOWJlNzRkNzZkZGZiM2M3NGVkIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LnBsYWNlT2ZSZWNlaXB0In0seyJ2YWx1ZSI6IjZkMWRkYTExOGExNWI4ZWM0OWRkMTYxZmFiNzZjMzFhZWExYTQ1YzFkMjg0Mzc1MDU5ZWE1NTQzYWQ4MzEzZmMiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QucGxhY2VPZkRlbGl2ZXJ5In0seyJ2YWx1ZSI6IjEyM2Y0ZGJjZmNiNGY5ZTYzNjg2Nzk5NWNiNWM1NzNhNWI4NTE3ZGVkOGFkZmM0OTliMzMwNTg3MGJhMTc2OGIiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QucGFja2FnZXNbMF0uZGVzY3JpcHRpb24ifSx7InZhbHVlIjoiMjllMDVlZDQ1ZjdiMjQ3YTQzYjE2NjU0MTcxM2JiMjVkYWQ1Y2UwNDA2Yjk3MzliYWEyYmQyM2I4MjRlNGMzYSIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5wYWNrYWdlc1swXS53ZWlnaHQifSx7InZhbHVlIjoiMzM3OGM0NmRjMjUyZWZhNDhlN2EyYzg1NzI0NWY1Mzk5MTcwZTIyNWIzNThlMjJjZWM5ODJmZjNkOWVjNTA3ZCIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5wYWNrYWdlc1swXS5tZWFzdXJlbWVudCJ9LHsidmFsdWUiOiIzNTViNjU2NTEwMzc5NTA4ZWU3ZjFhZDllNzc0M2Q1Nzg1NDcyYWEyZWNhZDkyMzYwYzQ2MzU5YTgxNjJkODZhIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmxpbmtzLnNlbGYuaHJlZiJ9LHsidmFsdWUiOiIxMDM3NjM0NGJiMWM4YmVlNWRmMzQ2NzU1YmVkYjMyMjMwZDdiZGNmYmE3YzM2Zjc0OGI2ODgwYzJjZjMwNjUyIiwicGF0aCI6Im5ldHdvcmsuY2hhaW4ifSx7InZhbHVlIjoiMjM5ZjJkZWMwODkxMzczOTUyNzYzMjYzOWNkYjk5NzE2ZGRkYzg0ODRmYWNmYTdhMDYwODY3MzE5MGMyMWIxNyIsInBhdGgiOiJuZXR3b3JrLmNoYWluSWQifV0=",
+ "privacy": {
+ "obfuscated": []
+ }
+ }
+}
diff --git a/src/__tests__/__fixtures__/w3c/bbs2020_w3c_transferable_record_v1_1.json b/src/__tests__/__fixtures__/w3c/bbs2020_w3c_transferable_record_v1_1.json
new file mode 100644
index 0000000..6d99d1b
--- /dev/null
+++ b/src/__tests__/__fixtures__/w3c/bbs2020_w3c_transferable_record_v1_1.json
@@ -0,0 +1,101 @@
+{
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "https://trustvc.io/context/bill-of-lading-carrier.json",
+ "https://trustvc.io/context/attachments-context.json",
+ "https://trustvc.io/context/render-method-context.json",
+ "https://trustvc.io/context/transferable-records-context.json",
+ "https://trustvc.io/context/qrcode-context.json",
+ "https://w3id.org/security/bbs/v1"
+ ],
+ "renderMethod": [
+ {
+ "type": "EMBEDDED_RENDERER",
+ "templateName": "BILL_OF_LADING_CARRIER",
+ "id": "https://generic-templates.tradetrust.io"
+ }
+ ],
+ "credentialSubject": {
+ "type": ["BillOfLadingCarrier"],
+ "shipperName": "MAERSK Co.",
+ "shipperAddressStreet": "101 ORCHARD ROAD",
+ "shipperAddressCountry": "Singapore",
+ "toOrderOfText": "TO ORDER",
+ "consigneeName": "ABC Natural Foods Inc.",
+ "notifyPartyName": "Amanda Green β Import Manager, ABC Natural Foods",
+ "packages": [
+ {
+ "packagesDescription": "Organic Cashew Kernels (25kg bags)",
+ "packagesMeasurement": "100 Bags",
+ "packagesWeight": "2.65 MT"
+ },
+ {
+ "packagesDescription": "Roasted Chickpeas (20kg packs)",
+ "packagesMeasurement": "60 Bundles",
+ "packagesWeight": "\t1.3"
+ }
+ ],
+ "blNumber": "SGCNM21566325",
+ "scac": "SGPU",
+ "carrierName": "Vikram Rao",
+ "logo": "data:image/png;base64,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",
+ "onwardInlandRouting": "Rail to Johor Port β Trucking to final inland delivery point (Long Beach, USA)",
+ "vessel": "MAERSK NATALIA",
+ "voyageNo": "7831W",
+ "portOfLoading": "Singapore",
+ "portOfDischarge": "LOS ANGELES, CA",
+ "placeOfReceipt": "JURONG PORT, SINGAPORE",
+ "placeOfDelivery": "Long Beach Distribution Center, CA",
+ "placeOfIssueBL": "Singapore",
+ "numberOfOriginalBL": "3",
+ "dateOfIssueBL": "2025-06-05",
+ "shippedOnBoardDate": "2025-06-05",
+ "signForTermsAndCondition": "The carrier accepts the goods as described in good order and condition for carriage under the terms stated herein and subject to the Carrier's standard Bill of Lading Terms and Conditions.",
+ "signedForCarrierText": "John Doe",
+ "carrierSignature": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABCwAAAG6CAYAAADDFddpAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3QHWoziWJtDoleX0ymJ6ZTO5splUVdDtcAASIKEn6f7n5MnqTgzSfcI2n4X4jx/+CBAgQIAAAQIECBAgQIAAAQLBBP4jWHs0hwABAgQIECBAgAABAgQIECDwQ2BhEBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAgTYC/+vHjx/pn//76582R7FXAgQIECBAgMCkAgKLSQurW90EPi9Q0v/+391a4sAECPQU+D+/worPNvyX94SeJXFsAgQIECBAYDQBgcVoFdPeyAJ7FyipvekiJf0JLyJXT9sI1BP4fye7SrMt/vZ+UA/bnggQIECAAIF5BQQW89ZWz94TSDMpfu78mrrXAr+wvlcXRyLQQyC9H6TwMvf3n24TyRH57wQIECBAgMDqAgKL1UeA/tcQOPs19Wj/KbhwX3sNffsgEEsgzaRKAWbJnwCzRMk2BAgQIECAwLICAotlS6/jlQSuXJzsHdLtIpUKYTcEgggc3Rp2Fl66XSxI8TSDAAECBAgQiCUgsIhVD60ZT+DO7Iq9XrqvfbzaazGBPYG9EDOd3+lWEaGFMUOAAAECBAgQuCAgsLiAZVMCXwJnsytyFyhnmO5tN9QIjCtw9HSQ1KOzW0XcHjJuzbWcAAECBAgQaCQgsGgEa7dLCJwFFtu5tU31Lr2nfYNz8bLEENLJCQX2AosthMwtyOm8n3BA6BIBAgQIECBwX0Bgcd/OKwkc3aueZlekC5TvvxRe/FX4NBHBhfFFYEyBs8Ai9SgXWphhNWbdtZoAAQIECBBoICCwaIBql8sIHK1fcRRYbDBXZ1341XWZIaWjEwjkAovUxdxivUKLCQaCLhAgQIAAAQLPBQQWzw3tYV2Bs6cBlFxwpF9a0z+lt4sILtYda3o+jkBJYFESWvh8HqfmWkqAAAECBAg0EvCFqBGs3S4hcBZY5GZZfAPlfnHdthdaLDG0dHJggb2ZV0cBZu4RqCXB58BUmk6AAAECBAgQOBcQWBghBO4L5C42roYLpWtcXN3v/R56JQECVwX23hfOPmvPHo18Nfi82lbbEyBAgAABAgRCCwgsQpdH44IL5BbPS82/Ey6UzLZIFzJp3+nf/ggQiCNwZYZFanXufL/zHhJHQ0sIECBAgAABAg8EBBYP8LyUwD/rT5z9OroB3Z3WnZvBkfZ/d9+KR4BAG4GrMyxSK3LnuvO8Ta3slQABAgQIEAguILAIXiDNCy+Qu9B4Glrkfn0VWoQfIhq4mMDVGRaJJzdby60hiw0i3SVAgAABAgT+LSCwMBIIPBPIXWhse39ywVESWpg2/qyOXk2glsCdGRbp2Lnw0zleq0L2Q4AAAQIECAwjILAYplQaGligJFBIzX8SWrigCTwANI3Ah0DpY02/0UrCT5/ZhhoBAgQIECCwlIAvP0uVW2cbCrwVWuSO41fYhkW2awIFAncDi7Rr53cBsE0IECBAgACBdQQEFuvUWk/bC+QuNrYWPA0Vcsd5uv/2Uo5AYF6BJ4FFUsndGmIBznnHjp4RIECAAAECXwICC0OCQF2B3MXGW6GFi5q6dbU3AqUCTwOL3K0hT28tK+2H7QgQIECAAAEC3QUEFt1LoAETCkQJLcy0mHBw6VJ4gaeBRepg7j3EuR1+GGggAQIECBAgUENAYFFD0T4I/Cmw92jDPaenFx6520PMtDA6CbwrsHdOXj3PS2ZZpH2m2Rb+CBAgQIAAAQLTCggspi2tjnUWyF1wfDbvaaiQCy2uXix1pnN4AkML7J37d87B3Hnt1pChh4nGEyBAgAABAiUCAosSJdsQuCcQKbR4GorcE/AqAusJ1Aosklxuppbzer3xpccECBAgQGApAYHFUuXW2Q4CuV9JtyalX0ufTvHOHevOr7wdyBySwNACe4HF3dkQudDz7n6HBtZ4AgQIECBAYB0BgcU6tdbTfgK5IOEztEi/mD75yx3LL7JPdL2WQF6gZmCRjpZbgNM5na+JLQgQIECAAIFBBQQWgxZOs4cTyAUJnx16el7mjmWmxXDDR4MHE9i7lePueW2WxWDF11wCBAgQIECgnsDdL1D1WmBPBNYRyAUJm8Qbt4c499cZd3r6vsDerIgn55xZFu/X0BEJECBAgACBAAJPvkAFaL4mEBhOIHfh8RlauD1kuPJqMIF/Ceyd509v3cgtwOnz3OAjQIAAAQIEphPwBWe6kurQAAKRQgvvAQMMGE0cTqBFYJGboeVWr+GGiQYTIECAAAECOQEXKzkh/51AfYF0T/rPHz9+pH/n/mo8BSB3ofP0l99cH/x3AqsJ1Hy06afd2SyLGu8Vq9VJfwkQIECAAIHgAgKL4AXSvKkFclO8Pzv/9Fw9Cy1c6Ew9zHSug0DtJ4VsXRA+diimQxIgQIAAAQL9BJ5eBPVruSMTmEPgyu0hacp3Chfu/uWOZabFXVmvI/C7QKvAIh3FLAujjQABAgQIEFhGQGCxTKl1NKjA1dtDnoYWfqENOhA0azqBmo82/cRxDk83VHSIAAECBAgQOBIQWBgbBPoL7P0ae9QqjzztXy8tIFAi0GLhze24ZlmUVMA2BAgQIECAwPACAovhS6gDkwhcCS1Sl58+EcDtIZMMHN0IK7A3E6LWbVfO37Bl17CFBbYZk3//MkjvAf4IECBA4KGAwOIhoJcTqCyQuxD5PNzT0CK3EOfT208q09gdgaEEWq5jkSDMshhqOGjspALpPE///LXz5C8LWk9adN0iQOBdAYHFu96ORqBE4M3QInesWr8Il/TbNgRmEmgdWDh3Zxot+jKaQOn6U09/WBjNRXsJECBQXUBgUZ3UDglUEchdjHwe5OkXIov4VSmZnRD4Q6DVwpvpQLnbyJ6+LygnAQJ/CpQGFdsrzbIwiggQIPBQQGDxENDLCTQUyAUJNUOLXEBipkXDQtv1tAJ751XNz92z89aF0rTDSsc6CFwNKj6bWPOc79B1hyRAgEBfAW+iff0dnUBOQGiRE/LfCcQVaLnwZup1bpaFoDHu2NCyMQS2NSp+3myumU434byMAAECm4DAwlggEF/gSmjx9FfV3LFcAMUfL1oYR2AvUKh9Dll8M069tWQugdznYUlva5/vJce0DQECBKYSEFhMVU6dmVjgyhcnocXEA0HXhhJovfBmwsi9N/icH2rIaGwAgdw5VdLE9DnsSVslUrYhQIBARsAXGUOEwDgCV79EPfllx5oW44wLLY0t8D0D4mmguNfbs1kWT94HYstqHYF6AtsaFWmP6X/f/dtCinSe+yNAgACBCgICiwqIdkHgRYHcPevfTXlysZJb0M+vRy8W3qGGFWi98GaCyZ2r6X3AHwECfwo8WUxz25vZFEYWAQIEGgoILBri2jWBhgK5GRCfh36y6JcLoYZFtOslBFovvJkQc0Hmk+ByiSLp5HICNYIKsymWGzY6TIBADwGBRQ91xyRQR+DKLSJPQouz47SY3l5Hx14IxBDYCxOenI9HvRIuxqi3VsQWqBFU+NyLXWOtI0BgMgGBxWQF1Z3lBN4KLVwMLTe0dLiSwBsLb+ZmWbjAqlRMuxlWQFAxbOk0nACB1QUEFquPAP2fQSA3Hfyzj09+2T0LLZ7sd4Ya6AOBM4G9RTFbfP5afNM4JPC7QI2gwuebUUWAAIGOAi2+MHXsjkMTWFqgdF2LJ7+2Ci2WHmI6f1Ng77xpsa6EmVA3C+Rl0wkIKqYrqQ4RILCqgMBi1crr96wCV24RuXv+n/2K65eoWUeWfj0ReGPhzdS+3GyrFiHJExevJVBbIJ0Dn48ovbN/T/24o+Y1BAgQaCRw94KlUXPslgCBCgJXQos7FzAuiioUyS6WEnhr4c2EahbUUkNLZz8Ernz27cE9mX2oEAQIECDQSEBg0QjWbgl0Fij94nb3l6Tc/r23dB4ADh9O4HtmUquLo7NAsdUxw2Fr0DICtW79SOdG+scfAQIECAQTcFERrCCaQ6CyQOm6FndmWnjcaeVi2d3UAm8FFgnx7Ly/c65PXRidG1KgVlCRPsf8ESBAgEBgAYFF4OJoGoFKArnZENth7qw/Yfp5pSLZzfQCb61jkSCFidMPp2U7WCOoMNNo2eGj4wQIjCggsBixatpM4LpAr9DCr7nXa+UVcwrshXstP4M94nTOcbRqrwQVq1ZevwkQWF6g5Zel5XEBEAgmkFss8+5MC/fMByu05oQU2DtPWv7Sa/ZTyGGgURcFngYV27oUKTz3R4AAAQIDCggsBiyaJhN4KFCyrsXV20POZnBc3dfD7nk5gZACbz4pJAHkAkqf/yGHiUb9EngaVKTd+OwxnAgQIDCBgC8sExRRFwjcECi5ReTql72zIMR7zY0iecl0At/nSMsZFgnP4pvTDaHpOySomL7EOkiAAIFrAi4irnnZmsBMArVDC7eGzDQ69KWFwNvrWDgnW1TRPlsI1AoqPJ60RXXskwABAh0FBBYd8R2aQACB3LTx1MQrMy3O9mcBzgAF14SuAm8+KWTrqMU3u5bcwU8EtpAibZL+992/9BklqLir53UECBAILiCwCF4gzSPwkkBuXYsrU9eP9nVlHy9122EIvCrw9joW24VgOif3/pyTr5bfwX4J1JhNkXYlqDCkCBAgsICAwGKBIusigUKB3C0ipTMtzmZZlO6jsMk2IzCUQI/AIgGZZTHUMJm2sYKKaUurYwQIEGgnILBoZ2vPBEYUyIUWpb/ImmUxYvW1+Q2BtxfeTH06O69Lz+k3bBxjPoHtVo+fD2/7SDJprG6zKuaT0iMCBAgQ2BUQWBgYBAh8C+RCi9JZEkehRenrVYbAjAJvL7y5GQoRZxxNcftUazZF6qFbP+LWWcsIECDQXEBg0ZzYAQgMKZALLUp+lT3aR8lrh0TTaAIFAnvnxRufxWfr1AgRCwpnkyKBWkFF+pz4+58jpvPFHwECBAgsLPDGl6SFeXWdwPACTy9yzLIYfgjoQGWBXutYpG6YZVG5mHb33wKCCoOBAAECBJoICCyasNopgakEntz/bpbFVENBZyoI9Awszs5lsywqFHexXdQKKRKb9SkWGzy6S4AAgVIBgUWplO0IrC3wZKbF0RMK/vPXl9S1ZfV+RYEeC28m57Mn+KT/7jvBiqPxWp/TGNqCimuv3N9aUFFD0T4IECAwsYAvJxMXV9cIVBbIrWtxFEAcXSRZy6JygexuGIG9EO+tz+Oz83jFEHF7isX2779+rZ2wDab0PpX+WfnvM6DYnJ56mNHzVNDrCRAgsIjAW1+QFuHUTQLTC5z9Qnu2SJpZFtMPDR28INDrSSGpiblzOIUWs//dvQDfgou0GOTn3/b/nynYuGuUGzuCipyQ/06AAAECvwkILAwIAgSuCqRfaNOvkEe/tO39SmuWxVVl288ssHc+vDm74ewWrzfb8WaNa663UNLu73BjhJkarYzc9lEyYmxDgAABArsCAgsDgwCBOwK5e+H3fkUzy+KOtNfMKNBz4c3kudIsi1zA+vb42maipeP2CjE+w+afvwBq3erx6SmoeHt0OR4BAgQmFBBYTFhUXSLwkkDu17jvX2o9MeSlwjhMeIHegUUCmnmWRe69KeoA+Q4zUj++bzPJ3XbyHTyk/zvNiNuCqjf6ngLrXmHMG/1zDAIECBB4UUBg8SK2QxGYVOBo5kTq7vdMC7MsJh0EunVZoNeTQraGns2yGHWdgVGDisuDJ+ALzKYIWBRNIkCAwAwCAosZqqgPBPoLnD154PPixyyL/rXSghgCPRfe3ATOwsaR1rIQVPQZ01tIkY6em/nRp4WOSoAAAQLDCwgshi+hDhAII3A2xfzzEaZ7F0kecRqmjBryksB3eNfjHJhhlkXuccsvlXOZw5hJsUypdZQAAQIxBAQWMeqgFQRmESiZaXEUbIw6DX2W2unHuwK9nxRSMssi+neEp2FFyQyBVKfPdSHeXg/i3VG5fzQhRYQqaAMBAgQWFYj+ZWTRsug2gaEFzi4itl+RzbIYusQaX0EgwsKbqRtn52vk20LuhhXbwpa1F4XcQo3vcKPF0zcqDL/sLoQUWSIbECBAgMAbAgKLN5Qdg8B6ArnHnh6JRL5AWq+Ketxa4Du46zH+c+dqxO8Jd8KKnk+u+JylkWZoRAsxtvUn/vZ0j9anvP0TIECAwFWBiF9ErvbB9gQIxBS4c1HR4z7+mHpatYLAd2DRa/yP9ojTs8VCP8dN9FkC37ebpLanQCMFBz9/hQc1wo1tVknadwpuPh+XarHMFd5p9JEAAQIDCwgsBi6ephMYQCD36+1eF7wvDVBYTawisHfh3WP8n52nvUKUI+CSIDR6UHF38HzedpL6uP3f2//+Dh+EEXelvY4AAQIEwgj0+GIUpvMaQoDAawKlv4imBkW7QHoNyYGWE9i7+O71uTzCLIuSsMLivcudRjpMgAABAjML9PpiNLOpvhEg8KfA1ZkW3puMohUE9i7Ae6xjkaxHmGWRCz6FFSucNfpIgAABAksJuChYqtw6S6C7QO6CY2tgr4u27kAasJTAXmDR86L7bJZF7+8LudkVPd2WGrQ6S4AAAQIE3hTo/QXkzb46FgECMQTOLoq2FrotJEattKK9QJSFN1NPo94WUjJDS8jZfqw6AgECBAgQeF1AYPE6uQMSIPCPQO7X0oTk/clQWUEgysKbm/XRLKieIWLu/cLsihXOFH0kQIAAgSUFXBAsWXadJhBCIHcR0vMCKQSQRiwhEGmGRcRZFrnZFd4nljhNdJIAAQIEVhUQWKxaef0mEEMgF1r45TRGnbSinUCkhTdTL6Mtvuk9ot3Ys2cCBAgQIBBeQGARvkQaSGB6gdwvqEKL6YfA0h3cWzei95iPtJbF2UK9vZ2WHrg6T4AAAQIE3hAQWLyh7BgECOQEhBY5If99VoG9sd/7QvxsVsObbTO7YtZRr18ECBAgQKBQQGBRCGUzAgSaC+QeeZruVU8XS+nf/gjMIrAXWPRelyHKbSFn7wm9jWYZf/pBgAABAgRCCwgsQpdH4wgsJZD7NXXDePMX3qUKoLNdBCIGFgmi920hufcD7wNdhquDEiBAgACBdwUEFu96OxoBAscCudtCPl/pYsVImklgbybBf3aeTdR7lkUusPD9ZaYzQF8IECBAgMCBgA98Q4MAgUgCZ7/qfrdTaBGpctryRGBv3PcOLFJ/zm7JaP39we0gT0aU1xIgQIAAgUkEWn/hmIRJNwgQeEngyiyL1CShxUuFcZimAnuBRYQ1Gs7Ox9bnnqeDNB1ydk6AAAECBMYQEFiMUSetJLCKwNXAQmixysiYu597tz+0DgRKRHPnY6vvELnbQSLMPinxsw0BAgQIECDwUKDVl42HzfJyAgQWFji6LST94pwuoPb+PEFk4QEzQdejLryZaHssvpkLLHx3mWDQ6wIBAgQIECgR8KFfomQbAgTeFDj6VTeFEn//c1/9z5PGRPhV+k0rx5pD4GjMR/iM7rH45llIEuFWmTlGnV4QIECAAIEBBCJ8GRqASRMJEHhZ4Oj+9fSelfv1VWjxcrEcropAxCeFbB17e5aFwKLKkLITAgQIECAwvoDAYvwa6gGBGQWOLli2e9dz99YLLWYcFXP3aW/MRxnHb8+yOAssopjMPRr1jgABAgQIBBEQWAQphGYQIPCbwNEsis/p4EILg2YmgahPCtmMz57aUXsRTE8ImWlk6wsBAgQIEHggILB4gOelBAg0EzgLI77ft85uEfFrbLMS2XFlgb1xHGm9hrPzrHY7BRaVB5fdESBAgACBUQUEFqNWTrsJzC9wdNGy92vumxdT88vf7+H3U1zShay/MoHIC29uPXhrloXAomzM2IoAAQIECEwvILCYvsQ6SGBYgaP72I9mTQgt3i91ushO//x18sjZ1CozXfK1OQosat9ukW/J8RZvnWMCiydV8loCBAgQIDCRgMBiomLqCoHJBM4eb5ou4vb+ck8Q8Z5XZ5BsQcXZI2b3jiS4OPeP/KSQreVvzLIQWNQ5T+2FAAECBAgML+DL+/Al1AECUwucPd70qOO5xTgj/WI9YvFyoVCuT+k2kb//2Sjtx9/vApGfFLK1tPUTPJy/zgoCBAgQIEDgvwUEFgYDAQKRBXKPNxVavFu93MXkldaYbfGnVvQnhWwtPpsB8fR7RW6MCRyvnGW2JUCAAAECgws8/WIxePc1nwCB4AJ3A4u3fg0Ozle1eU9nVuw1Rmjxu8qRcbTP6pZP5smNs2gWVU8yOyNAgAABAgR+F/DBb0QQIBBZ4M46Ft/9aT2FPbJfzbad/ar+5Di1H4n5pC29XzvCwpvJKDcL4sl3C4FF71Ho+AQIECBAIJDAky8VgbqhKQQITCpQI7BINC1/EZ6U/rdu5S4inxqYafFvwVECi9w59SSEyo0131uenm1eT4AAAQIEBhLwwT9QsTSVwKICdxbe3KMSWtwfQK1mV3y26MlF7v2exXvlnnVEm1azLM5mREV0iDeCtIgAAQIECEwkILCYqJi6QmBSgaOL5TuL7wktrg+S3C/e36FDegJI+vvr14yBK0c00+LHj1EW3kx1PRsbd8MFt3BdOWNsS4AAAQIEJhcQWExeYN0jMIHA04U3vwmEFtcGRUlgcRQ0lLz2uzV3L3Sv9Sru1qMsvJkEW8yyOJvNI9CKO261jAABAgQINBEQWDRhtVMCBCoKHAUWTy5ezi60nuy3YrfD7Cp3O0hJwHA1uFi5BiOtY5EGac1ZFrkA5M6sqjAnkoYQIECAAAEC1wUEFtfNvIIAgXcFji6IalzUtghD3tVpe7TcBWQ6emkdzqb67/WiJAhp2/s+ex8tsEhKZ6HW1ZCh5r76VNBRCRAgQIAAgWoCAotqlHZEgEAjgVpPCtlrXtr3z4O1FkovxBt1O8RuS2ZGXPkcKdnfZ8dXDS1GWXhzq9VZsHWlhjVna4Q4gTSCAAECBAgQeCZw5YvmsyN5NQECBO4JtAwsthYd/aq7emiRmxVx5WJ0s74aWqxYg5EW3tzqejZWSmdZ5MaG7yz33kO9igABAgQIDCvgw3/Y0mk4gaUE3vjF2e0hfw6p3PoVd8OE3IXpd0vuHmfUk+RoLEb+zK4xy8IMi1FHrHYTIECAAIFGApG//DTqst0SIDCgwBuBRWJpuV7GaOwl61eU/nK+13ehxfGIGHEdi9Sbs1kWJbNxnr5+tHNMewkQIECAAIGMgMDCECFAYASBo1/6W7yHmWnx7xFREig89S85xuf4XGWmxVFgEb3/T2dZCCxGeDfWRgIECBAg8KLA0y+bLzbVoQgQWFjgzcDi7GL9yYyC0cqXW7+i1sXz1dBilRq8Nauo9rh8EjqcvbbWeKvdX/sjQIAAAQIEGgoILBri2jUBAtUE3g4szkKLVS6c3goszqz3BlDJrQXVBl7HHY24jkXiyt1KdBY4PQk7OpbKoQkQIECAAIFWAgKLVrL2S4BATYGjC5nWv7Yf/frf+rg17e7uK7fgZm2DXEDy2Y8VQoteY/7uePl83d1bQ8ywqKFvHwQIECBAYCIBgcVExdQVAhML9Py1edWFOHOBRYvPjyuhxewzXUYPy87Gz1HYZYbFxG/iukaAAAECBO4ItPjCeacdXkOAAIEzgd6/No9+8XhndOUCi9ozLFIb0y/zP3/9u6TNLdpQctw3thl14c3N5s6tIQKLN0aWYxAgQIAAgYEEBBYDFUtTCSws0DuwSPSrzbTIzXZoFRbkLnQ/T4PZbw0ZdeHNrUZXA4ir2y/8lqjrBAgQIEBgDQGBxRp11ksCowtECCzOQotWF+8969YrsEh9vhJazHxrSM9boWqMvVwdvwMngUUNdfsgQIAAAQITCQgsJiqmrhCYWCBKYLFdTO/dtjDbhXPPwOIsHNob5rN+lkUa93ffXnKhxed5c3Yb0mzn111PryNAgAABAksJzPolb6ki6iyBBQSiXbitsKZF78AiDetcG7ahP+utIUcX+6PN6MnVMdVvCwOP3s4EFgu80esiAQIECBD4FhBYGBMECIwgEC2w2C6uZp5pkbvIfOvzI7f45zZ+R7uILznvjgKLEQOa3HjKeQgsckL+OwECBAgQmFDgrS+cE9LpEgECLwpEDCxS92eeaZELCt76/MjdUrANwxEv4ktOodEX3tz6WFrHI5O3xltJTWxDgAABAgQIvCTgC8BL0A5DgMAjgcjBwNGjOEf+RfjI+7OIb35+lLQntW1k86MTZPSFNz/7VVrHb4sZ6/roDdGLCRAgQIDAKgJvfuFcxVQ/CRCoLxB1hsXW01nWGtj6k7uw7HEBWXJLwYyzLKKP/atne25s7e1vxtt9rrrZngABAgQILCkgsFiy7DpNYDiByDMsPi/y//r1SM5P4B4X908LnLuo7NGn0lsKerTtqffZ60cY+1f7nxtfo58/Vz1sT4AAAQIECBwICCwMDQIERhAY5VfmWWZa5Nav6PWLd8ksizSee7Wvxbk008Kbnz6pX9vtVEdus4VPLcaHfRIgQIAAgakFBBZTl1fnCEwjMNKvzKmto8+0OAssel9E5sKUNOhnuzVkloU3996Q0vmS/tLeknM5AAAgAElEQVQTd7bapX+ncbY97nSaNzIdIUCAAAECBK4JCCyuedmaAIE+AiMFFkno6Ffx3hf7JdXL3XrRuw+59m19nGmWxUwLb5aMQdsQIECAAAECBP4lILAwEAgQGEFglFtCPi1HfXpIbn2BCJ8bJbeGzDTLYsTxP8L7ijYSIECAAAECwQUifPEMTqR5BAj8EkgX4Okv/Tvd8vD3r3/ngNJ233/pYvLKdO/RZlhs/R1xpkXk20E+XdMtBNuYPBqDs8yyGHX8594b/HcCBAgQIECAwKmAwMIAIUDgSOBzQbzcheFTxXSbwfa33dP+uc+jX5h7355Q2u+99kecAZCbXRHJO9fWrTYzfM7NuvBm6fljOwIECBAgQGBRgRm+yC1aOt0m0ETg6DaGJgfL7HQLMdKF/dHTBEb5Bf1spsXV2SatalESAET7zFjl1hCBRatRb78ECBAgQIBAaIFoXz5DY2kcgYkFSi5WI3Z/lMBis4s406I0pIo0u2LzLF2Ac4bPOgtvRnwH0iYCBAgQIECgqcAMX+KaAtk5gckFRg0qPsuyrYWR1srY/veV9THeLnGU9QhKg4rNJ+rnRckYjnj7zdVxJ7C4KmZ7AgQIECBAYHiBqF9Ah4fVAQIDCJRc6A3QjWwTvwONdKHeO9jouSbH1aAiAUecXfFZ+JJbQ0abjfM9sD0pJHuq24AAAQIECBCYTUBgMVtF9YdAmcAqYUWJxrZGRvr35xNN9hb/LNlf6TZHNWgZDpRc2H+3v2V7Sq1y261wa8hRH0cPYnK19d8JECBAgACBhQUEFgsXX9eXFrgaWBzdYvH5aNPtf3//O0G3fspIy2J+ztDYjlNrocw3Qos7Myq2fo4QVmxtLRnTo98asvfI2dH71PLctW8CBAgQIEBgcAGBxeAF1HwCNwVyv7Rvsw1azTJIF9GfIcZfA4ca3zMzrt5uUjO02EzTv5+YpqCiVihzc4heflnpLIuRZyQILC4PCy8gQIAAAQIERhYQWIxcPW0ncF/gLLDo/YvtTGHGd4U+Z2ukQGGbjXI0AyUFB5/bfe5vW4uj5uyV1kHV/RFb9srZZ1lYeLNsHNiKAAECBAgQmERAYDFJIXWDwEWBkgu7SL9EH7X381aVmhfuFzmH3zw5brMqRu9MbvZQ6l+ksX3F28KbV7RsS4AAAQIECAwvILAYvoQ6QOC2QMmFXZQ1DO7cNvF5e8SGlGYrpD/hxv88KWWWoGKrcemtISN+/kV5JO7tNx0vJECAAAECBAhcERjxC9uV/tmWAIFjgdILuwihxZ3AorT23+s+fF74lu5jpO1mmk1x5D5SGHdl7Byds71v47rSB9sSIECAAAECBIoFBBbFVDYkMK3ACBd3PX9Z/lx49MlClj0H0BZSpDYcPfGlZ/tqH7s0jBvx1hALb9YeLfZHgAABAgQIhBUQWIQtjYYReFWgZE2L1KBesy1azrC4C/19y8l2u0na31u3nKTwYVt887MfaTHPq08ruesQ9XUlY3rEmQkCi6gjTrsIECBAgACB6gICi+qkdkhgWIGSC7xeoUXEwOJqoXNP9djCh8/9pv/f0QyYXuHR1X733L5k9tBosywsvNlzRDk2AQIECBAg8KqAwOJVbgcjEF6gNLR4O7joeUtIhKLNENj0cCy5NWS0WRYebdpjJDkmAQIECBAg0EVAYNGF3UEJhBZIF3k/L9zW8MYv/S7Yf/xgcO+0KZll8cYYvtf6P191FMKM1IdaFvZDgAABAgQITC4gsJi8wLpH4IHAldkW6VfqtG7C5wKVDw79x0tdrP+bZPWZJnfG1GyzLAQWd0aB1xAgQIAAAQJDCggshiybRhN4TaDkYu+zMa2Ci6ML9dGm89conPDmuuJssywsvHl9DHgFAQIECBAgMKCAwGLAomkygQ4CV2ZbbM2rOUXdwpO/F11ocf0k2LvI/97LKAtwHvXFZ/r1ceEVBAgQIECAQGABX24CF0fTCAQU6BVcmGHx52AQWlw7QUrG7igzdiy8ea32tiZAgAABAgQGFRBYDFo4zSbQUSBd+P11YVHOzxkX6YIw/XP17+gCbZQLzKv9Ld3emhalUv/ebpZZFup+re62JkCAAAECBAYVEFgMWjjNJhBA4G5wcWedC7MJjgvu4rX8ZChZk2WEEMzCm+U1tyUBAgQIECAwsIDAYuDiaTqBIAIlU+2PmprWuSiZdWGGxXmx93xGuPDuMYRLFuCMvpbFUWCh5j1GlGMSIECAAAECzQQEFs1o7ZjAcgJ3Z1wkqBRcpL+jx6JadDM/nPaMai58mm/BGFvMMsvCk0LGGG9aSYAAAQIECDwQEFg8wPNSAgR2BbbQ4edNn71ZF2ZY5DHThXgyT//+/Is+WyDfs/pbzDDLwqya+uPCHgkQIECAAIFgAgKLYAXRHAKTCTy9XSRxpH2YYVE2MI5mDwgtfvebYZaFtUvKzglbESBAgAABAgMLCCwGLp6mExhI4ElwcdZNtzz8qbNnbW2DP51Gn2UhsBjoDVBTCRAgQIAAgXsCAot7bl5FgMB9gZrhhcBivw7Ws8iPz9FnWZhNk6+xLQgQIECAAIHBBQQWgxdQ8wkMLPB0rYvUdYHF8QDYCy3cGvK71+izLCy8OfAboKYTIECAAAECeQGBRd7IFgQItBV4GlyUPhq1bS/i7X3vF3i3hvxep9FnWQgs4p13WkSAAAECBAhUFBBYVMS0KwIEHgs8CS/Sxfjf/7Tg6NGojxs34A7cGpIv2sizLDwpJF9fWxAgQIAAAQIDCwgsBi6ephOYWCD98r09pvNON90q8j9qe7/Ce+//H5+RZ1lYePPOu4PXECBAgAABAsMI+NI6TKk0lMCyAiW/gB/huF3k3zNOfn4BCXR+BykZYxHX/zhqt8/2Zd8udZwAAQIECMwl4EvNXPXUGwIzCtR4qsjqt4uYZZE/M/aMPl8Vcf0PTwrJ19UWBAgQIECAwMACAouBi6fpBBYRKPn1+wrFirML9i5sV3Q4Gycl4yzaZ+ZRYBExXLlyjtqWAAECBAgQIPAvgWhfvpSFAAEC3wJnv3yni+6/fq13cVVutQv27wtyF7W/j5gR17I4avNqY/vquW97AgQIECBAYBABgcUghdJMAgsLHAUWnxfcTxbpXOXibu/WmojrMvQc6iPOsrCORc8R49gECBAgQIBAUwGBRVNeOydA4KHA2a/eR0HD3TUvVljn4jv8McvizwGaW8siWsB1FFgIox6++Xg5AQIECBAg0F9AYNG/BlpAgMCxwFn4kLtwTK91u8jvtnsXty5sfzfKBV7RQh6BhXdQAgQIECBAYFoBgcW0pdUxAlMInF08ll5oPw0uEmTaxwx/Ft/MV7FkLYvSsZc/2vMtLLz53NAeCBAgQIAAgaACAoughdEsAgT+JXA2Pf/q+9cWOvy8YTvT7SJuC8kPgNxaFpFmWRyFepHamBe3BQECBAgQIEBgR+DqF36IBAgQeEvg7JfuJxdjT4KL1Pd0K0r6G3XWhdtC8iN4pFkWZljk62kLAgQIECBAYFABgcWghdNsAgsIPFm/ooTnyZNFtv2PGF7sXeBGusWhpHZvbDPSLIujmUjq+sZIcQwCBAgQIECgmYDAohmtHRMg8FCgdWDx2bzcQoulXdkCjDQDJP0T9c9tIWWVyT0xJEogcNTO3MK0ZQq2IkCAAAECBAh0EhBYdIJ3WAIEsgI116/IHuzXBrWCi+1429oXW3gRJcT4tnVhuz9CcuPhya1JpWOyZLuj2SBR2lfSB9sQIECAAAECBP4QEFgYFAQIRBXoEVhsFk+eLFLiuc3ESNu+ORsj3Q6SFh1N//78E1jsV22UtSzOghWf8yVnpG0IECBAgACBkAK+yIQsi0YRWF7gzdtBzrBrrHNRWsxt9sXfHy+otbDnUVCxHcpnwXGVRphlUePxv6Xj1HYECBAgQIAAgdcEfEl9jdqBCBC4IBAlsPhs8tOni1zo/u6me4FG+v+lMCK1bQtXtv/fX7/28j2b4nvnZlecV2aEWRZnbYyyzsbT8e/1BAgQIECAwIICAosFi67LBAYQ6Hk7SAnPFg6kUCAXCJTsr9c2wooy+eizLM4CCzUuq7GtCBAgQIAAgYACAouARdEkAosLnF0cRl1EcJt9MVKA4UL22okW/YkhFt68Vk9bEyBAgAABAgMICCwGKJImElhMIOLtIFdLsM3ASK9Li1xG+kuhTworojyxJJLNWVtyt4b0DtOOAovUJ5/1o4wy7SRAgAABAgR+E/AlxoAgQCCawNkv2aPfj/+5iOZbszG2YEJI8Xykn4UCae89x+dZ23q267m6PRAgQIAAAQLLCggsli29jhMIKZD7FXvm96zvJ4KkQCM9MWRbPPOzYMkpBRGf/33739tTRragwkyKekM9Nz57zrKYYWZSvUrZEwECBAgQIDCFwMxf/qcokE4QWEzARddiBR+wu7lZFr0+V8/ClJ5ByoAl1mQCBAgQIEAgikCvL1ZR+q8dBAjEEji7GLRIZKxardqayLMsoj9dZ9Uxo98ECBAgQIDATQGBxU04LyNAoImAC64mrHZaWeAsWOs5m2Hm9V8ql9DuCBAgQIAAgREEBBYjVEkbCawh4HaQNeo8Sy/PwoFes4EsvDnL6NIPAgQIECBA4F8CAgsDgQCBKAICiyiV0I4SgYhrWUSd+VHiaRsCBAgQIECAwB8CAguDggCBKAJuB4lSCe0oFYi25oqFN0srZzsCBAgQIEBgCAGBxRBl0kgCSwgcBRY91wRYAl4nbwuczQpKO/3PX4+fvX2Aiy/MLQj6dnsuNt/mBAgQIECAAIHfBQQWRgQBAhEE3A4SoQracEcg2iwLC2/eqaLXECBAgAABAiEFBBYhy6JRBJYTsFjgciWfpsPRZjVYx2KaoaUjBAgQIECAgMDCGCBAIIKA9SsiVEEb7gpEmmUhsLhbRa8jQIAAAQIEwgkILMKVRIMILCfgdpDlSj5lh6PcipGb8eFzf8rhp1MECBAgQGBOAV9c5qyrXhEYSeAssLBI4EiVXLutUWY25AIL59Ta41TvCRAgQIDAUAICi6HKpbEEphRwO8iUZV2uU7mg4M3P27Nz6r/+qUwKCf0RIECAAAECBMILvPkFKjyGBhIg8LqA20FeJ3fAhgJRZllEaUdDarsmQIAAAQIEVhAQWKxQZX0kEFdAYBG3Nlp2XSA3y+Kt2zHOAovUK5/912vrFQQIECBAgEAHAV9aOqA7JAEC/y3gdhCDYTaBsxDu//748SOFFq3/ztqQjv1WcNK6n/ZPgAABAgQITC4gsJi8wLpHILDA2a/Rb13YBebRtEEFIsyyiNCGQcun2QQIECBAgEAkAYFFpGpoC4G1BCL8Er2WuN6+JRDhVqez2UsCwbdGguMQIECAAAECjwQEFo/4vJjAY4H0S+jPf6Zo//1rTyut3u9JBo+Hjx0EFYgww0FgEXRwaBYBAgQIECBQLiCwKLeyJYHaAnu/wq7yyMEIF3S162l/BD4Fes8gsvCm8UiAAAECBAgMLyCwGL6EOjCwwN4voKtM1c4tCui9aeCBren/EugdyuXOMQtvGqgECBAgQIBAeAEXBeFLpIETCxxN2V7hQuLs199VZplMPLR17ZdAz3HeOzAxCAgQIECAAAECjwUEFo8J7YDAbYGji5nZZ1nkLqQEFreHlBcGE8iN9ZbhZO7Ys7/PBBsKmkOAAAECBAjcERBY3FHzGgL1BI5Ci5YXMvVaf29Puanq3pfuuXpVTIGesywsvBlzTGgVAQIECBAgUCjgwqAQymYEGgkcXczMPMtAYNFoMNltSIHcTIeWn8MW3gw5JDSKAAECBAgQKBVo+UWptA22I7CywNnFzKznp8eZrjzi1+z7WXDQcjZVLrBoeew1K63XBAgQIECAQFWBWS+IqiLZGYHGAivNssjNrph5ZknjYWT3gQXOgsmWa0nkzjeBReBBo2kECBAgQIDAjx8CC6OAQH+Bs4uK2c5Rv/j2H29a0EegxyyL3O0oAsI+Y8FRCRAgQIAAgUKB2S6GCrttMwLhBFZZfPPsdpBUFO9J4YamBlUSOAsmW82yyAUWrY5bicxuCBAgQIAAgdUFXBysPgL0P4rACo84zU1P92tvlNGoHa0EzgK7VrdneFJIq2raLwECBAgQINBcQGDRnNgBCBQJnP0S2upCpqhhFTfK3Q4isKiIbVchBc5Cu1bj36ymkENBowgQIECAAIESAYFFiZJtCLwjMPssi9yF0yzBzDujxVFGFOix+GYuKPQ9YMSRpM0ECBAgQGARAV9UFim0bg4hMPMjTnO3g7iXfoghqpEVBN5efDMXWAgKKxTVLggQIECAAIE2AgKLNq72SuCuwNEshNEvKnKzK1pNh79bB68j0Erg7VkWubBw9PeWVnWyXwIECBAgQCCAgMAiQBE0gcCHwIy3heSeVJC676LJabCSwJuLb+YCC2HhSiNPXwkQIECAwGACAovBCqa50wvMuPhm7oIpFdV70fRDWwc/BN6cZZELDAUWhiYBAgQIECAQVsBFQtjSaNjCArPNsnA7yMKDWdcPBd6aZZELLKwfY5ASIECAAAECYQUEFmFLo2ELC5wtkjfaOVsyu8LtIAsP9oW7fnZu1AwRBBYLDzJdJ0CAAAECowuMdvEzurf2EygReHO6eEl7nmyTm12R9u196Imw144s8MYsC4HFyCNE2wkQIECAwOICLhQWHwC6H1bg7UcftoAomV3h/vkW8vY5isBbsyzOgpGaszlGcddOAgQIECBAYBABgcUghdLM5QTOLmRGucg3u2K5YavDFwVysx9qneu5c9F3gYuFszkBAgQIECDwjoAvKe84OwqBqwK5C5no6z6YXXG14rZfVSB3rtT4nD6bsZXcaxxj1frpNwECBAgQINBQwJeUhrh2TeChwNlFRvRp3LlfdBNN9NDlYfm8nECxQOtzXWBRXAobEiBAgAABApEEBBaRqqEtBH4XGHWWRe4X49TLWlPdjRkCMwi0PtcFFjOMEn0gQIAAAQILCggsFiy6Lg8lMOKFRsnsCoHFUMNQY18QaDnLYsT3kRfIHYIAAQIECBCILiCwiF4h7VtdIDdbIdqFf669qZ7Rb2dZfczpfx+B3CyLJ+d6LrBwe1afmjsqAQIECBAgkBEQWBgiBOIL5GYsRLnYKAkrkvaTC6/41dJCAvcFcufQ3XM9F1j4LnC/Zl5JgAABAgQINBTwJaUhrl0TqCSQu9iIMmMhF6wIKyoNCLuZWqDFrSG595C7QcjUhdA5AgQIECBAoL+AwKJ/DbSAQIlALgzofcGR+2V466P3nJJq22ZlgdytIXcCylxg4bxcecTpOwECBAgQCCzgS0rg4mgagQ+B3AVH2rRXaFEaVrgVxJAmUCaQO6eunuu191fWC1sRIECAAAECBB4KCCweAno5gZcEcr+6pmbc+eW1RvNzsz+2Y3i/qaFtH6sI5ELKK6FFzX2t4q+fBAgQIECAQAABFxABiqAJBAoFchcdaTdXLmIKD3u6We6X2+3FZlfU0LaP1QTOwsArAWXuPPVdYLWRpb8ECBAgQGAQAV9SBimUZhL4ZwZFySyLBPXWeZ27CBJWGLYEngnkzrHS0CIXdr4ddD5T8WoCBAgQIEBgGYG3LmyWAdVRAo0Fchce6fClFzFPm+pWkKeCXk8gL5ALLUrChtz7hu8C+TrYggABAgQIEOgg4EtKB3SHJPBAIMosi9xF1NZFt4I8KLaXEvglkAsccqHF09crBAECBAgQIECgi4DAogu7gxJ4JFASFrScZVFy/NRBYcWjMnsxgd8EcutZpPMtnfd7f7nAwncBg40AAQIECBAIKeBLSsiyaBSBU4GesyxKj5064P3FQCZQT6AkKDyaaZG7fcu5Wq9O9kSAAAECBAhUFPAlpSKmXRF4UaDk4qXFLIvcL7UbgdkVLw4Gh1pGoOS83zv3cuet7wLLDCEdJUCAAAECYwn4kjJWvbSWwKdA7lfTtG3u3vYroiUXS2l/woorqrYlcE0gFz58hoYptEz/5G4nSe8T/ggQIECAAAEC4QQEFuFKokEEigVKAoRasyxKjrU13PtKcQltSOCWwJXzseQAztkSJdsQIECAAAECrwv4kvI6uQMSqCrwxiyLKxdH3lOqltfOCBwKlM60yBHWCjVzx/HfCRAgQIAAAQKXBVxcXCbzAgKhBErDhLu3hpTuP6G4FSTU0NCYBQSunJ9HHM7bBQaKLhIgQIAAgVEFBBajVk67CfyPQMksizuBwpWLIRc9RiSBPgJXztO9Fvoe0KdujkqAAAECBAgUCPiiUoBkEwLBBa48anQLLrbF+L67lvaV/vl5sc/eSy6C2ZxAZYE7wYWgsXIR7I4AAQIECBCoK+Aio66nvRHoJfDkfvYUXqSQ4s5fem266En/9keAQH+BFFykv7PQ0Xnbv05aQIAAAQIECBQICCwKkGxCYACBq7MsanXp7toYtY5vPwQI7AtsIWT691+/Nvn7V7goYDRqCBAgQIAAgSEEBBZDlEkjCRQJvB1aCCuKymIjAgQIECBAgAABAgTuCAgs7qh5DYG4AnfuY7/TG/e+31HzGgIECBAgQIAAAQIEigUEFsVUNiQwjEDL0MK978MMAw0lQIAAAQIECBAgMLaAwGLs+mk9gTOB2sGFWRXGGwECBAgQIECAAAECrwkILF6jdiACXQTuPqb0s7FmVXQpnYMSIECAAAECBAgQWFtAYLF2/fV+LYGSxx1uIimkSE8U2F6zlpTeEiBAgAABAgQIECDQXUBg0b0EGkCgm8DnYw8/H3PokYfdSuLABAgQIECAAAECBAhsAgILY4EAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIXVqaAAAAbSSURBVIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBP4/csBUQgAdQDwAAAAASUVORK5CYII=",
+ "termsOfCarriage": "All shipments are subject to the Hague-Visby Rules. The carrier assumes liability only for loss or damage due to its own negligence. Responsibility ceases at the time goods are delivered to the consignee or their agent. Claims must be submitted within 7 working days of delivery.",
+ "attachments": [
+ {
+ "data": "JVBERi0xLjYNJeLjz9MNCjI0IDAgb2JqDTw8L0ZpbHRlci9GbGF0ZURlY29kZS9GaXJzdCA0L0xlbmd0aCAyMTYvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjePI9RS8MwFIX/yn1bi9jepCQ6GYNpFBTEMsW97CVLbjWYNpImmz/fVsXXcw/f/c4SEFarepPTe4iFok8dU09DgtDBQx6TMwT74vaLTE7uSPDUdXM0Xe/73r1FnVwYYEtHR6d9WdY3kX4ipRMV6oojSmxQMoGyac5RLBAXf63p38aGA7XPorLewyvFcYaJile8rB+D/YcwiRdMMGScszO8/IW0MdhsaKKYGA46gXKTr/cUQVY4We/cYMNpnLVeXPJUXHs9fECr7kAFk+eZ5Xr9LcAAfKpQrA0KZW5kc3RyZWFtDWVuZG9iag0yNSAwIG9iag08PC9GaWx0ZXIvRmxhdGVEZWNvZGUvRmlyc3QgNC9MZW5ndGggNDkvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjeslAwULCx0XfOL80rUTDU985MKY42NAIKBsXqh1QWpOoHJKanFtvZAQQYAN/6C60NCmVuZHN0cmVhbQ1lbmRvYmoNMjYgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0ZpcnN0IDkvTGVuZ3RoIDQyL04gMi9UeXBlL09ialN0bT4+c3RyZWFtDQpo3jJTMFAwVzC0ULCx0fcrzS2OBnENFIJi7eyAIsH6LnZ2AAEGAI2FCDcNCmVuZHN0cmVhbQ1lbmRvYmoNMjcgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0ZpcnN0IDUvTGVuZ3RoIDEyMC9OIDEvVHlwZS9PYmpTdG0+PnN0cmVhbQ0KaN4yNFIwULCx0XfOzytJzSspVjAyBgoE6TsX5Rc45VdEGwB5ZoZGCuaWRrH6vqkpmYkYogGJRUCdChZgfUGpxfmlRcmpxUAzA4ryk4NTS6L1A1zc9ENSK0pi7ez0g/JLEktSFQz0QyoLUoF601Pt7AACDADYoCeWDQplbmRzdHJlYW0NZW5kb2JqDTIgMCBvYmoNPDwvTGVuZ3RoIDM1MjUvU3VidHlwZS9YTUwvVHlwZS9NZXRhZGF0YT4+c3RyZWFtDQo8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjQtYzAwNSA3OC4xNDczMjYsIDIwMTIvMDgvMjMtMTM6MDM6MDMgICAgICAgICI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnBkZj0iaHR0cDovL25zLmFkb2JlLmNvbS9wZGYvMS4zLyIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIj4KICAgICAgICAgPHBkZjpQcm9kdWNlcj5BY3JvYmF0IERpc3RpbGxlciA2LjAgKFdpbmRvd3MpPC9wZGY6UHJvZHVjZXI+CiAgICAgICAgIDx4bXA6Q3JlYXRlRGF0ZT4yMDA2LTAzLTA2VDE1OjA2OjMzLTA1OjAwPC94bXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZVBTNS5kbGwgVmVyc2lvbiA1LjIuMjwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxNi0wNy0xNVQxMDoxMjoyMSswODowMDwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6TWV0YWRhdGFEYXRlPjIwMTYtMDctMTVUMTA6MTI6MjErMDg6MDA8L3htcDpNZXRhZGF0YURhdGU+CiAgICAgICAgIDx4bXBNTTpEb2N1bWVudElEPnV1aWQ6ZmYzZGNmZDEtMjNmYS00NzZmLTgzOWEtM2U1Y2FlMmRhMmViPC94bXBNTTpEb2N1bWVudElEPgogICAgICAgICA8eG1wTU06SW5zdGFuY2VJRD51dWlkOjM1OTM1MGIzLWFmNDAtNGQ4YS05ZDZjLTAzMTg2YjRmZmIzNjwveG1wTU06SW5zdGFuY2VJRD4KICAgICAgICAgPGRjOmZvcm1hdD5hcHBsaWNhdGlvbi9wZGY8L2RjOmZvcm1hdD4KICAgICAgICAgPGRjOnRpdGxlPgogICAgICAgICAgICA8cmRmOkFsdD4KICAgICAgICAgICAgICAgPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5CbGFuayBQREYgRG9jdW1lbnQ8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6QWx0PgogICAgICAgICA8L2RjOnRpdGxlPgogICAgICAgICA8ZGM6Y3JlYXRvcj4KICAgICAgICAgICAgPHJkZjpTZXE+CiAgICAgICAgICAgICAgIDxyZGY6bGk+RGVwYXJ0bWVudCBvZiBKdXN0aWNlIChFeGVjdXRpdmUgT2ZmaWNlIG9mIEltbWlncmF0aW9uIFJldmlldyk8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6U2VxPgogICAgICAgICA8L2RjOmNyZWF0b3I+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz4NCmVuZHN0cmVhbQ1lbmRvYmoNMTEgMCBvYmoNPDwvTWV0YWRhdGEgMiAwIFIvUGFnZUxhYmVscyA2IDAgUi9QYWdlcyA4IDAgUi9UeXBlL0NhdGFsb2c+Pg1lbmRvYmoNMjMgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0xlbmd0aCAxMD4+c3RyZWFtDQpIiQIIMAAAAAABDQplbmRzdHJlYW0NZW5kb2JqDTI4IDAgb2JqDTw8L0RlY29kZVBhcm1zPDwvQ29sdW1ucyA0L1ByZWRpY3RvciAxMj4+L0ZpbHRlci9GbGF0ZURlY29kZS9JRFs8REI3Nzc1Q0NFMjI3RjZCMzBDNDQwREY0MjIxREMzOTA+PEJGQ0NDRjNGNTdGNjEzNEFCRDNDMDRBOUU0Q0ExMDZFPl0vSW5mbyA5IDAgUi9MZW5ndGggODAvUm9vdCAxMSAwIFIvU2l6ZSAyOS9UeXBlL1hSZWYvV1sxIDIgMV0+PnN0cmVhbQ0KaN5iYgACJjDByGzIwPT/73koF0wwMUiBWYxA4v9/EMHA9I/hBVCxoDOQeH8DxH2KrIMIglFwIpD1vh5IMJqBxPpArHYgwd/KABBgAP8bEC0NCmVuZHN0cmVhbQ1lbmRvYmoNc3RhcnR4cmVmDQo0NTc2DQolJUVPRg0K",
+ "filename": "blank.pdf",
+ "mimeType": "application/pdf"
+ },
+ {
+ "data": "e30=",
+ "filename": "empty.json",
+ "mimeType": "application/json"
+ },
+ {
+ "data": "JVBERi0xLjMNCiXi48/TDQoNCjEgMCBvYmoNCjw8DQovVHlwZSAvQ2F0YWxvZw0KL091dGxpbmVzIDIgMCBSDQovUGFnZXMgMyAwIFINCj4+DQplbmRvYmoNCg0KMiAwIG9iag0KPDwNCi9UeXBlIC9PdXRsaW5lcw0KL0NvdW50IDANCj4+DQplbmRvYmoNCg0KMyAwIG9iag0KPDwNCi9UeXBlIC9QYWdlcw0KL0NvdW50IDINCi9LaWRzIFsgNCAwIFIgNiAwIFIgXSANCj4+DQplbmRvYmoNCg0KNCAwIG9iag0KPDwNCi9UeXBlIC9QYWdlDQovUGFyZW50IDMgMCBSDQovUmVzb3VyY2VzIDw8DQovRm9udCA8PA0KL0YxIDkgMCBSIA0KPj4NCi9Qcm9jU2V0IDggMCBSDQo+Pg0KL01lZGlhQm94IFswIDAgNjEyLjAwMDAgNzkyLjAwMDBdDQovQ29udGVudHMgNSAwIFINCj4+DQplbmRvYmoNCg0KNSAwIG9iag0KPDwgL0xlbmd0aCAxMDc0ID4+DQpzdHJlYW0NCjIgSg0KQlQNCjAgMCAwIHJnDQovRjEgMDAyNyBUZg0KNTcuMzc1MCA3MjIuMjgwMCBUZA0KKCBBIFNpbXBsZSBQREYgRmlsZSApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY4OC42MDgwIFRkDQooIFRoaXMgaXMgYSBzbWFsbCBkZW1vbnN0cmF0aW9uIC5wZGYgZmlsZSAtICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjY0LjcwNDAgVGQNCigganVzdCBmb3IgdXNlIGluIHRoZSBWaXJ0dWFsIE1lY2hhbmljcyB0dXRvcmlhbHMuIE1vcmUgdGV4dC4gQW5kIG1vcmUgKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NTIuNzUyMCBUZA0KKCB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDYyOC44NDgwIFRkDQooIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjE2Ljg5NjAgVGQNCiggdGV4dC4gQW5kIG1vcmUgdGV4dC4gQm9yaW5nLCB6enp6ei4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjA0Ljk0NDAgVGQNCiggbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDU5Mi45OTIwIFRkDQooIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNTY5LjA4ODAgVGQNCiggQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA1NTcuMTM2MCBUZA0KKCB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBFdmVuIG1vcmUuIENvbnRpbnVlZCBvbiBwYWdlIDIgLi4uKSBUag0KRVQNCmVuZHN0cmVhbQ0KZW5kb2JqDQoNCjYgMCBvYmoNCjw8DQovVHlwZSAvUGFnZQ0KL1BhcmVudCAzIDAgUg0KL1Jlc291cmNlcyA8PA0KL0ZvbnQgPDwNCi9GMSA5IDAgUiANCj4+DQovUHJvY1NldCA4IDAgUg0KPj4NCi9NZWRpYUJveCBbMCAwIDYxMi4wMDAwIDc5Mi4wMDAwXQ0KL0NvbnRlbnRzIDcgMCBSDQo+Pg0KZW5kb2JqDQoNCjcgMCBvYmoNCjw8IC9MZW5ndGggNjc2ID4+DQpzdHJlYW0NCjIgSg0KQlQNCjAgMCAwIHJnDQovRjEgMDAyNyBUZg0KNTcuMzc1MCA3MjIuMjgwMCBUZA0KKCBTaW1wbGUgUERGIEZpbGUgMiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY4OC42MDgwIFRkDQooIC4uLmNvbnRpbnVlZCBmcm9tIHBhZ2UgMS4gWWV0IG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NzYuNjU2MCBUZA0KKCBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY2NC43MDQwIFRkDQooIHRleHQuIE9oLCBob3cgYm9yaW5nIHR5cGluZyB0aGlzIHN0dWZmLiBCdXQgbm90IGFzIGJvcmluZyBhcyB3YXRjaGluZyApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY1Mi43NTIwIFRkDQooIHBhaW50IGRyeS4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NDAuODAwMCBUZA0KKCBCb3JpbmcuICBNb3JlLCBhIGxpdHRsZSBtb3JlIHRleHQuIFRoZSBlbmQsIGFuZCBqdXN0IGFzIHdlbGwuICkgVGoNCkVUDQplbmRzdHJlYW0NCmVuZG9iag0KDQo4IDAgb2JqDQpbL1BERiAvVGV4dF0NCmVuZG9iag0KDQo5IDAgb2JqDQo8PA0KL1R5cGUgL0ZvbnQNCi9TdWJ0eXBlIC9UeXBlMQ0KL05hbWUgL0YxDQovQmFzZUZvbnQgL0hlbHZldGljYQ0KL0VuY29kaW5nIC9XaW5BbnNpRW5jb2RpbmcNCj4+DQplbmRvYmoNCg0KMTAgMCBvYmoNCjw8DQovQ3JlYXRvciAoUmF2ZSBcKGh0dHA6Ly93d3cubmV2cm9uYS5jb20vcmF2ZVwpKQ0KL1Byb2R1Y2VyIChOZXZyb25hIERlc2lnbnMpDQovQ3JlYXRpb25EYXRlIChEOjIwMDYwMzAxMDcyODI2KQ0KPj4NCmVuZG9iag0KDQp4cmVmDQowIDExDQowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDAwMTkgMDAwMDAgbg0KMDAwMDAwMDA5MyAwMDAwMCBuDQowMDAwMDAwMTQ3IDAwMDAwIG4NCjAwMDAwMDAyMjIgMDAwMDAgbg0KMDAwMDAwMDM5MCAwMDAwMCBuDQowMDAwMDAxNTIyIDAwMDAwIG4NCjAwMDAwMDE2OTAgMDAwMDAgbg0KMDAwMDAwMjQyMyAwMDAwMCBuDQowMDAwMDAyNDU2IDAwMDAwIG4NCjAwMDAwMDI1NzQgMDAwMDAgbg0KDQp0cmFpbGVyDQo8PA0KL1NpemUgMTENCi9Sb290IDEgMCBSDQovSW5mbyAxMCAwIFINCj4+DQoNCnN0YXJ0eHJlZg0KMjcxNA0KJSVFT0YNCg==",
+ "filename": "sample.pdf",
+ "mimeType": "application/pdf"
+ },
+ {
+ "data": "JVBERi0xLjYNJeLjz9MNCjI0IDAgb2JqDTw8L0ZpbHRlci9GbGF0ZURlY29kZS9GaXJzdCA0L0xlbmd0aCAyMTYvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjePI9RS8MwFIX/yn1bi9jepCQ6GYNpFBTEMsW97CVLbjWYNpImmz/fVsXXcw/f/c4SEFarepPTe4iFok8dU09DgtDBQx6TMwT74vaLTE7uSPDUdXM0Xe/73r1FnVwYYEtHR6d9WdY3kX4ipRMV6oojSmxQMoGyac5RLBAXf63p38aGA7XPorLewyvFcYaJile8rB+D/YcwiRdMMGScszO8/IW0MdhsaKKYGA46gXKTr/cUQVY4We/cYMNpnLVeXPJUXHs9fECr7kAFk+eZ5Xr9LcAAfKpQrA0KZW5kc3RyZWFtDWVuZG9iag0yNSAwIG9iag08PC9GaWx0ZXIvRmxhdGVEZWNvZGUvRmlyc3QgNC9MZW5ndGggNDkvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjeslAwULCx0XfOL80rUTDU985MKY42NAIKBsXqh1QWpOoHJKanFtvZAQQYAN/6C60NCmVuZHN0cmVhbQ1lbmRvYmoNMjYgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0ZpcnN0IDkvTGVuZ3RoIDQyL04gMi9UeXBlL09ialN0bT4+c3RyZWFtDQpo3jJTMFAwVzC0ULCx0fcrzS2OBnENFIJi7eyAIsH6LnZ2AAEGAI2FCDcNCmVuZHN0cmVhbQ1lbmRvYmoNMjcgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0ZpcnN0IDUvTGVuZ3RoIDEyMC9OIDEvVHlwZS9PYmpTdG0+PnN0cmVhbQ0KaN4yNFIwULCx0XfOzytJzSspVjAyBgoE6TsX5Rc45VdEGwB5ZoZGCuaWRrH6vqkpmYkYogGJRUCdChZgfUGpxfmlRcmpxUAzA4ryk4NTS6L1A1zc9ENSK0pi7ez0g/JLEktSFQz0QyoLUoF601Pt7AACDADYoCeWDQplbmRzdHJlYW0NZW5kb2JqDTIgMCBvYmoNPDwvTGVuZ3RoIDM1MjUvU3VidHlwZS9YTUwvVHlwZS9NZXRhZGF0YT4+c3RyZWFtDQo8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjQtYzAwNSA3OC4xNDczMjYsIDIwMTIvMDgvMjMtMTM6MDM6MDMgICAgICAgICI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnBkZj0iaHR0cDovL25zLmFkb2JlLmNvbS9wZGYvMS4zLyIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIj4KICAgICAgICAgPHBkZjpQcm9kdWNlcj5BY3JvYmF0IERpc3RpbGxlciA2LjAgKFdpbmRvd3MpPC9wZGY6UHJvZHVjZXI+CiAgICAgICAgIDx4bXA6Q3JlYXRlRGF0ZT4yMDA2LTAzLTA2VDE1OjA2OjMzLTA1OjAwPC94bXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZVBTNS5kbGwgVmVyc2lvbiA1LjIuMjwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxNi0wNy0xNVQxMDoxMjoyMSswODowMDwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6TWV0YWRhdGFEYXRlPjIwMTYtMDctMTVUMTA6MTI6MjErMDg6MDA8L3htcDpNZXRhZGF0YURhdGU+CiAgICAgICAgIDx4bXBNTTpEb2N1bWVudElEPnV1aWQ6ZmYzZGNmZDEtMjNmYS00NzZmLTgzOWEtM2U1Y2FlMmRhMmViPC94bXBNTTpEb2N1bWVudElEPgogICAgICAgICA8eG1wTU06SW5zdGFuY2VJRD51dWlkOjM1OTM1MGIzLWFmNDAtNGQ4YS05ZDZjLTAzMTg2YjRmZmIzNjwveG1wTU06SW5zdGFuY2VJRD4KICAgICAgICAgPGRjOmZvcm1hdD5hcHBsaWNhdGlvbi9wZGY8L2RjOmZvcm1hdD4KICAgICAgICAgPGRjOnRpdGxlPgogICAgICAgICAgICA8cmRmOkFsdD4KICAgICAgICAgICAgICAgPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5CbGFuayBQREYgRG9jdW1lbnQ8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6QWx0PgogICAgICAgICA8L2RjOnRpdGxlPgogICAgICAgICA8ZGM6Y3JlYXRvcj4KICAgICAgICAgICAgPHJkZjpTZXE+CiAgICAgICAgICAgICAgIDxyZGY6bGk+RGVwYXJ0bWVudCBvZiBKdXN0aWNlIChFeGVjdXRpdmUgT2ZmaWNlIG9mIEltbWlncmF0aW9uIFJldmlldyk8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6U2VxPgogICAgICAgICA8L2RjOmNyZWF0b3I+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz4NCmVuZHN0cmVhbQ1lbmRvYmoNMTEgMCBvYmoNPDwvTWV0YWRhdGEgMiAwIFIvUGFnZUxhYmVscyA2IDAgUi9QYWdlcyA4IDAgUi9UeXBlL0NhdGFsb2c+Pg1lbmRvYmoNMjMgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0xlbmd0aCAxMD4+c3RyZWFtDQpIiQIIMAAAAAABDQplbmRzdHJlYW0NZW5kb2JqDTI4IDAgb2JqDTw8L0RlY29kZVBhcm1zPDwvQ29sdW1ucyA0L1ByZWRpY3RvciAxMj4+L0ZpbHRlci9GbGF0ZURlY29kZS9JRFs8REI3Nzc1Q0NFMjI3RjZCMzBDNDQwREY0MjIxREMzOTA+PEJGQ0NDRjNGNTdGNjEzNEFCRDNDMDRBOUU0Q0ExMDZFPl0vSW5mbyA5IDAgUi9MZW5ndGggODAvUm9vdCAxMSAwIFIvU2l6ZSAyOS9UeXBlL1hSZWYvV1sxIDIgMV0+PnN0cmVhbQ0KaN5iYgACJjDByGzIwPT/73koF0wwMUiBWYxA4v9/EMHA9I/hBVCxoDOQeH8DxH2KrIMIglFwIpD1vh5IMJqBxPpArHYgwd/KABBgAP8bEC0NCmVuZHN0cmVhbQ1lbmRvYmoNc3RhcnR4cmVmDQo0NTc2DQolJUVPRg0K",
+ "filename": "veryverylongfilenameoverhereveryverylongfilenameoverhere.pdf",
+ "mimeType": "application/pdf"
+ }
+ ]
+ },
+ "type": ["VerifiableCredential"],
+ "qrCode": {
+ "type": "TrustVCQRCode",
+ "uri": "https://actions.tradetrust.io?q=%7B%22type%22%3A%22DOCUMENT%22%2C%22payload%22%3A%7B%22uri%22%3A%22https%3A%2F%2Fgallery.tradetrust.io%2Fstatic%2Fbill-of-lading-operative.json%22%2C%22redirect%22%3A%22https%3A%2F%2Fref.tradetrust.io%2F%22%2C%22chainId%22%3A%22101010%22%7D%7D"
+ },
+ "credentialStatus": {
+ "type": "TransferableRecords",
+ "tokenNetwork": { "chain": "FREE", "chainId": 101010 },
+ "tokenRegistry": "0x7202363bBDb126036F7C3243Ebac310d9d145040",
+ "tokenId": "9cb05e6ca63093f2a5d5ee3965fc63653442ed542c41c27a7e4dbb1d4521f93c"
+ },
+ "issuer": "did:web:trustvc.github.io:did:1",
+ "issuanceDate": "2025-06-05T11:29:32.860Z",
+ "id": "urn:bnid:_:0198cc65-36f5-7cca-a034-7a141d772a7d",
+ "proof": {
+ "type": "BbsBlsSignature2020",
+ "created": "2025-08-21T11:30:42Z",
+ "proofPurpose": "assertionMethod",
+ "proofValue": "i41csJtnGrZYhTJi/6Oe+i1CFev2jroBD1r9CE7Yc9MMEUPag8SUFEccvzdG+oxkTVi6gpUcPkkCBODbBNb7En+DKJjVkRzj29k3yt6vrb5lYE1Nq8yRJAwmWHqlhogOAAgXSIC4aZn5QKHAt0E4Hw==",
+ "verificationMethod": "did:web:trustvc.github.io:did:1#keys-1"
+ }
+}
diff --git a/src/__tests__/__fixtures__/w3c/bbs2020_w3c_verifiable_document_v1_1.json b/src/__tests__/__fixtures__/w3c/bbs2020_w3c_verifiable_document_v1_1.json
new file mode 100644
index 0000000..d5194bb
--- /dev/null
+++ b/src/__tests__/__fixtures__/w3c/bbs2020_w3c_verifiable_document_v1_1.json
@@ -0,0 +1,69 @@
+{
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "https://trustvc.io/context/invoice.json",
+ "https://trustvc.io/context/render-method-context.json",
+ "https://trustvc.io/context/qrcode-context.json",
+ "https://w3id.org/security/bbs/v1"
+ ],
+ "renderMethod": [
+ {
+ "type": "EMBEDDED_RENDERER",
+ "templateName": "INVOICE",
+ "id": "https://generic-templates.tradetrust.io"
+ }
+ ],
+ "credentialSubject": {
+ "type": ["Invoice"],
+ "billFromName": "ABC Exports Pvt. Ltd.",
+ "billFromStreetAddress": "12/F, Industrial Plaza, Near MIDC",
+ "billFromCity": "Navi Mumbai",
+ "billFromPostalCode": "400703",
+ "billFromPhoneNumber": "+91-22-4455-9988",
+ "billToName": "David Thomson",
+ "billToEmail": "david.thomson@example.co.uk",
+ "billToCompanyName": "XYZ Foods Ltd.",
+ "billToCompanyStreetAddress": "Unit 17, Royal Wharf, Docklands Industrial Area",
+ "billToCompanyCity": "London",
+ "billToCompanyPostalCode": "E16 2AA",
+ "billToCompanyPhoneNumber": "+44-20-8899-4455",
+ "billableItems": [
+ {
+ "billableItemsDescription": "Organic Basmati Rice (20kg Bags)",
+ "billableItemsQuantity": "100",
+ "billableItemsUnitPrice": "125",
+ "billableItemsAmount": "12500"
+ },
+ {
+ "billableItemsDescription": "Vacuum-Packed Almonds (10kg)",
+ "billableItemsQuantity": "50",
+ "billableItemsUnitPrice": "80",
+ "billableItemsAmount": "4000"
+ }
+ ],
+ "invoiceId": "INV-20250604-001",
+ "invoiceName": "Export of Organic Basmati Rice",
+ "date": "2025-06-04",
+ "customerId": "CUST-UK-55678",
+ "terms": "Net 30 Days",
+ "subtotal": "$16,500.00",
+ "tax": "5%",
+ "taxTotal": "$825.00",
+ "total": "$17,325.00"
+ },
+ "type": ["VerifiableCredential"],
+ "qrCode": {
+ "type": "TrustVCQRCode",
+ "uri": "https://actions.tradetrust.io?q=%7B%22type%22%3A%22DOCUMENT%22%2C%22payload%22%3A%7B%22uri%22%3A%22https%3A%2F%2Fgallery.tradetrust.io%2Fstatic%2Finvoice-default.json%22%2C%22redirect%22%3A%22https%3A%2F%2Fref.tradetrust.io%2F%22%2C%22chainId%22%3A%22101010%22%7D%7D"
+ },
+ "issuer": "did:web:trustvc.github.io:did:1",
+ "issuanceDate": "2025-06-09T09:36:15.971Z",
+ "id": "urn:bnid:_:0198d00f-e703-700e-8262-3411f6f1a7b4",
+ "proof": {
+ "type": "BbsBlsSignature2020",
+ "created": "2025-08-22T04:35:59Z",
+ "proofPurpose": "assertionMethod",
+ "proofValue": "o32r+zLI4dcOeLY1omIXoqQKXwwC6z9ihFkGowUvlpgdk3lBA02NRiOuTNQt8CExDEfRAbq+17MwX8sMkSSN7Drbug0evckYllVnzTy4hzRTItiHNrGr9979b4i486eQCEF3QwrQPSU1k/v3LXx6lQ==",
+ "verificationMethod": "did:web:trustvc.github.io:did:1#keys-1"
+ }
+}
diff --git a/src/__tests__/__fixtures__/w3c/bbs2023_w3c_transferable_document_v2_0.json b/src/__tests__/__fixtures__/w3c/bbs2023_w3c_transferable_document_v2_0.json
new file mode 100644
index 0000000..63fde06
--- /dev/null
+++ b/src/__tests__/__fixtures__/w3c/bbs2023_w3c_transferable_document_v2_0.json
@@ -0,0 +1,100 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/credentials/v2",
+ "https://w3id.org/security/data-integrity/v2",
+ "https://trustvc.io/context/render-method-context-v2.json",
+ "https://trustvc.io/context/bill-of-lading-carrier.json",
+ "https://trustvc.io/context/attachments-context.json",
+ "https://trustvc.io/context/transferable-records-context.json",
+ "https://trustvc.io/context/qrcode-context.json"
+ ],
+ "renderMethod": [
+ {
+ "type": "EMBEDDED_RENDERER",
+ "templateName": "BILL_OF_LADING_CARRIER",
+ "id": "https://generic-templates.tradetrust.io"
+ }
+ ],
+ "credentialSubject": {
+ "type": ["BillOfLadingCarrier"],
+ "shipperName": "MAERSK Co.",
+ "shipperAddressStreet": "101 ORCHARD ROAD",
+ "shipperAddressCountry": "Singapore",
+ "toOrderOfText": "TO ORDER",
+ "consigneeName": "ABC Natural Foods Inc.",
+ "notifyPartyName": "Amanda Green β Import Manager, ABC Natural Foods",
+ "packages": [
+ {
+ "packagesDescription": "Organic Cashew Kernels (25kg bags)",
+ "packagesMeasurement": "100 Bags",
+ "packagesWeight": "2.65 MT"
+ },
+ {
+ "packagesDescription": "Roasted Chickpeas (20kg packs)",
+ "packagesMeasurement": "60 Bundles",
+ "packagesWeight": "1.3"
+ }
+ ],
+ "blNumber": "SGCNM21566325",
+ "scac": "SGPU",
+ "carrierName": "Vikram Rao",
+ "logo": "data:image/png;base64,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",
+ "onwardInlandRouting": "Rail to Johor Port β Trucking to final inland delivery point (Long Beach, USA)",
+ "vessel": "MAERSK NATALIA",
+ "voyageNo": "7831W",
+ "portOfLoading": "Singapore",
+ "portOfDischarge": "LOS ANGELES, CA",
+ "placeOfReceipt": "JURONG PORT, SINGAPORE",
+ "placeOfDelivery": "Long Beach Distribution Center, CA",
+ "placeOfIssueBL": "Singapore",
+ "numberOfOriginalBL": "3",
+ "dateOfIssueBL": "2025-06-05",
+ "shippedOnBoardDate": "2025-06-05",
+ "signForTermsAndCondition": "The carrier accepts the goods as described in good order and condition for carriage under the terms stated herein and subject to the Carrier's standard Bill of Lading Terms and Conditions.",
+ "signedForCarrierText": "John Doe",
+ "carrierSignature": "data:image/png;base64,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gMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIXVqaAAAAbSSURBVIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBP4/csBUQgAdQDwAAAAASUVORK5CYII=",
+ "termsOfCarriage": "All shipments are subject to the Hague-Visby Rules. The carrier assumes liability only for loss or damage due to its own negligence. Responsibility ceases at the time goods are delivered to the consignee or their agent. Claims must be submitted within 7 working days of delivery.",
+ "attachments": [
+ {
+ "data": "JVBERi0xLjYNJeLjz9MNCjI0IDAgb2JqDTw8L0ZpbHRlci9GbGF0ZURlY29kZS9GaXJzdCA0L0xlbmd0aCAyMTYvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjePI9RS8MwFIX/yn1bi9jepCQ6GYNpFBTEMsW97CVLbjWYNpImmz/fVsXXcw/f/c4SEFarepPTe4iFok8dU09DgtDBQx6TMwT74vaLTE7uSPDUdXM0Xe/73r1FnVwYYEtHR6d9WdY3kX4ipRMV6oojSmxQMoGyac5RLBAXf63p38aGA7XPorLewyvFcYaJile8rB+D/YcwiRdMMGScszO8/IW0MdhsaKKYGA46gXKTr/cUQVY4We/cYMNpnLVeXPJUXHs9fECr7kAFk+eZ5Xr9LcAAfKpQrA0KZW5kc3RyZWFtDWVuZG9iag0yNSAwIG9iag08PC9GaWx0ZXIvRmxhdGVEZWNvZGUvRmlyc3QgNC9MZW5ndGggNDkvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjeslAwULCx0XfOL80rUTDU985MKY42NAIKBsXqh1QWpOoHJKanFtvZAQQYAN/6C60NCmVuZHN0cmVhbQ1lbmRvYmoNMjYgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0ZpcnN0IDkvTGVuZ3RoIDQyL04gMi9UeXBlL09ialN0bT4+c3RyZWFtDQpo3jJTMFAwVzC0ULCx0fcrzS2OBnENFIJi7eyAIsH6LnZ2AAEGAI2FCDcNCmVuZHN0cmVhbQ1lbmRvYmoNMjcgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0ZpcnN0IDUvTGVuZ3RoIDEyMC9OIDEvVHlwZS9PYmpTdG0+PnN0cmVhbQ0KaN4yNFIwULCx0XfOzytJzSspVjAyBgoE6TsX5Rc45VdEGwB5ZoZGCuaWRrH6vqkpmYkYogGJRUCdChZgfUGpxfmlRcmpxUAzA4ryk4NTS6L1A1zc9ENSK0pi7ez0g/JLEktSFQz0QyoLUoF601Pt7AACDADYoCeWDQplbmRzdHJlYW0NZW5kb2JqDTIgMCBvYmoNPDwvTGVuZ3RoIDM1MjUvU3VidHlwZS9YTUwvVHlwZS9NZXRhZGF0YT4+c3RyZWFtDQo8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjQtYzAwNSA3OC4xNDczMjYsIDIwMTIvMDgvMjMtMTM6MDM6MDMgICAgICAgICI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnBkZj0iaHR0cDovL25zLmFkb2JlLmNvbS9wZGYvMS4zLyIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIj4KICAgICAgICAgPHBkZjpQcm9kdWNlcj5BY3JvYmF0IERpc3RpbGxlciA2LjAgKFdpbmRvd3MpPC9wZGY6UHJvZHVjZXI+CiAgICAgICAgIDx4bXA6Q3JlYXRlRGF0ZT4yMDA2LTAzLTA2VDE1OjA2OjMzLTA1OjAwPC94bXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZVBTNS5kbGwgVmVyc2lvbiA1LjIuMjwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxNi0wNy0xNVQxMDoxMjoyMSswODowMDwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6TWV0YWRhdGFEYXRlPjIwMTYtMDctMTVUMTA6MTI6MjErMDg6MDA8L3htcDpNZXRhZGF0YURhdGU+CiAgICAgICAgIDx4bXBNTTpEb2N1bWVudElEPnV1aWQ6ZmYzZGNmZDEtMjNmYS00NzZmLTgzOWEtM2U1Y2FlMmRhMmViPC94bXBNTTpEb2N1bWVudElEPgogICAgICAgICA8eG1wTU06SW5zdGFuY2VJRD51dWlkOjM1OTM1MGIzLWFmNDAtNGQ4YS05ZDZjLTAzMTg2YjRmZmIzNjwveG1wTU06SW5zdGFuY2VJRD4KICAgICAgICAgPGRjOmZvcm1hdD5hcHBsaWNhdGlvbi9wZGY8L2RjOmZvcm1hdD4KICAgICAgICAgPGRjOnRpdGxlPgogICAgICAgICAgICA8cmRmOkFsdD4KICAgICAgICAgICAgICAgPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5CbGFuayBQREYgRG9jdW1lbnQ8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6QWx0PgogICAgICAgICA8L2RjOnRpdGxlPgogICAgICAgICA8ZGM6Y3JlYXRvcj4KICAgICAgICAgICAgPHJkZjpTZXE+CiAgICAgICAgICAgICAgIDxyZGY6bGk+RGVwYXJ0bWVudCBvZiBKdXN0aWNlIChFeGVjdXRpdmUgT2ZmaWNlIG9mIEltbWlncmF0aW9uIFJldmlldyk8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6U2VxPgogICAgICAgICA8L2RjOmNyZWF0b3I+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz4NCmVuZHN0cmVhbQ1lbmRvYmoNMTEgMCBvYmoNPDwvTWV0YWRhdGEgMiAwIFIvUGFnZUxhYmVscyA2IDAgUi9QYWdlcyA4IDAgUi9UeXBlL0NhdGFsb2c+Pg1lbmRvYmoNMjMgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0xlbmd0aCAxMD4+c3RyZWFtDQpIiQIIMAAAAAABDQplbmRzdHJlYW0NZW5kb2JqDTI4IDAgb2JqDTw8L0RlY29kZVBhcm1zPDwvQ29sdW1ucyA0L1ByZWRpY3RvciAxMj4+L0ZpbHRlci9GbGF0ZURlY29kZS9JRFs8REI3Nzc1Q0NFMjI3RjZCMzBDNDQwREY0MjIxREMzOTA+PEJGQ0NDRjNGNTdGNjEzNEFCRDNDMDRBOUU0Q0ExMDZFPl0vSW5mbyA5IDAgUi9MZW5ndGggODAvUm9vdCAxMSAwIFIvU2l6ZSAyOS9UeXBlL1hSZWYvV1sxIDIgMV0+PnN0cmVhbQ0KaN5iYgACJjDByGzIwPT/73koF0wwMUiBWYxA4v9/EMHA9I/hBVCxoDOQeH8DxH2KrIMIglFwIpD1vh5IMJqBxPpArHYgwd/KABBgAP8bEC0NCmVuZHN0cmVhbQ1lbmRvYmoNc3RhcnR4cmVmDQo0NTc2DQolJUVPRg0K",
+ "filename": "blank.pdf",
+ "mimeType": "application/pdf"
+ },
+ {
+ "data": "e30=",
+ "filename": "empty.json",
+ "mimeType": "application/json"
+ },
+ {
+ "data": "JVBERi0xLjMNCiXi48/TDQoNCjEgMCBvYmoNCjw8DQovVHlwZSAvQ2F0YWxvZw0KL091dGxpbmVzIDIgMCBSDQovUGFnZXMgMyAwIFINCj4+DQplbmRvYmoNCg0KMiAwIG9iag0KPDwNCi9UeXBlIC9PdXRsaW5lcw0KL0NvdW50IDANCj4+DQplbmRvYmoNCg0KMyAwIG9iag0KPDwNCi9UeXBlIC9QYWdlcw0KL0NvdW50IDINCi9LaWRzIFsgNCAwIFIgNiAwIFIgXSANCj4+DQplbmRvYmoNCg0KNCAwIG9iag0KPDwNCi9UeXBlIC9QYWdlDQovUGFyZW50IDMgMCBSDQovUmVzb3VyY2VzIDw8DQovRm9udCA8PA0KL0YxIDkgMCBSIA0KPj4NCi9Qcm9jU2V0IDggMCBSDQo+Pg0KL01lZGlhQm94IFswIDAgNjEyLjAwMDAgNzkyLjAwMDBdDQovQ29udGVudHMgNSAwIFINCj4+DQplbmRvYmoNCg0KNSAwIG9iag0KPDwgL0xlbmd0aCAxMDc0ID4+DQpzdHJlYW0NCjIgSg0KQlQNCjAgMCAwIHJnDQovRjEgMDAyNyBUZg0KNTcuMzc1MCA3MjIuMjgwMCBUZA0KKCBBIFNpbXBsZSBQREYgRmlsZSApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY4OC42MDgwIFRkDQooIFRoaXMgaXMgYSBzbWFsbCBkZW1vbnN0cmF0aW9uIC5wZGYgZmlsZSAtICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjY0LjcwNDAgVGQNCigganVzdCBmb3IgdXNlIGluIHRoZSBWaXJ0dWFsIE1lY2hhbmljcyB0dXRvcmlhbHMuIE1vcmUgdGV4dC4gQW5kIG1vcmUgKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NTIuNzUyMCBUZA0KKCB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDYyOC44NDgwIFRkDQooIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjE2Ljg5NjAgVGQNCiggdGV4dC4gQW5kIG1vcmUgdGV4dC4gQm9yaW5nLCB6enp6ei4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjA0Ljk0NDAgVGQNCiggbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDU5Mi45OTIwIFRkDQooIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNTY5LjA4ODAgVGQNCiggQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA1NTcuMTM2MCBUZA0KKCB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBFdmVuIG1vcmUuIENvbnRpbnVlZCBvbiBwYWdlIDIgLi4uKSBUag0KRVQNCmVuZHN0cmVhbQ0KZW5kb2JqDQoNCjYgMCBvYmoNCjw8DQovVHlwZSAvUGFnZQ0KL1BhcmVudCAzIDAgUg0KL1Jlc291cmNlcyA8PA0KL0ZvbnQgPDwNCi9GMSA5IDAgUiANCj4+DQovUHJvY1NldCA4IDAgUg0KPj4NCi9NZWRpYUJveCBbMCAwIDYxMi4wMDAwIDc5Mi4wMDAwXQ0KL0NvbnRlbnRzIDcgMCBSDQo+Pg0KZW5kb2JqDQoNCjcgMCBvYmoNCjw8IC9MZW5ndGggNjc2ID4+DQpzdHJlYW0NCjIgSg0KQlQNCjAgMCAwIHJnDQovRjEgMDAyNyBUZg0KNTcuMzc1MCA3MjIuMjgwMCBUZA0KKCBTaW1wbGUgUERGIEZpbGUgMiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY4OC42MDgwIFRkDQooIC4uLmNvbnRpbnVlZCBmcm9tIHBhZ2UgMS4gWWV0IG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NzYuNjU2MCBUZA0KKCBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY2NC43MDQwIFRkDQooIHRleHQuIE9oLCBob3cgYm9yaW5nIHR5cGluZyB0aGlzIHN0dWZmLiBCdXQgbm90IGFzIGJvcmluZyBhcyB3YXRjaGluZyApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY1Mi43NTIwIFRkDQooIHBhaW50IGRyeS4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NDAuODAwMCBUZA0KKCBCb3JpbmcuICBNb3JlLCBhIGxpdHRsZSBtb3JlIHRleHQuIFRoZSBlbmQsIGFuZCBqdXN0IGFzIHdlbGwuICkgVGoNCkVUDQplbmRzdHJlYW0NCmVuZG9iag0KDQo4IDAgb2JqDQpbL1BERiAvVGV4dF0NCmVuZG9iag0KDQo5IDAgb2JqDQo8PA0KL1R5cGUgL0ZvbnQNCi9TdWJ0eXBlIC9UeXBlMQ0KL05hbWUgL0YxDQovQmFzZUZvbnQgL0hlbHZldGljYQ0KL0VuY29kaW5nIC9XaW5BbnNpRW5jb2RpbmcNCj4+DQplbmRvYmoNCg0KMTAgMCBvYmoNCjw8DQovQ3JlYXRvciAoUmF2ZSBcKGh0dHA6Ly93d3cubmV2cm9uYS5jb20vcmF2ZVwpKQ0KL1Byb2R1Y2VyIChOZXZyb25hIERlc2lnbnMpDQovQ3JlYXRpb25EYXRlIChEOjIwMDYwMzAxMDcyODI2KQ0KPj4NCmVuZG9iag0KDQp4cmVmDQowIDExDQowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDAwMTkgMDAwMDAgbg0KMDAwMDAwMDA5MyAwMDAwMCBuDQowMDAwMDAwMTQ3IDAwMDAwIG4NCjAwMDAwMDAyMjIgMDAwMDAgbg0KMDAwMDAwMDM5MCAwMDAwMCBuDQowMDAwMDAxNTIyIDAwMDAwIG4NCjAwMDAwMDE2OTAgMDAwMDAgbg0KMDAwMDAwMjQyMyAwMDAwMCBuDQowMDAwMDAyNDU2IDAwMDAwIG4NCjAwMDAwMDI1NzQgMDAwMDAgbg0KDQp0cmFpbGVyDQo8PA0KL1NpemUgMTENCi9Sb290IDEgMCBSDQovSW5mbyAxMCAwIFINCj4+DQoNCnN0YXJ0eHJlZg0KMjcxNA0KJSVFT0YNCg==",
+ "filename": "sample.pdf",
+ "mimeType": "application/pdf"
+ },
+ {
+ "data": "JVBERi0xLjYNJeLjz9MNCjI0IDAgb2JqDTw8L0ZpbHRlci9GbGF0ZURlY29kZS9GaXJzdCA0L0xlbmd0aCAyMTYvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjePI9RS8MwFIX/yn1bi9jepCQ6GYNpFBTEMsW97CVLbjWYNpImmz/fVsXXcw/f/c4SEFarepPTe4iFok8dU09DgtDBQx6TMwT74vaLTE7uSPDUdXM0Xe/73r1FnVwYYEtHR6d9WdY3kX4ipRMV6oojSmxQMoGyac5RLBAXf63p38aGA7XPorLewyvFcYaJile8rB+D/YcwiRdMMGScszO8/IW0MdhsaKKYGA46gXKTr/cUQVY4We/cYMNpnLVeXPJUXHs9fECr7kAFk+eZ5Xr9LcAAfKpQrA0KZW5kc3RyZWFtDWVuZG9iag0yNSAwIG9iag08PC9GaWx0ZXIvRmxhdGVEZWNvZGUvRmlyc3QgNC9MZW5ndGggNDkvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjeslAwULCx0XfOL80rUTDU985MKY42NAIKBsXqh1QWpOoHJKanFtvZAQQYAN/6C60NCmVuZHN0cmVhbQ1lbmRvYmoNMjYgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0ZpcnN0IDkvTGVuZ3RoIDQyL04gMi9UeXBlL09ialN0bT4+c3RyZWFtDQpo3jJTMFAwVzC0ULCx0fcrzS2OBnENFIJi7eyAIsH6LnZ2AAEGAI2FCDcNCmVuZHN0cmVhbQ1lbmRvYmoNMjcgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0ZpcnN0IDUvTGVuZ3RoIDEyMC9OIDEvVHlwZS9PYmpTdG0+PnN0cmVhbQ0KaN4yNFIwULCx0XfOzytJzSspVjAyBgoE6TsX5Rc45VdEGwB5ZoZGCuaWRrH6vqkpmYkYogGJRUCdChZgfUGpxfmlRcmpxUAzA4ryk4NTS6L1A1zc9ENSK0pi7ez0g/JLEktSFQz0QyoLUoF601Pt7AACDADYoCeWDQplbmRzdHJlYW0NZW5kb2JqDTIgMCBvYmoNPDwvTGVuZ3RoIDM1MjUvU3VidHlwZS9YTUwvVHlwZS9NZXRhZGF0YT4+c3RyZWFtDQo8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjQtYzAwNSA3OC4xNDczMjYsIDIwMTIvMDgvMjMtMTM6MDM6MDMgICAgICAgICI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnBkZj0iaHR0cDovL25zLmFkb2JlLmNvbS9wZGYvMS4zLyIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIj4KICAgICAgICAgPHBkZjpQcm9kdWNlcj5BY3JvYmF0IERpc3RpbGxlciA2LjAgKFdpbmRvd3MpPC9wZGY6UHJvZHVjZXI+CiAgICAgICAgIDx4bXA6Q3JlYXRlRGF0ZT4yMDA2LTAzLTA2VDE1OjA2OjMzLTA1OjAwPC94bXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZVBTNS5kbGwgVmVyc2lvbiA1LjIuMjwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxNi0wNy0xNVQxMDoxMjoyMSswODowMDwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6TWV0YWRhdGFEYXRlPjIwMTYtMDctMTVUMTA6MTI6MjErMDg6MDA8L3htcDpNZXRhZGF0YURhdGU+CiAgICAgICAgIDx4bXBNTTpEb2N1bWVudElEPnV1aWQ6ZmYzZGNmZDEtMjNmYS00NzZmLTgzOWEtM2U1Y2FlMmRhMmViPC94bXBNTTpEb2N1bWVudElEPgogICAgICAgICA8eG1wTU06SW5zdGFuY2VJRD51dWlkOjM1OTM1MGIzLWFmNDAtNGQ4YS05ZDZjLTAzMTg2YjRmZmIzNjwveG1wTU06SW5zdGFuY2VJRD4KICAgICAgICAgPGRjOmZvcm1hdD5hcHBsaWNhdGlvbi9wZGY8L2RjOmZvcm1hdD4KICAgICAgICAgPGRjOnRpdGxlPgogICAgICAgICAgICA8cmRmOkFsdD4KICAgICAgICAgICAgICAgPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5CbGFuayBQREYgRG9jdW1lbnQ8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6QWx0PgogICAgICAgICA8L2RjOnRpdGxlPgogICAgICAgICA8ZGM6Y3JlYXRvcj4KICAgICAgICAgICAgPHJkZjpTZXE+CiAgICAgICAgICAgICAgIDxyZGY6bGk+RGVwYXJ0bWVudCBvZiBKdXN0aWNlIChFeGVjdXRpdmUgT2ZmaWNlIG9mIEltbWlncmF0aW9uIFJldmlldyk8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6U2VxPgogICAgICAgICA8L2RjOmNyZWF0b3I+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz4NCmVuZHN0cmVhbQ1lbmRvYmoNMTEgMCBvYmoNPDwvTWV0YWRhdGEgMiAwIFIvUGFnZUxhYmVscyA2IDAgUi9QYWdlcyA4IDAgUi9UeXBlL0NhdGFsb2c+Pg1lbmRvYmoNMjMgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0xlbmd0aCAxMD4+c3RyZWFtDQpIiQIIMAAAAAABDQplbmRzdHJlYW0NZW5kb2JqDTI4IDAgb2JqDTw8L0RlY29kZVBhcm1zPDwvQ29sdW1ucyA0L1ByZWRpY3RvciAxMj4+L0ZpbHRlci9GbGF0ZURlY29kZS9JRFs8REI3Nzc1Q0NFMjI3RjZCMzBDNDQwREY0MjIxREMzOTA+PEJGQ0NDRjNGNTdGNjEzNEFCRDNDMDRBOUU0Q0ExMDZFPl0vSW5mbyA5IDAgUi9MZW5ndGggODAvUm9vdCAxMSAwIFIvU2l6ZSAyOS9UeXBlL1hSZWYvV1sxIDIgMV0+PnN0cmVhbQ0KaN5iYgACJjDByGzIwPT/73koF0wwMUiBWYxA4v9/EMHA9I/hBVCxoDOQeH8DxH2KrIMIglFwIpD1vh5IMJqBxPpArHYgwd/KABBgAP8bEC0NCmVuZHN0cmVhbQ1lbmRvYmoNc3RhcnR4cmVmDQo0NTc2DQolJUVPRg0K",
+ "filename": "veryverylongfilenameoverhereveryverylongfilenameoverhere.pdf",
+ "mimeType": "application/pdf"
+ }
+ ]
+ },
+ "type": ["VerifiableCredential"],
+ "credentialStatus": {
+ "type": "TransferableRecords",
+ "tokenNetwork": {
+ "chain": "FREE",
+ "chainId": 101010
+ },
+ "tokenRegistry": "0x7202363bBDb126036F7C3243Ebac310d9d145040",
+ "tokenId": "77b056e4524e69c51fa0d27459ae504b9b2ab88359b4be40e2b4a60ccda2e900"
+ },
+ "issuer": "did:web:trustvc.github.io:did:1",
+ "validFrom": "2024-04-01T12:19:52Z",
+ "id": "urn:uuid:019bde89-fa0b-733c-b663-6b46c6a6908b",
+ "proof": {
+ "type": "DataIntegrityProof",
+ "verificationMethod": "did:web:trustvc.github.io:did:1#multikey-2",
+ "cryptosuite": "bbs-2023",
+ "proofPurpose": "assertionMethod",
+ "proofValue": "u2V0ChVhQhzENt_A1gmatFwG5rfZiOth1JD3ALwgh_esbGXFU5Nhs3hrQ4iw_jzpy5-W1c1OrKWdKLgNG2Lj1G6zQyA3myYqrNy4DgtO86ElSV_ewJqdYQD9WZttHrlLy5Yt4KX5JlbD4AqxyPhcyoKk-Wo6FkAtpZsbWIE72P8kG5m7fT_DVEHnjS2aA2qqbQ2cORbU0hWtYYLDx2EM7LXGzSqyTOC8ZKJ9hgD0GHrf59LhRlLV3-pK34L5ohGo8I-g81SD6xVKofBMNiXxFLrp7w56sQlEOkcpISekB2jtn0DeTWzNHrnVwuejhZPM1PPtOuxtkbzj6J1ggi8bJHCZOPnakpm0CkyIIs5xNfTu0U_0e-0iy-iqq2nuCZy9pc3N1ZXJqL3ZhbGlkRnJvbQ"
+ }
+}
diff --git a/src/__tests__/__fixtures__/w3c/bbs2023_w3c_verifiable_document_v2_0.json b/src/__tests__/__fixtures__/w3c/bbs2023_w3c_verifiable_document_v2_0.json
new file mode 100644
index 0000000..6424d05
--- /dev/null
+++ b/src/__tests__/__fixtures__/w3c/bbs2023_w3c_verifiable_document_v2_0.json
@@ -0,0 +1,31 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/credentials/v2",
+ "https://w3id.org/security/data-integrity/v2",
+ "https://w3c-ccg.github.io/citizenship-vocab/contexts/citizenship-v2.jsonld"
+ ],
+ "credentialStatus": {
+ "id": "https://trustvc.github.io/did/credentials/statuslist/1#1",
+ "type": "BitstringStatusListEntry",
+ "statusPurpose": "revocation",
+ "statusListIndex": "10",
+ "statusListCredential": "https://trustvc.github.io/did/credentials/statuslist/1"
+ },
+ "credentialSubject": {
+ "name": "TrustVC",
+ "birthDate": "2024-04-01T12:19:52Z",
+ "type": ["PermanentResident", "Person"]
+ },
+ "validUntil": "2029-12-03T12:19:52Z",
+ "issuer": "did:web:trustvc.github.io:did:1",
+ "type": ["VerifiableCredential"],
+ "validFrom": "2024-04-01T12:19:52Z",
+ "id": "urn:uuid:019bdac4-15af-7ddf-a904-e29e770cb8d0",
+ "proof": {
+ "type": "DataIntegrityProof",
+ "verificationMethod": "did:web:trustvc.github.io:did:1#multikey-2",
+ "cryptosuite": "bbs-2023",
+ "proofPurpose": "assertionMethod",
+ "proofValue": "u2V0ChVhQl3FQf1iCsd8UOYKoOtE05EwyADb2pFKw3CXYhmAAKstq1lX4o69u58Z67M_2i6RIRZZv-KITPWVH00hkQKuNH3GWNpw9JO1RMQ_iKJm4u-xYQD9WZttHrlLy5Yt4KX5JlbD4AqxyPhcyoKk-Wo6FkAtpZhBXKsdUqNY4_vn1ieMbuZLxuIijh5S1vR5UbeeBlupYYLDx2EM7LXGzSqyTOC8ZKJ9hgD0GHrf59LhRlLV3-pK34L5ohGo8I-g81SD6xVKofBMNiXxFLrp7w56sQlEOkcpISekB2jtn0DeTWzNHrnVwuejhZPM1PPtOuxtkbzj6J1ggQDXmZWDv7Gc0fHBKsyn_bGwKW8HLyTRJZZ-2ielvfxqCZy9pc3N1ZXJqL3ZhbGlkRnJvbQ"
+ }
+}
diff --git a/src/__tests__/__fixtures__/w3c/ecdsa_w3c_transferable_document_v2_0.json b/src/__tests__/__fixtures__/w3c/ecdsa_w3c_transferable_document_v2_0.json
new file mode 100644
index 0000000..76000f3
--- /dev/null
+++ b/src/__tests__/__fixtures__/w3c/ecdsa_w3c_transferable_document_v2_0.json
@@ -0,0 +1,105 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/credentials/v2",
+ "https://w3id.org/security/data-integrity/v2",
+ "https://trustvc.io/context/render-method-context-v2.json",
+ "https://trustvc.io/context/bill-of-lading-carrier.json",
+ "https://trustvc.io/context/attachments-context.json",
+ "https://trustvc.io/context/transferable-records-context.json",
+ "https://trustvc.io/context/qrcode-context.json"
+ ],
+ "renderMethod": [
+ {
+ "type": "EMBEDDED_RENDERER",
+ "templateName": "BILL_OF_LADING_CARRIER",
+ "id": "https://generic-templates.tradetrust.io"
+ }
+ ],
+ "credentialSubject": {
+ "type": ["BillOfLadingCarrier"],
+ "shipperName": "MAERSK Co.",
+ "shipperAddressStreet": "101 ORCHARD ROAD",
+ "shipperAddressCountry": "Singapore",
+ "toOrderOfText": "TO ORDER",
+ "consigneeName": "ABC Natural Foods Inc.",
+ "notifyPartyName": "Amanda Green β Import Manager, ABC Natural Foods",
+ "packages": [
+ {
+ "packagesDescription": "Organic Cashew Kernels (25kg bags)",
+ "packagesMeasurement": "100 Bags",
+ "packagesWeight": "2.65 MT"
+ },
+ {
+ "packagesDescription": "Roasted Chickpeas (20kg packs)",
+ "packagesMeasurement": "60 Bundles",
+ "packagesWeight": "1.3"
+ }
+ ],
+ "blNumber": "SGCNM21566325",
+ "scac": "SGPU",
+ "carrierName": "Vikram Rao",
+ "logo": "data:image/png;base64,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",
+ "onwardInlandRouting": "Rail to Johor Port β Trucking to final inland delivery point (Long Beach, USA)",
+ "vessel": "MAERSK NATALIA",
+ "voyageNo": "7831W",
+ "portOfLoading": "Singapore",
+ "portOfDischarge": "LOS ANGELES, CA",
+ "placeOfReceipt": "JURONG PORT, SINGAPORE",
+ "placeOfDelivery": "Long Beach Distribution Center, CA",
+ "placeOfIssueBL": "Singapore",
+ "numberOfOriginalBL": "3",
+ "dateOfIssueBL": "2025-06-05",
+ "shippedOnBoardDate": "2025-06-05",
+ "signForTermsAndCondition": "The carrier accepts the goods as described in good order and condition for carriage under the terms stated herein and subject to the Carrier's standard Bill of Lading Terms and Conditions.",
+ "signedForCarrierText": "John Doe",
+ "carrierSignature": "data:image/png;base64,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gMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIXVqaAAAAbSSURBVIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBP4/csBUQgAdQDwAAAAASUVORK5CYII=",
+ "termsOfCarriage": "All shipments are subject to the Hague-Visby Rules. The carrier assumes liability only for loss or damage due to its own negligence. Responsibility ceases at the time goods are delivered to the consignee or their agent. Claims must be submitted within 7 working days of delivery.",
+ "attachments": [
+ {
+ "data": "JVBERi0xLjYNJeLjz9MNCjI0IDAgb2JqDTw8L0ZpbHRlci9GbGF0ZURlY29kZS9GaXJzdCA0L0xlbmd0aCAyMTYvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjePI9RS8MwFIX/yn1bi9jepCQ6GYNpFBTEMsW97CVLbjWYNpImmz/fVsXXcw/f/c4SEFarepPTe4iFok8dU09DgtDBQx6TMwT74vaLTE7uSPDUdXM0Xe/73r1FnVwYYEtHR6d9WdY3kX4ipRMV6oojSmxQMoGyac5RLBAXf63p38aGA7XPorLewyvFcYaJile8rB+D/YcwiRdMMGScszO8/IW0MdhsaKKYGA46gXKTr/cUQVY4We/cYMNpnLVeXPJUXHs9fECr7kAFk+eZ5Xr9LcAAfKpQrA0KZW5kc3RyZWFtDWVuZG9iag0yNSAwIG9iag08PC9GaWx0ZXIvRmxhdGVEZWNvZGUvRmlyc3QgNC9MZW5ndGggNDkvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjeslAwULCx0XfOL80rUTDU985MKY42NAIKBsXqh1QWpOoHJKanFtvZAQQYAN/6C60NCmVuZHN0cmVhbQ1lbmRvYmoNMjYgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0ZpcnN0IDkvTGVuZ3RoIDQyL04gMi9UeXBlL09ialN0bT4+c3RyZWFtDQpo3jJTMFAwVzC0ULCx0fcrzS2OBnENFIJi7eyAIsH6LnZ2AAEGAI2FCDcNCmVuZHN0cmVhbQ1lbmRvYmoNMjcgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0ZpcnN0IDUvTGVuZ3RoIDEyMC9OIDEvVHlwZS9PYmpTdG0+PnN0cmVhbQ0KaN4yNFIwULCx0XfOzytJzSspVjAyBgoE6TsX5Rc45VdEGwB5ZoZGCuaWRrH6vqkpmYkYogGJRUCdChZgfUGpxfmlRcmpxUAzA4ryk4NTS6L1A1zc9ENSK0pi7ez0g/JLEktSFQz0QyoLUoF601Pt7AACDADYoCeWDQplbmRzdHJlYW0NZW5kb2JqDTIgMCBvYmoNPDwvTGVuZ3RoIDM1MjUvU3VidHlwZS9YTUwvVHlwZS9NZXRhZGF0YT4+c3RyZWFtDQo8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjQtYzAwNSA3OC4xNDczMjYsIDIwMTIvMDgvMjMtMTM6MDM6MDMgICAgICAgICI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnBkZj0iaHR0cDovL25zLmFkb2JlLmNvbS9wZGYvMS4zLyIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIj4KICAgICAgICAgPHBkZjpQcm9kdWNlcj5BY3JvYmF0IERpc3RpbGxlciA2LjAgKFdpbmRvd3MpPC9wZGY6UHJvZHVjZXI+CiAgICAgICAgIDx4bXA6Q3JlYXRlRGF0ZT4yMDA2LTAzLTA2VDE1OjA2OjMzLTA1OjAwPC94bXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZVBTNS5kbGwgVmVyc2lvbiA1LjIuMjwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxNi0wNy0xNVQxMDoxMjoyMSswODowMDwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6TWV0YWRhdGFEYXRlPjIwMTYtMDctMTVUMTA6MTI6MjErMDg6MDA8L3htcDpNZXRhZGF0YURhdGU+CiAgICAgICAgIDx4bXBNTTpEb2N1bWVudElEPnV1aWQ6ZmYzZGNmZDEtMjNmYS00NzZmLTgzOWEtM2U1Y2FlMmRhMmViPC94bXBNTTpEb2N1bWVudElEPgogICAgICAgICA8eG1wTU06SW5zdGFuY2VJRD51dWlkOjM1OTM1MGIzLWFmNDAtNGQ4YS05ZDZjLTAzMTg2YjRmZmIzNjwveG1wTU06SW5zdGFuY2VJRD4KICAgICAgICAgPGRjOmZvcm1hdD5hcHBsaWNhdGlvbi9wZGY8L2RjOmZvcm1hdD4KICAgICAgICAgPGRjOnRpdGxlPgogICAgICAgICAgICA8cmRmOkFsdD4KICAgICAgICAgICAgICAgPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5CbGFuayBQREYgRG9jdW1lbnQ8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6QWx0PgogICAgICAgICA8L2RjOnRpdGxlPgogICAgICAgICA8ZGM6Y3JlYXRvcj4KICAgICAgICAgICAgPHJkZjpTZXE+CiAgICAgICAgICAgICAgIDxyZGY6bGk+RGVwYXJ0bWVudCBvZiBKdXN0aWNlIChFeGVjdXRpdmUgT2ZmaWNlIG9mIEltbWlncmF0aW9uIFJldmlldyk8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6U2VxPgogICAgICAgICA8L2RjOmNyZWF0b3I+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz4NCmVuZHN0cmVhbQ1lbmRvYmoNMTEgMCBvYmoNPDwvTWV0YWRhdGEgMiAwIFIvUGFnZUxhYmVscyA2IDAgUi9QYWdlcyA4IDAgUi9UeXBlL0NhdGFsb2c+Pg1lbmRvYmoNMjMgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0xlbmd0aCAxMD4+c3RyZWFtDQpIiQIIMAAAAAABDQplbmRzdHJlYW0NZW5kb2JqDTI4IDAgb2JqDTw8L0RlY29kZVBhcm1zPDwvQ29sdW1ucyA0L1ByZWRpY3RvciAxMj4+L0ZpbHRlci9GbGF0ZURlY29kZS9JRFs8REI3Nzc1Q0NFMjI3RjZCMzBDNDQwREY0MjIxREMzOTA+PEJGQ0NDRjNGNTdGNjEzNEFCRDNDMDRBOUU0Q0ExMDZFPl0vSW5mbyA5IDAgUi9MZW5ndGggODAvUm9vdCAxMSAwIFIvU2l6ZSAyOS9UeXBlL1hSZWYvV1sxIDIgMV0+PnN0cmVhbQ0KaN5iYgACJjDByGzIwPT/73koF0wwMUiBWYxA4v9/EMHA9I/hBVCxoDOQeH8DxH2KrIMIglFwIpD1vh5IMJqBxPpArHYgwd/KABBgAP8bEC0NCmVuZHN0cmVhbQ1lbmRvYmoNc3RhcnR4cmVmDQo0NTc2DQolJUVPRg0K",
+ "filename": "blank.pdf",
+ "mimeType": "application/pdf"
+ },
+ {
+ "data": "e30=",
+ "filename": "empty.json",
+ "mimeType": "application/json"
+ },
+ {
+ "data": "JVBERi0xLjMNCiXi48/TDQoNCjEgMCBvYmoNCjw8DQovVHlwZSAvQ2F0YWxvZw0KL091dGxpbmVzIDIgMCBSDQovUGFnZXMgMyAwIFINCj4+DQplbmRvYmoNCg0KMiAwIG9iag0KPDwNCi9UeXBlIC9PdXRsaW5lcw0KL0NvdW50IDANCj4+DQplbmRvYmoNCg0KMyAwIG9iag0KPDwNCi9UeXBlIC9QYWdlcw0KL0NvdW50IDINCi9LaWRzIFsgNCAwIFIgNiAwIFIgXSANCj4+DQplbmRvYmoNCg0KNCAwIG9iag0KPDwNCi9UeXBlIC9QYWdlDQovUGFyZW50IDMgMCBSDQovUmVzb3VyY2VzIDw8DQovRm9udCA8PA0KL0YxIDkgMCBSIA0KPj4NCi9Qcm9jU2V0IDggMCBSDQo+Pg0KL01lZGlhQm94IFswIDAgNjEyLjAwMDAgNzkyLjAwMDBdDQovQ29udGVudHMgNSAwIFINCj4+DQplbmRvYmoNCg0KNSAwIG9iag0KPDwgL0xlbmd0aCAxMDc0ID4+DQpzdHJlYW0NCjIgSg0KQlQNCjAgMCAwIHJnDQovRjEgMDAyNyBUZg0KNTcuMzc1MCA3MjIuMjgwMCBUZA0KKCBBIFNpbXBsZSBQREYgRmlsZSApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY4OC42MDgwIFRkDQooIFRoaXMgaXMgYSBzbWFsbCBkZW1vbnN0cmF0aW9uIC5wZGYgZmlsZSAtICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjY0LjcwNDAgVGQNCigganVzdCBmb3IgdXNlIGluIHRoZSBWaXJ0dWFsIE1lY2hhbmljcyB0dXRvcmlhbHMuIE1vcmUgdGV4dC4gQW5kIG1vcmUgKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NTIuNzUyMCBUZA0KKCB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDYyOC44NDgwIFRkDQooIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjE2Ljg5NjAgVGQNCiggdGV4dC4gQW5kIG1vcmUgdGV4dC4gQm9yaW5nLCB6enp6ei4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjA0Ljk0NDAgVGQNCiggbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDU5Mi45OTIwIFRkDQooIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNTY5LjA4ODAgVGQNCiggQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA1NTcuMTM2MCBUZA0KKCB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBFdmVuIG1vcmUuIENvbnRpbnVlZCBvbiBwYWdlIDIgLi4uKSBUag0KRVQNCmVuZHN0cmVhbQ0KZW5kb2JqDQoNCjYgMCBvYmoNCjw8DQovVHlwZSAvUGFnZQ0KL1BhcmVudCAzIDAgUg0KL1Jlc291cmNlcyA8PA0KL0ZvbnQgPDwNCi9GMSA5IDAgUiANCj4+DQovUHJvY1NldCA4IDAgUg0KPj4NCi9NZWRpYUJveCBbMCAwIDYxMi4wMDAwIDc5Mi4wMDAwXQ0KL0NvbnRlbnRzIDcgMCBSDQo+Pg0KZW5kb2JqDQoNCjcgMCBvYmoNCjw8IC9MZW5ndGggNjc2ID4+DQpzdHJlYW0NCjIgSg0KQlQNCjAgMCAwIHJnDQovRjEgMDAyNyBUZg0KNTcuMzc1MCA3MjIuMjgwMCBUZA0KKCBTaW1wbGUgUERGIEZpbGUgMiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY4OC42MDgwIFRkDQooIC4uLmNvbnRpbnVlZCBmcm9tIHBhZ2UgMS4gWWV0IG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NzYuNjU2MCBUZA0KKCBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY2NC43MDQwIFRkDQooIHRleHQuIE9oLCBob3cgYm9yaW5nIHR5cGluZyB0aGlzIHN0dWZmLiBCdXQgbm90IGFzIGJvcmluZyBhcyB3YXRjaGluZyApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY1Mi43NTIwIFRkDQooIHBhaW50IGRyeS4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NDAuODAwMCBUZA0KKCBCb3JpbmcuICBNb3JlLCBhIGxpdHRsZSBtb3JlIHRleHQuIFRoZSBlbmQsIGFuZCBqdXN0IGFzIHdlbGwuICkgVGoNCkVUDQplbmRzdHJlYW0NCmVuZG9iag0KDQo4IDAgb2JqDQpbL1BERiAvVGV4dF0NCmVuZG9iag0KDQo5IDAgb2JqDQo8PA0KL1R5cGUgL0ZvbnQNCi9TdWJ0eXBlIC9UeXBlMQ0KL05hbWUgL0YxDQovQmFzZUZvbnQgL0hlbHZldGljYQ0KL0VuY29kaW5nIC9XaW5BbnNpRW5jb2RpbmcNCj4+DQplbmRvYmoNCg0KMTAgMCBvYmoNCjw8DQovQ3JlYXRvciAoUmF2ZSBcKGh0dHA6Ly93d3cubmV2cm9uYS5jb20vcmF2ZVwpKQ0KL1Byb2R1Y2VyIChOZXZyb25hIERlc2lnbnMpDQovQ3JlYXRpb25EYXRlIChEOjIwMDYwMzAxMDcyODI2KQ0KPj4NCmVuZG9iag0KDQp4cmVmDQowIDExDQowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDAwMTkgMDAwMDAgbg0KMDAwMDAwMDA5MyAwMDAwMCBuDQowMDAwMDAwMTQ3IDAwMDAwIG4NCjAwMDAwMDAyMjIgMDAwMDAgbg0KMDAwMDAwMDM5MCAwMDAwMCBuDQowMDAwMDAxNTIyIDAwMDAwIG4NCjAwMDAwMDE2OTAgMDAwMDAgbg0KMDAwMDAwMjQyMyAwMDAwMCBuDQowMDAwMDAyNDU2IDAwMDAwIG4NCjAwMDAwMDI1NzQgMDAwMDAgbg0KDQp0cmFpbGVyDQo8PA0KL1NpemUgMTENCi9Sb290IDEgMCBSDQovSW5mbyAxMCAwIFINCj4+DQoNCnN0YXJ0eHJlZg0KMjcxNA0KJSVFT0YNCg==",
+ "filename": "sample.pdf",
+ "mimeType": "application/pdf"
+ },
+ {
+ "data": "JVBERi0xLjYNJeLjz9MNCjI0IDAgb2JqDTw8L0ZpbHRlci9GbGF0ZURlY29kZS9GaXJzdCA0L0xlbmd0aCAyMTYvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjePI9RS8MwFIX/yn1bi9jepCQ6GYNpFBTEMsW97CVLbjWYNpImmz/fVsXXcw/f/c4SEFarepPTe4iFok8dU09DgtDBQx6TMwT74vaLTE7uSPDUdXM0Xe/73r1FnVwYYEtHR6d9WdY3kX4ipRMV6oojSmxQMoGyac5RLBAXf63p38aGA7XPorLewyvFcYaJile8rB+D/YcwiRdMMGScszO8/IW0MdhsaKKYGA46gXKTr/cUQVY4We/cYMNpnLVeXPJUXHs9fECr7kAFk+eZ5Xr9LcAAfKpQrA0KZW5kc3RyZWFtDWVuZG9iag0yNSAwIG9iag08PC9GaWx0ZXIvRmxhdGVEZWNvZGUvRmlyc3QgNC9MZW5ndGggNDkvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjeslAwULCx0XfOL80rUTDU985MKY42NAIKBsXqh1QWpOoHJKanFtvZAQQYAN/6C60NCmVuZHN0cmVhbQ1lbmRvYmoNMjYgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0ZpcnN0IDkvTGVuZ3RoIDQyL04gMi9UeXBlL09ialN0bT4+c3RyZWFtDQpo3jJTMFAwVzC0ULCx0fcrzS2OBnENFIJi7eyAIsH6LnZ2AAEGAI2FCDcNCmVuZHN0cmVhbQ1lbmRvYmoNMjcgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0ZpcnN0IDUvTGVuZ3RoIDEyMC9OIDEvVHlwZS9PYmpTdG0+PnN0cmVhbQ0KaN4yNFIwULCx0XfOzytJzSspVjAyBgoE6TsX5Rc45VdEGwB5ZoZGCuaWRrH6vqkpmYkYogGJRUCdChZgfUGpxfmlRcmpxUAzA4ryk4NTS6L1A1zc9ENSK0pi7ez0g/JLEktSFQz0QyoLUoF601Pt7AACDADYoCeWDQplbmRzdHJlYW0NZW5kb2JqDTIgMCBvYmoNPDwvTGVuZ3RoIDM1MjUvU3VidHlwZS9YTUwvVHlwZS9NZXRhZGF0YT4+c3RyZWFtDQo8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjQtYzAwNSA3OC4xNDczMjYsIDIwMTIvMDgvMjMtMTM6MDM6MDMgICAgICAgICI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnBkZj0iaHR0cDovL25zLmFkb2JlLmNvbS9wZGYvMS4zLyIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIj4KICAgICAgICAgPHBkZjpQcm9kdWNlcj5BY3JvYmF0IERpc3RpbGxlciA2LjAgKFdpbmRvd3MpPC9wZGY6UHJvZHVjZXI+CiAgICAgICAgIDx4bXA6Q3JlYXRlRGF0ZT4yMDA2LTAzLTA2VDE1OjA2OjMzLTA1OjAwPC94bXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZVBTNS5kbGwgVmVyc2lvbiA1LjIuMjwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxNi0wNy0xNVQxMDoxMjoyMSswODowMDwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6TWV0YWRhdGFEYXRlPjIwMTYtMDctMTVUMTA6MTI6MjErMDg6MDA8L3htcDpNZXRhZGF0YURhdGU+CiAgICAgICAgIDx4bXBNTTpEb2N1bWVudElEPnV1aWQ6ZmYzZGNmZDEtMjNmYS00NzZmLTgzOWEtM2U1Y2FlMmRhMmViPC94bXBNTTpEb2N1bWVudElEPgogICAgICAgICA8eG1wTU06SW5zdGFuY2VJRD51dWlkOjM1OTM1MGIzLWFmNDAtNGQ4YS05ZDZjLTAzMTg2YjRmZmIzNjwveG1wTU06SW5zdGFuY2VJRD4KICAgICAgICAgPGRjOmZvcm1hdD5hcHBsaWNhdGlvbi9wZGY8L2RjOmZvcm1hdD4KICAgICAgICAgPGRjOnRpdGxlPgogICAgICAgICAgICA8cmRmOkFsdD4KICAgICAgICAgICAgICAgPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5CbGFuayBQREYgRG9jdW1lbnQ8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6QWx0PgogICAgICAgICA8L2RjOnRpdGxlPgogICAgICAgICA8ZGM6Y3JlYXRvcj4KICAgICAgICAgICAgPHJkZjpTZXE+CiAgICAgICAgICAgICAgIDxyZGY6bGk+RGVwYXJ0bWVudCBvZiBKdXN0aWNlIChFeGVjdXRpdmUgT2ZmaWNlIG9mIEltbWlncmF0aW9uIFJldmlldyk8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6U2VxPgogICAgICAgICA8L2RjOmNyZWF0b3I+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz4NCmVuZHN0cmVhbQ1lbmRvYmoNMTEgMCBvYmoNPDwvTWV0YWRhdGEgMiAwIFIvUGFnZUxhYmVscyA2IDAgUi9QYWdlcyA4IDAgUi9UeXBlL0NhdGFsb2c+Pg1lbmRvYmoNMjMgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0xlbmd0aCAxMD4+c3RyZWFtDQpIiQIIMAAAAAABDQplbmRzdHJlYW0NZW5kb2JqDTI4IDAgb2JqDTw8L0RlY29kZVBhcm1zPDwvQ29sdW1ucyA0L1ByZWRpY3RvciAxMj4+L0ZpbHRlci9GbGF0ZURlY29kZS9JRFs8REI3Nzc1Q0NFMjI3RjZCMzBDNDQwREY0MjIxREMzOTA+PEJGQ0NDRjNGNTdGNjEzNEFCRDNDMDRBOUU0Q0ExMDZFPl0vSW5mbyA5IDAgUi9MZW5ndGggODAvUm9vdCAxMSAwIFIvU2l6ZSAyOS9UeXBlL1hSZWYvV1sxIDIgMV0+PnN0cmVhbQ0KaN5iYgACJjDByGzIwPT/73koF0wwMUiBWYxA4v9/EMHA9I/hBVCxoDOQeH8DxH2KrIMIglFwIpD1vh5IMJqBxPpArHYgwd/KABBgAP8bEC0NCmVuZHN0cmVhbQ1lbmRvYmoNc3RhcnR4cmVmDQo0NTc2DQolJUVPRg0K",
+ "filename": "veryverylongfilenameoverhereveryverylongfilenameoverhere.pdf",
+ "mimeType": "application/pdf"
+ }
+ ]
+ },
+ "type": ["VerifiableCredential"],
+ "qrCode": {
+ "type": "TrustVCQRCode",
+ "uri": "https://actions.tradetrust.io?q=%7B%22type%22%3A%22DOCUMENT%22%2C%22payload%22%3A%7B%22uri%22%3A%22https%3A%2F%2Fgallery.tradetrust.io%2Fstatic%2Fw3c%2Fv2_0%2Fbill-of-lading-operative.json%22%2C%22redirect%22%3A%22https%3A%2F%2Fref.tradetrust.io%2F%22%2C%22chainId%22%3A%22101010%22%7D%7D"
+ },
+ "credentialStatus": {
+ "type": "TransferableRecords",
+ "tokenNetwork": {
+ "chain": "FREE",
+ "chainId": 101010
+ },
+ "tokenRegistry": "0x7202363bBDb126036F7C3243Ebac310d9d145040",
+ "tokenId": "00dc804498ef4be3f85d308f481889862c47427de812ce07fb68451870f568b9"
+ },
+ "issuer": "did:web:trustvc.github.io:did:1",
+ "validFrom": "2024-04-01T12:19:52Z",
+ "id": "urn:uuid:01992dfa-c12f-7dde-9b11-39ca137e9302",
+ "proof": {
+ "type": "DataIntegrityProof",
+ "created": "2025-09-09T10:17:12Z",
+ "verificationMethod": "did:web:trustvc.github.io:did:1#multikey-1",
+ "cryptosuite": "ecdsa-sd-2023",
+ "proofPurpose": "assertionMethod",
+ "proofValue": "u2V0AhVhAhXO0SlwhidFxMbpIV-KYjjIegFgYCcPGZ6VjO1DsGfW6wug2xWoHGUH0doaCXydbwRi0EtOOoJlfDbn5vYj7uVgjgCQCaSm-p60ackROchjdiT5kjv9sKn_mnlp8JaeWVUMJJVFYIIrSdA2TyIuExK3IoAuJIByBnck7lRfeJu6O9gZNpgVXgIZnL2lzc3VlcmovdmFsaWRGcm9tci9jcmVkZW50aWFsU3ViamVjdG0vcmVuZGVyTWV0aG9kcS9jcmVkZW50aWFsU3RhdHVzZy9xckNvZGU"
+ }
+}
diff --git a/src/__tests__/__fixtures__/w3c/ecdsa_w3c_verifiable_document_v2_0.json b/src/__tests__/__fixtures__/w3c/ecdsa_w3c_verifiable_document_v2_0.json
new file mode 100644
index 0000000..c3f1622
--- /dev/null
+++ b/src/__tests__/__fixtures__/w3c/ecdsa_w3c_verifiable_document_v2_0.json
@@ -0,0 +1,101 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/credentials/v2",
+ "https://w3id.org/security/data-integrity/v2",
+ "https://trustvc.io/context/render-method-context-v2.json",
+ "https://trustvc.io/context/coo.json",
+ "https://trustvc.io/context/attachments-context.json",
+ "https://trustvc.io/context/qrcode-context.json"
+ ],
+ "renderMethod": [
+ {
+ "type": "EMBEDDED_RENDERER",
+ "templateName": "CHAFTA_COO",
+ "id": "https://generic-templates.tradetrust.io"
+ }
+ ],
+ "credentialSubject": {
+ "type": ["Coo"],
+ "signature": "data:image/png;base64,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gMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIXVqaAAAAbSSURBVIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBAQW4UqiQQQIECBAgAABAgQIECBAgIDAwhggQIAAAQIECBAgQIAAAQIEwgkILMKVRIMIECBAgAABAgQIECBAgAABgYUxQIAAAQIECBAgQIAAAQIECIQTEFiEK4kGESBAgAABAgQIECBAgAABAgILY4AAAQIECBAgQIAAAQIECBAIJyCwCFcSDSJAgAABAgQIECBAgAABAgQEFsYAAQIECBAgQIAAAQIECBAgEE5AYBGuJBpEgAABAgQIECBAgAABAgQICCyMAQIECBAgQIAAAQIECBAgQCCcgMAiXEk0iAABAgQIECBAgAABAgQIEBBYGAMECBAgQIAAAQIECBAgQIBAOAGBRbiSaBABAgQIECBAgAABAgQIECAgsDAGCBAgQIAAAQIECBAgQIAAgXACAotwJdEgAgQIECBAgAABAgQIECBAQGBhDBAgQIAAAQIECBAgQIAAAQLhBP4/csBUQgAdQDwAAAAASUVORK5CYII=",
+ "supplyChainConsignmentId": "CONS-EX456789",
+ "exportCountryCode": "IN",
+ "exporterId": "EXP-IN-00987",
+ "exporterName": "ABC Exports Pvt. Ltd.",
+ "exporterLine1": "12/F, Industrial Plaza",
+ "exporterLine2": "Near MIDC, Vashi",
+ "exporterCityName": "Navi Mumbai",
+ "exporterPostcode": "400703",
+ "exporterCountrySubDivisionName": "Maharashtra",
+ "exporterCountryCode": "IN",
+ "importCountryCode": "GB",
+ "importerId": "IMP-UK-88456",
+ "importerName": "XYZ Foods Ltd.",
+ "importerLine1": "Unit 17, Royal Wharf",
+ "importerLine2": "Docklands Industrial Area",
+ "importerCityName": "London",
+ "importerPostcode": "E16 2AA",
+ "importerCountrySubDivisionName": "Greater London",
+ "importerCountryCode": "GB",
+ "includedConsignmentItems": [
+ {
+ "manufacturerId": "MFG-IN-3211",
+ "manufacturerName": "LMN Grains Co.",
+ "manufacturerLine1": "Plot 7, Grain Belt Estate",
+ "manufacturerLine2": "Sector 9",
+ "manufacturerCityName": "Karnal",
+ "manufacturerPostcode": "132001",
+ "manufacturerCountrySubDivisionName": "Haryana",
+ "manufacturerCountryCode": "IN",
+ "tradeLineItems": [
+ {
+ "invoiceReferenceId": "INV-904/UK",
+ "formattedIssueDateTime": "2025-06-06T09:15:00.000Z",
+ "originCountryCode": "IN",
+ "tradeProductId": "TP-98121",
+ "tradeProductDescription": "Basmati Rice, Organic, 20kg Pack",
+ "harmonisedTariffclassCode": "10063010",
+ "harmonisedTariffclassName": "Semi-milled or wholly milled rice, whether or not polished or glazed",
+ "transportPackages": [
+ {
+ "transportPackagesId": "PKG-0101",
+ "transportPackagesGrossVolume": "3.2",
+ "transportPackagesGrossWeight": "2050"
+ }
+ ],
+ "sequenceNumber": -4
+ }
+ ],
+ "includedConsignmentItemsId": "ITEM001",
+ "includedConsignmentItemsInformation": "Organic Basmati Rice β 20kg vacuum packs",
+ "originCriteriaText": "Wholly Obtained in India"
+ }
+ ],
+ "loadingBaseportLocationId": "PORT-IN-MUM",
+ "loadingBaseportLocationName": "Nhava Sheva (JNPT), India",
+ "mainCarriageTransportMovementId": "MCTM-8458",
+ "mainCarriageTransportMovementInformation": "Ocean freight via ABC Shipping",
+ "usedTransportMeansName": "Vessel β XYZ VESSEL 001",
+ "usedTransportMeansId": "VSL-77381-AZ",
+ "departureDateTime": "2025-06-05T09:15:00.000Z",
+ "unloadingBaseportLocationId": "PORT-UK-FEL",
+ "unloadingBaseportLocationName": "Port of Felixstowe, United Kingdom",
+ "cooId": "COO-20250604-00",
+ "issueDateTime": "2025-06-05T21:15:00.000Z"
+ },
+ "type": ["VerifiableCredential"],
+ "qrCode": {
+ "type": "TrustVCQRCode",
+ "uri": "https://actions.tradetrust.io?q=%7B%22type%22%3A%22DOCUMENT%22%2C%22payload%22%3A%7B%22uri%22%3A%22https%3A%2F%2Fgallery.tradetrust.io%2Fstatic%2Fw3c%2Fv2_0%2Fcertificate-of-origin-default.json%22%2C%22redirect%22%3A%22https%3A%2F%2Fref.tradetrust.io%2F%22%2C%22chainId%22%3A%22101010%22%7D%7D"
+ },
+ "issuer": "did:web:trustvc.github.io:did:1",
+ "validFrom": "2024-04-01T12:19:52Z",
+ "id": "urn:uuid:01992e71-8f2b-7000-aa91-4e2970c8bf4b",
+ "proof": {
+ "type": "DataIntegrityProof",
+ "created": "2025-09-09T12:26:58Z",
+ "verificationMethod": "did:web:trustvc.github.io:did:1#multikey-1",
+ "cryptosuite": "ecdsa-sd-2023",
+ "proofPurpose": "assertionMethod",
+ "proofValue": "u2V0AhVhAN0HNoehLZWCVx43aBosJW3XeHtsL7S-C3PvE8tt5qb_mXfBOD5Wgsza84uGljsaqjRG9svPnQbiJTuCsDGXmW1gjgCQDK674hOYZJLdhCH8Vxiir0VYTPbMDx8zSjZo7OAjRcdhYIIFAEQgGCGbGJjfPUROKRgEnjyBP_YzvtAr-xCfkgjZkgIZnL2lzc3VlcmovdmFsaWRGcm9tci9jcmVkZW50aWFsU3ViamVjdG0vcmVuZGVyTWV0aG9kZy9xckNvZGVlL3R5cGU"
+ }
+}
diff --git a/src/__tests__/__fixtures__/w3c/expired_bbs2020_w3c_verifiable_document_v1_1.json b/src/__tests__/__fixtures__/w3c/expired_bbs2020_w3c_verifiable_document_v1_1.json
new file mode 100644
index 0000000..ad8e64f
--- /dev/null
+++ b/src/__tests__/__fixtures__/w3c/expired_bbs2020_w3c_verifiable_document_v1_1.json
@@ -0,0 +1,70 @@
+{
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "https://trustvc.io/context/invoice.json",
+ "https://trustvc.io/context/render-method-context.json",
+ "https://trustvc.io/context/qrcode-context.json",
+ "https://w3id.org/security/bbs/v1"
+ ],
+ "renderMethod": [
+ {
+ "type": "EMBEDDED_RENDERER",
+ "templateName": "INVOICE",
+ "id": "https://generic-templates.tradetrust.io"
+ }
+ ],
+ "credentialSubject": {
+ "type": ["Invoice"],
+ "billFromName": "ABC Exports Pvt. Ltd.",
+ "billFromStreetAddress": "12/F, Industrial Plaza, Near MIDC",
+ "billFromCity": "Navi Mumbai",
+ "billFromPostalCode": "400703",
+ "billFromPhoneNumber": "+91-22-4455-9988",
+ "billToName": "David Thomson",
+ "billToEmail": "david.thomson@example.co.uk",
+ "billToCompanyName": "XYZ Foods Ltd.",
+ "billToCompanyStreetAddress": "Unit 17, Royal Wharf, Docklands Industrial Area",
+ "billToCompanyCity": "London",
+ "billToCompanyPostalCode": "E16 2AA",
+ "billToCompanyPhoneNumber": "+44-20-8899-4455",
+ "billableItems": [
+ {
+ "billableItemsDescription": "Organic Basmati Rice (20kg Bags)",
+ "billableItemsQuantity": "100",
+ "billableItemsUnitPrice": "125",
+ "billableItemsAmount": "12500"
+ },
+ {
+ "billableItemsDescription": "Vacuum-Packed Almonds (10kg)",
+ "billableItemsQuantity": "50",
+ "billableItemsUnitPrice": "80",
+ "billableItemsAmount": "4000"
+ }
+ ],
+ "invoiceId": "INV-20250604-001",
+ "invoiceName": "Export of Organic Basmati Rice",
+ "date": "2025-06-04",
+ "customerId": "CUST-UK-55678",
+ "terms": "Net 30 Days",
+ "subtotal": "$16,500.00",
+ "tax": "5%",
+ "taxTotal": "$825.00",
+ "total": "$17,325.00"
+ },
+ "type": ["VerifiableCredential"],
+ "qrCode": {
+ "type": "TrustVCQRCode",
+ "uri": "https://actions.tradetrust.io?q=%7B%22type%22%3A%22DOCUMENT%22%2C%22payload%22%3A%7B%22uri%22%3A%22https%3A%2F%2Fgallery.tradetrust.io%2Fstatic%2Finvoice-expired.json%22%2C%22redirect%22%3A%22https%3A%2F%2Fref.tradetrust.io%2F%22%2C%22chainId%22%3A%22101010%22%7D%7D"
+ },
+ "issuer": "did:web:trustvc.github.io:did:1",
+ "issuanceDate": "2025-06-09T09:36:15.971Z",
+ "expirationDate": "2025-06-09T11:56:12Z",
+ "id": "urn:bnid:_:0198d013-0fcf-7dde-a0ad-8b5ea02c7af3",
+ "proof": {
+ "type": "BbsBlsSignature2020",
+ "created": "2025-08-22T04:39:26Z",
+ "proofPurpose": "assertionMethod",
+ "proofValue": "k8QwxKBS7BXAWfs1xNv8GuGrEO3FB8/pDIOyWNqI2d2wGYp/tQixwqU0M9GAHV8VNef4c84qgcFZsLSBSM2An8tsYuBoKA8SFJ/lDWfG1KE8rVGAm30irXwSs/afEDzoT2K1T+FA8pEMxuqc1YXdyg==",
+ "verificationMethod": "did:web:trustvc.github.io:did:1#keys-1"
+ }
+}
diff --git a/src/__tests__/__fixtures__/w3c/revoked_ecdsa_w3c_verifiable_document_v2_0.json b/src/__tests__/__fixtures__/w3c/revoked_ecdsa_w3c_verifiable_document_v2_0.json
new file mode 100644
index 0000000..065681a
--- /dev/null
+++ b/src/__tests__/__fixtures__/w3c/revoked_ecdsa_w3c_verifiable_document_v2_0.json
@@ -0,0 +1,79 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/credentials/v2",
+ "https://w3id.org/security/data-integrity/v2",
+ "https://trustvc.io/context/render-method-context-v2.json",
+ "https://trustvc.io/context/invoice.json",
+ "https://trustvc.io/context/qrcode-context.json"
+ ],
+ "renderMethod": [
+ {
+ "type": "EMBEDDED_RENDERER",
+ "templateName": "INVOICE",
+ "id": "https://generic-templates.tradetrust.io"
+ }
+ ],
+ "credentialStatus": [
+ {
+ "id": "https://trustvc.github.io/did/credentials/statuslist/2#5",
+ "statusListCredential": "https://trustvc.github.io/did/credentials/statuslist/2",
+ "statusListIndex": "5",
+ "statusPurpose": "revocation",
+ "type": "BitstringStatusListEntry"
+ }
+ ],
+ "credentialSubject": {
+ "type": ["Invoice"],
+ "billFromName": "ABC Exports Pvt. Ltd.",
+ "billFromStreetAddress": "12/F, Industrial Plaza, Near MIDC",
+ "billFromCity": "Navi Mumbai",
+ "billFromPostalCode": "400703",
+ "billFromPhoneNumber": "+91-22-4455-9988",
+ "billToName": "David Thomson",
+ "billToEmail": "david.thomson@example.co.uk",
+ "billToCompanyName": "XYZ Foods Ltd.",
+ "billToCompanyStreetAddress": "Unit 17, Royal Wharf, Docklands Industrial Area",
+ "billToCompanyCity": "London",
+ "billToCompanyPostalCode": "E16 2AA",
+ "billToCompanyPhoneNumber": "+44-20-8899-4455",
+ "billableItems": [
+ {
+ "billableItemsDescription": "Organic Basmati Rice (20kg Bags)",
+ "billableItemsQuantity": "100",
+ "billableItemsUnitPrice": "125",
+ "billableItemsAmount": "12500"
+ },
+ {
+ "billableItemsDescription": "Vacuum-Packed Almonds (10kg)",
+ "billableItemsQuantity": "50",
+ "billableItemsUnitPrice": "80",
+ "billableItemsAmount": "4000"
+ }
+ ],
+ "invoiceId": "INV-20250604-001",
+ "invoiceName": "Export of Organic Basmati Rice",
+ "date": "2025-06-04",
+ "customerId": "CUST-UK-55678",
+ "terms": "Net 30 Days",
+ "subtotal": "$16,500.00",
+ "tax": "5%",
+ "taxTotal": "$825.00",
+ "total": "$17,325.00"
+ },
+ "type": ["VerifiableCredential"],
+ "qrCode": {
+ "type": "TrustVCQRCode",
+ "uri": "https://actions.tradetrust.io?q=%7B%22type%22%3A%22DOCUMENT%22%2C%22payload%22%3A%7B%22uri%22%3A%22https%3A%2F%2Fgallery.tradetrust.io%2Fstatic%2Fw3c%2Fv2_0%2Finvoice-revoked.json%22%2C%22redirect%22%3A%22https%3A%2F%2Fref.tradetrust.io%2F%22%2C%22chainId%22%3A%22101010%22%7D%7D"
+ },
+ "issuer": "did:web:trustvc.github.io:did:1",
+ "validFrom": "2024-04-01T12:19:52Z",
+ "id": "urn:uuid:019931db-b8b0-7ff3-bdbc-f3ac820c7668",
+ "proof": {
+ "type": "DataIntegrityProof",
+ "created": "2025-09-10T04:21:47Z",
+ "verificationMethod": "did:web:trustvc.github.io:did:1#multikey-1",
+ "cryptosuite": "ecdsa-sd-2023",
+ "proofPurpose": "assertionMethod",
+ "proofValue": "u2V0AhVhAJkFjMn4zWrJXhY-EqWhy-NvFRdLm_dF7IIMd4UbLPwjGJyJAnuwqqiM79LZT4bxow0jXYfNF4XWL5bqVFI1MPFgjgCQDuTPhBc6tZKIWyZZZKeARqDErduYv0x3bD7QfG1ke0cNYINX2Sd4geOSiHU73zBQ3d9GTYw79js0OaXhYlf15Z9f4mCZYQCF6ABtuDUFHHkHwG-AsYwmKXytxMMNeSwW2rPP9LHvUt0Ip75uLBjhPONLssKKj_nM7jrRnRAVyNegggCILiZVYQJIxUqZt95fsygUkWlvPkHShl9ZYkf6IZIheKS-uR-SZI7-xcffLx4xZAx1i5dWIJGJC58oBmjlUwsQuM_hKKLlYQFFDtmRXcM1WTY4l3tZdgV1XYPDm8TgvM7m0eguGP1Cwmu2LrGQjFOGLK3PWV89h_GJrRY9zqyDV8019GPztc3FYQABL9bzxVJimp-Zx7rhuN4PsHprgE4jnsk037M3Ge7fcLGXS6j81xtdlCRxMrOOuI4A6VKGufmaTCoyOEbw5KkdYQPXhkK7IO60JL1B8N9VJuZISeF2agzBVLIGZ30XhOc6o6eew9kh9mOtGrJsboEdDL1avqJ5ljs9fRbkGNW4ielFYQGsERhavqeEJAPjw-8Nmqasok0dbtIuwSJK3LuQ-Hw2ac3kkvAC1_qs6wrq2NJZAAPb0osrD4AtRbhadvAih5T5YQIJsg5i4MLmJFmB3Xqh8Fgt-2Bj5yE6k97dWW4z7yAkg9pOBQscRSrooVK5UUhD-pxNaN_pAIJEQSgKp83E_gClYQDAkopPK1GYD_EbAPZtismuEeQ1gyGMPV4-F-Hq25L1XekQrT9xgBtPuq7_YJFErgGGCwq1bdMQNj9WCGRYleIlYQLvmP8E8h9YpwLvTyhQc9U2tS9SNN8ygF4H2671VoL8nnlW57Id8aCdn_tUmjbCQfvCXw3ipHX5umiUEC-OZPU5YQDBDLEuMbQaL4pUjAKr5h3HO_llDwJEHfitVFA21wDkye5kEdAazzA9ar7KgD_p3BwjtBJBsJt4zK5n8A5up9lhYQEj3rJ3XuKwu9PuAsEKJyRk1K5uL7uiLgXZAbZuHVH4cLJSFM3QdwJFz_JiA0IvkrbHReUw5HjLEgFdX1RDE7tpYQGDY7GFYBxlA4ZHTBEQSuGphQ8sCoAU0EufbkdUZDDe8-4BSu9c76qkzyJ4cPygBMgyXVpL4rAoPlQN_zvbTukdYQIhO2jtOfbydRUWukWaAS_Gvc_dWwBRxzKhQ2ESUR2iN_bwIPWlYjQhr2Erhw-ELq1TeMoTMsDInS9zHa6FWD2ZYQC93YRsCDVTFoLdA6MZw1sLafxknnrvX9qJQrYQltDjMGqDCLGXF-LWgWrL2qa2q4mOgho-HiRa8vAhwYkfNSpVYQDL9Xk94G1IH82smnWRuSHcTeLnJjnr6c2SOMz9aP4x13WQOd06R4gEjfgnSJQsUUbL_wrqxmauaVz2jXXfRC_JYQNocs3VegQSfmTkvq1C8Z5dVixoG0KhNxJASSuaAztL5GC_kxxYtjcwwlY2q3MzEBrj2UPXdBJL2fyYi5WqJtZVYQLDjRcAQ0krynJpv-yP3cWqsMhM73dCu59gncXaOw5WJ5UgLnYc2YFa1cd-gTXWHGsW-IWnF6tsi90ZYCerPNXVYQJnoq7XnxXv2LgzJEVAkYUz219wZvMzDokZLa6dM0FaUhE73MOz804icZCqNFInVJfEyaiq1NZMmIucLEmM0vVRYQOJrji5BLxnKLgKT7J8Wry_Zk5jKrS0u1gAyvoT7ncae6-_mvBHqwEBTp3RCJCI57CqW3Q88FlZxWVAlXCnCzKxYQEZoZWUtK3Y7LYaI8M5ZV5VeenK1aRf5KinI0OWBMicpTDUxtsvjHY-3gMUc650S1fDfTQiSENCZwmV9FxjoxbdYQKroB5h1N-9QPPC41dpVws0ugwmtcBUkiM2Nnu34TH65Nd9inaM26RNEQkYopXt9VyuCQDjhkKxHA5W2M7nTYQlYQCL2PoH71g3kID39lWoK2zJRGujHo1FrHkdwulPtPdUgS5SDhjXQfmjVRnStBufeSv6vmWN7csQN6nRbt2YcO4xYQBxYT5jPmvyfYDaZ03w7cWcXDXu5Y4kNYDdyjrIOM_fcpMW-VKTWT6wD8_AJVbHCPO-BSM_pQ0Ov_JrDZssi9UJYQCWuFoWeJslXYK9gYE9m0aF9R9MO--92j4Nrxf4lf4PXP357Jt3LiDd0kKa6LoCHHYd1x1DVoAwQn323oECyrIJYQMm00znThXQHQ1H4cPNXO9JLwmYTrm-vygxgXaAhwU4Jzn2GD-0vcLhINVRYc0d8inTS9skpooOLcvxEwrBqa9NYQPBT4mzzUThagwfa6YImH8JQ501YL_yPXw9ztrfKcbTf6JlseTzW0eTyxcNTrpQv5yUlUTu0OOy9FM2TVKB-_KZYQHrO2SxRwP4t2L_S-hkBBi9F5MxWJNL-udV_e92PhPr176bVyT3MJ-DDKg242iRed_dr3QOpcPHxJMzgqfGhpupYQJE_UDT_FvTy0fz8nuQlvEVr0ToTl7lmiMZMi5WWQgxS0Hl9OHi1agMgGIg3H2qt5Qk_j0YcmbSDyPM52uiufyFYQBgcljiVyU03hcQk-ZjvwuvvtFU9WBqWIsyQv1mot-nIql5N0-0kWmSYRAmGCpICh0wodYp3xewB-0W8qRLVtplYQM9kWzMGkAcFtqyNN64H2GCaKEK9aGZ0RTjYh5HcQZ8FghNcHrJ24lBus3TfQNTHnTYe6o8NfnRaaBFw65g6UjJYQPF_qKw0cjByCfTwO4sqgFtZih3KMwcRYH--L9SVaQaYI3UtmH2IvARu8vaw7S4rRg9KlO2EcNATmLD63ipOghdYQEhgvX1uH4X6uACyOIKWNG6QjQQRVlnxdhi8a1iTBqkpZkqwlP1bWHQUHn7lqJ3whWFJmX5cBxgfwGglIBw5znFYQF1pocG5tSvP_EnYB_uZcK0nkN3-p4T0-PA_c5KO8aq__jgsGmAG0aFl3A_ITN7JQ3UXHhuVDX1ubQdHT2v8wRNYQKqG-6HeI66LRORGVywhlpoOPCrYfUYeBCgtApKHgwn6DWz_VpdKv_01gEuu_FWi_tD8KOdSJbBbvBjpDZA_wOhYQBtBYPPaGwEHANhFjhMMBFgHTuxT1a1f56Ij1IqK8EKeUvZsEN8NX3VVsB2_2w0AC1bw4bABzDoE3Ok4FCn5B_tYQLmyeMXuQCJVXbi91StAwvI-Gp5yLDTOACuxd0Bqj4_N8DtxaFvakp3KoSxySdY2O5fixsfhUf_EX_vBXPdZ3S9YQJNPn48f1iUCkuGI9UInd9g6aYoQ1sDmGMpU_D7irjKe1CWPx_6aRLqeT-SqO3X5DzNm689trKLSRfDgX5ucM_JYQEnc98-_RPkcGVdcfIUEOs-WnbDPgf9zacuIaDH_Nx14RhJt0XTZ4x0YQBOd7a6yKo-P_lLizKva4V-uC4p0q6yFZy9pc3N1ZXJqL3ZhbGlkRnJvbW0vcmVuZGVyTWV0aG9kZy9xckNvZGVlL3R5cGU"
+ }
+}
diff --git a/src/__tests__/__mocks__/swiper.tsx b/src/__tests__/__mocks__/swiper.tsx
new file mode 100644
index 0000000..8db839a
--- /dev/null
+++ b/src/__tests__/__mocks__/swiper.tsx
@@ -0,0 +1,51 @@
+import React, { ReactNode } from 'react'
+
+export type MockSwiperApi = {
+ slideNext: () => void
+ slidePrev: () => void
+}
+
+export function MockSwiper({
+ children,
+ onSwiper,
+}: {
+ children: ReactNode
+ onSwiper?: (_swiper: MockSwiperApi) => void
+}) {
+ const [slideNumber, setSlideNumber] = React.useState(0)
+
+ const slides = React.Children.toArray(children)
+ const totalSlides = slides.length
+
+ const swiper = React.useMemo
(
+ () => ({
+ slideNext: () =>
+ setSlideNumber(i => (totalSlides === 0 ? 0 : (i + 1) % totalSlides)),
+ slidePrev: () =>
+ setSlideNumber(i =>
+ totalSlides === 0 ? 0 : (i - 1 + totalSlides) % totalSlides
+ ),
+ }),
+ [totalSlides]
+ )
+
+ // Call onSwiper once when the Swiper object is ready
+ React.useEffect(() => {
+ onSwiper?.(swiper)
+ }, [onSwiper, swiper])
+
+ return {slides[slideNumber]}
+}
+
+export function MockSwiperSlide({
+ children,
+ ...rest
+}: {
+ children: ReactNode
+}) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/__tests__/home.test.tsx b/src/__tests__/home.test.tsx
new file mode 100644
index 0000000..64751d1
--- /dev/null
+++ b/src/__tests__/home.test.tsx
@@ -0,0 +1,56 @@
+import { describe, expect, it, vi } from 'vitest'
+import { render, screen, within } from '@testing-library/react'
+import { MemoryRouter } from 'react-router-dom'
+
+import Home from '../pages/Home'
+import carouselData from '../data/carousel.json'
+
+vi.mock('swiper/react', async () => {
+ const { MockSwiper, MockSwiperSlide } = await import('./__mocks__/swiper')
+
+ return {
+ Swiper: MockSwiper,
+ SwiperSlide: MockSwiperSlide,
+ }
+})
+
+vi.mock('swiper/modules', () => ({
+ Navigation: {},
+ Pagination: {},
+ Autoplay: {},
+}))
+
+const renderHome = (isDarkMode: boolean) =>
+ render(
+
+
+
+ )
+
+describe('Home page', () => {
+ it('renders hero and verify sections', () => {
+ renderHome(false)
+
+ expect(screen.getByText(/Simple,/i)).toBeInTheDocument()
+ expect(screen.getByText(/Trustworthy/i)).toBeInTheDocument()
+ expect(
+ screen.getByText(/Drop TrustVC files here to verify/i)
+ ).toBeInTheDocument()
+ })
+
+ it('wires the carousel data through the router', () => {
+ renderHome(false)
+
+ const firstItem = carouselData.items[0]
+ const firstSlide = screen.getByLabelText('carousel-slide-0')
+
+ expect(
+ within(firstSlide).getByText(firstItem.content.subtitle)
+ ).toBeInTheDocument()
+ })
+
+ it('renders the Built for Developers section title', () => {
+ renderHome(false)
+ expect(screen.getByText(/Built for Developers,/i)).toBeInTheDocument()
+ })
+})
diff --git a/src/__tests__/useVerify.integration.test.ts b/src/__tests__/useVerify.integration.test.ts
new file mode 100644
index 0000000..546c427
--- /dev/null
+++ b/src/__tests__/useVerify.integration.test.ts
@@ -0,0 +1,226 @@
+// @vitest-environment node
+import { describe, it, expect } from 'vitest'
+import {
+ verifyDocument,
+ getChainId,
+ isTransferableRecord,
+ isDocumentRevokable,
+ isValid,
+ VerificationFragment,
+} from '@trustvc/trustvc'
+
+// βββ Fixtures βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+// OA v2
+import oaDnsDidV2 from './__fixtures__/oa/2.0/signed_wrapped_oa_dns_did_v2.json'
+import oaDnsTxtDocstoreV2 from './__fixtures__/oa/2.0/signed_wrapped_oa_dns_txt_docstore_v2.json'
+import oaDnsTxtTokenRegistryV2 from './__fixtures__/oa/2.0/signed_wrapped_oa_dns_txt_token_registry_v2.json'
+import oaNoNetworkEthereumV2 from './__fixtures__/oa/2.0/oa_dns_txt_docstore_no_network_field_ethereum_v2.json'
+
+// OA v3
+import oaDnsDidV3 from './__fixtures__/oa/3.0/signed_wrapped_oa_dns_did_v3.json'
+import oaDnsTxtDocstoreV3 from './__fixtures__/oa/3.0/signed_wrapped_oa_dns_txt_docstore_v3.json'
+import oaDnsTxtTokenRegistryV3 from './__fixtures__/oa/3.0/signed_wrapped_oa_dns_txt_token_registry_v3.json'
+import oaNoNetworkStabilityV3 from './__fixtures__/oa/3.0/oa_dns_txt_token_registry_no_network_field_stability_v3.json'
+
+// W3C
+import w3cBbs2020VerifiableDoc from './__fixtures__/w3c/bbs2020_w3c_verifiable_document_v1_1.json'
+import w3cBbs2020TransferableRecord from './__fixtures__/w3c/bbs2020_w3c_transferable_record_v1_1.json'
+import w3cBbs2023VerifiableDoc from './__fixtures__/w3c/bbs2023_w3c_verifiable_document_v2_0.json'
+import w3cBbs2023TransferableDoc from './__fixtures__/w3c/bbs2023_w3c_transferable_document_v2_0.json'
+import w3cEcdsaVerifiableDoc from './__fixtures__/w3c/ecdsa_w3c_verifiable_document_v2_0.json'
+import w3cEcdsaTransferableDoc from './__fixtures__/w3c/ecdsa_w3c_transferable_document_v2_0.json'
+import w3cExpiredDoc from './__fixtures__/w3c/expired_bbs2020_w3c_verifiable_document_v1_1.json'
+import w3cRevokedDoc from './__fixtures__/w3c/revoked_ecdsa_w3c_verifiable_document_v2_0.json'
+
+// βββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+interface VerifyOptions {
+ rpcProviderUrl?: string
+ network?: string
+}
+
+const verify = async (doc: unknown, options: VerifyOptions = {}) => {
+ const results = (await verifyDocument(
+ doc as any,
+ options
+ )) as VerificationFragment[]
+
+ return {
+ results,
+ isValid: isValid(results),
+ }
+}
+
+const needsNetworkSelect = (doc: unknown): boolean => {
+ const chainId = getChainId(doc as any)
+ return (
+ !chainId &&
+ (isTransferableRecord(doc as any) || isDocumentRevokable(doc as any))
+ )
+}
+
+const getRpcUrl = (chainId: string): string | undefined => {
+ return process.env[`VITE_RPC_URL_${chainId}`]
+}
+
+// βββ Tests ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+describe('verifyDocument β integration with real fixtures', () => {
+ // ββ Network-select detection βββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('documents with no network field β need network selection', () => {
+ it('OA v2 docstore (no network field) β detects need for network selection', () => {
+ expect(needsNetworkSelect(oaNoNetworkEthereumV2)).toBe(true)
+ })
+
+ it('OA v3 token registry (no network field) β detects need for network selection', () => {
+ expect(needsNetworkSelect(oaNoNetworkStabilityV3)).toBe(true)
+ })
+ })
+
+ // ββ DID-based (no RPC needed) ββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('DNS-DID documents β valid', () => {
+ it('OA v2 DNS-DID', async () => {
+ const { isValid } = await verify(oaDnsDidV2)
+ expect(isValid).toBe(true)
+ })
+
+ it('OA v3 DNS-DID', async () => {
+ const { isValid } = await verify(oaDnsDidV3)
+ expect(isValid).toBe(true)
+ })
+ })
+
+ // ββ W3C verifiable documents βββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('W3C verifiable documents β valid', () => {
+ it('BBS2020 verifiable document', async () => {
+ const { isValid } = await verify(w3cBbs2020VerifiableDoc)
+ expect(isValid).toBe(true)
+ })
+
+ it('BBS2020 expired verifiable document', async () => {
+ const { isValid } = await verify(w3cExpiredDoc)
+ expect(isValid).toBe(true)
+ })
+
+ it('BBS2023 verifiable document', async () => {
+ const { isValid } = await verify(w3cBbs2023VerifiableDoc)
+ expect(isValid).toBe(true)
+ })
+
+ it('ECDSA verifiable document', async () => {
+ const { isValid } = await verify(w3cEcdsaVerifiableDoc)
+ expect(isValid).toBe(true)
+ })
+ })
+
+ describe.skipIf(!getRpcUrl('101010'))(
+ 'W3C transferable documents β valid',
+ () => {
+ const rpcUrl = getRpcUrl('101010') // Stability
+
+ it('BBS2020 transferable record', async () => {
+ const { isValid } = await verify(w3cBbs2020TransferableRecord, {
+ rpcProviderUrl: rpcUrl,
+ })
+ expect(isValid).toBe(true)
+ })
+
+ it('BBS2023 transferable document', async () => {
+ const { isValid } = await verify(w3cBbs2023TransferableDoc, {
+ rpcProviderUrl: rpcUrl,
+ })
+ expect(isValid).toBe(true)
+ })
+
+ it('ECDSA transferable document', async () => {
+ const { isValid } = await verify(w3cEcdsaTransferableDoc, {
+ rpcProviderUrl: rpcUrl,
+ })
+ expect(isValid).toBe(true)
+ })
+ }
+ )
+
+ // ββ W3C invalid documents ββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe.skipIf(!getRpcUrl('101010'))(
+ 'W3C documents expected to be invalid',
+ () => {
+ it('revoked ECDSA W3C document β invalid', async () => {
+ const rpcUrl = getRpcUrl('101010') // Stability
+
+ const { isValid } = await verify(w3cRevokedDoc, {
+ rpcProviderUrl: rpcUrl,
+ })
+ expect(isValid).toBe(false)
+ })
+ }
+ )
+
+ // ββ OA blockchain documents ββββββββββββββββββββββββββββ
+
+ describe.skipIf(!getRpcUrl('101010'))(
+ 'OA blockchain documents β valid',
+ () => {
+ const stabilityRpcUrl = getRpcUrl('101010') // Stability
+
+ it('OA v2 DNS-TXT docstore', async () => {
+ const { isValid } = await verify(oaDnsTxtDocstoreV2, {
+ rpcProviderUrl: stabilityRpcUrl,
+ })
+ expect(isValid).toBe(true)
+ })
+
+ it('OA v2 DNS-TXT token registry', async () => {
+ const { isValid } = await verify(oaDnsTxtTokenRegistryV2, {
+ rpcProviderUrl: stabilityRpcUrl,
+ })
+ expect(isValid).toBe(true)
+ })
+
+ it('OA v3 DNS-TXT docstore', async () => {
+ const { isValid } = await verify(oaDnsTxtDocstoreV3, {
+ rpcProviderUrl: stabilityRpcUrl,
+ })
+ expect(isValid).toBe(true)
+ })
+
+ it('OA v3 DNS-TXT token registry', async () => {
+ const { isValid } = await verify(oaDnsTxtTokenRegistryV3, {
+ rpcProviderUrl: stabilityRpcUrl,
+ })
+ expect(isValid).toBe(true)
+ })
+ }
+ )
+
+ // ββ Documents without network field (need explicit RPC) ββββββββββββββββββ
+
+ describe('Documents without embedded network β valid with explicit RPC', () => {
+ it.skipIf(!getRpcUrl('1'))(
+ 'OA v2 docstore (no network field) β valid with Ethereum RPC',
+ async () => {
+ const ethereumRpcUrl = getRpcUrl('1')
+ const { isValid } = await verify(oaNoNetworkEthereumV2, {
+ rpcProviderUrl: ethereumRpcUrl,
+ })
+ expect(isValid).toBe(true)
+ }
+ )
+
+ it.skipIf(!getRpcUrl('101010'))(
+ 'OA v3 token registry (no network field) β valid with Stability RPC',
+ async () => {
+ const stabilityRpcUrl = getRpcUrl('101010')
+ const { isValid } = await verify(oaNoNetworkStabilityV3, {
+ rpcProviderUrl: stabilityRpcUrl,
+ })
+ expect(isValid).toBe(true)
+ }
+ )
+ })
+})
diff --git a/src/assets/icons/ChevronDownIcon.tsx b/src/assets/icons/ChevronDownIcon.tsx
new file mode 100644
index 0000000..5a1fdbe
--- /dev/null
+++ b/src/assets/icons/ChevronDownIcon.tsx
@@ -0,0 +1,29 @@
+import React from 'react'
+
+interface ChevronDownIconProps extends React.SVGProps {
+ className?: string
+ fillColor?: string
+}
+
+const ChevronDownIcon: React.FC = ({
+ className,
+ fillColor = '#5B6571',
+ ...props
+}) => (
+
+
+
+)
+
+export default ChevronDownIcon
diff --git a/src/assets/icons/UploadIcon.tsx b/src/assets/icons/UploadIcon.tsx
new file mode 100644
index 0000000..514b990
--- /dev/null
+++ b/src/assets/icons/UploadIcon.tsx
@@ -0,0 +1,24 @@
+import React from 'react'
+
+interface UploadIconProps extends React.SVGProps {
+ className?: string
+}
+
+const UploadIcon: React.FC = ({ className, ...props }) => (
+
+
+
+)
+
+export default UploadIcon
diff --git a/src/components/common/AttachmentDropzone/AttachmentDropzone.tsx b/src/components/common/AttachmentDropzone/AttachmentDropzone.tsx
new file mode 100644
index 0000000..f7aad12
--- /dev/null
+++ b/src/components/common/AttachmentDropzone/AttachmentDropzone.tsx
@@ -0,0 +1,93 @@
+import React from 'react'
+
+interface AttachmentDropzoneProps {
+ isDarkMode: boolean
+ dragActive: boolean
+ onDragEnter: React.DragEventHandler
+ onDragLeave: React.DragEventHandler
+ onDragOver: React.DragEventHandler
+ onDrop: React.DragEventHandler
+ onFileInput: React.ChangeEventHandler
+ fileInfoText: string
+}
+
+const AttachmentDropzone = ({
+ isDarkMode,
+ dragActive,
+ onDragEnter,
+ onDragLeave,
+ onDragOver,
+ onDrop,
+ onFileInput,
+ fileInfoText,
+}: AttachmentDropzoneProps) => {
+ const fileInputRef = React.useRef(null)
+
+ return (
+
+
+ Attachment
+
+
+
+
+
+ Drop files here to upload screenshots
+
+
+
+
+
fileInputRef.current?.click()}
+ aria-label="Browse Files"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default AttachmentDropzone
diff --git a/src/components/common/AttachmentDropzone/index.ts b/src/components/common/AttachmentDropzone/index.ts
new file mode 100644
index 0000000..e723380
--- /dev/null
+++ b/src/components/common/AttachmentDropzone/index.ts
@@ -0,0 +1 @@
+export { default } from './AttachmentDropzone'
diff --git a/src/components/common/AttachmentFileList/AttachmentFileList.tsx b/src/components/common/AttachmentFileList/AttachmentFileList.tsx
new file mode 100644
index 0000000..6c60333
--- /dev/null
+++ b/src/components/common/AttachmentFileList/AttachmentFileList.tsx
@@ -0,0 +1,58 @@
+import React from 'react'
+import { FileItem } from '@/components/common/FileItem'
+import type { AttachmentItem } from '@/types/attachment'
+
+interface AttachmentFileListProps {
+ attachments: AttachmentItem[]
+ onRemove: (_id: string) => void
+ onClearAll: () => void
+ isDarkMode: boolean
+}
+
+export function AttachmentFileList({
+ attachments,
+ onRemove,
+ onClearAll,
+ isDarkMode,
+}: AttachmentFileListProps) {
+ if (attachments.length === 0) return null
+
+ return (
+
+
+
+
+
+
+ Clear All Files
+
+
+
+ {attachments.map(item => (
+
+ ))}
+
+
+ )
+}
diff --git a/src/components/common/AttachmentFileList/index.ts b/src/components/common/AttachmentFileList/index.ts
new file mode 100644
index 0000000..3937c2f
--- /dev/null
+++ b/src/components/common/AttachmentFileList/index.ts
@@ -0,0 +1 @@
+export { AttachmentFileList } from './AttachmentFileList'
diff --git a/src/components/common/FieldError/FieldError.tsx b/src/components/common/FieldError/FieldError.tsx
new file mode 100644
index 0000000..73129d8
--- /dev/null
+++ b/src/components/common/FieldError/FieldError.tsx
@@ -0,0 +1,37 @@
+import clsx from 'clsx'
+import React from 'react'
+
+interface FieldErrorProps {
+ message: string
+ id?: string
+ containerClassName?: string
+ textClassName?: string
+ iconClassName?: string
+}
+
+export function FieldError({
+ message,
+ id,
+ containerClassName,
+ textClassName,
+ iconClassName,
+}: FieldErrorProps) {
+ return (
+
+
+ {message}
+
+ )
+}
diff --git a/src/components/common/FieldError/index.ts b/src/components/common/FieldError/index.ts
new file mode 100644
index 0000000..420ade2
--- /dev/null
+++ b/src/components/common/FieldError/index.ts
@@ -0,0 +1 @@
+export { FieldError } from './FieldError'
diff --git a/src/components/common/FileItem/FileItem.tsx b/src/components/common/FileItem/FileItem.tsx
new file mode 100644
index 0000000..7c4ba11
--- /dev/null
+++ b/src/components/common/FileItem/FileItem.tsx
@@ -0,0 +1,118 @@
+import React from 'react'
+import { truncateFilename } from '@/types/attachment'
+import type { AttachmentItem } from '@/types/attachment'
+
+interface FileItemProps {
+ item: AttachmentItem
+ onRemove: (_id: string) => void
+ isDarkMode: boolean
+}
+
+export function FileItem({ item, onRemove, isDarkMode }: FileItemProps) {
+ const { id, filename, status, progress, error, previewUrl } = item
+ const displayName = truncateFilename(filename, 36)
+ const isUploading = status === 'uploading'
+ const isError = status === 'error'
+
+ const getStatusLabel = () => {
+ if (isUploading) {
+ return `Uploading - ${progress}%`
+ }
+ if (status === 'uploaded') {
+ return 'Uploaded'
+ }
+ if (status === 'pending') {
+ return ''
+ }
+ return error || 'Error'
+ }
+
+ return (
+
+
+ {isError ? (
+
!
+ ) : previewUrl ? (
+
+ ) : isUploading ? (
+
+
+
+
+
+
+ ) : (
+
+ Preview
+
+ )}
+
+
+
+ {displayName}
+
+
+ {getStatusLabel()}
+
+
+
onRemove(id)}
+ className={`flex-shrink-0 p-1 rounded transition-colors ${isDarkMode ? 'hover:bg-white/10 text-neutral-50' : 'hover:bg-black/10 text-neutral-20'}`}
+ aria-label={`Remove ${filename}`}
+ >
+
+
+
+
+
+ )
+}
diff --git a/src/components/common/FileItem/index.ts b/src/components/common/FileItem/index.ts
new file mode 100644
index 0000000..1ef09e0
--- /dev/null
+++ b/src/components/common/FileItem/index.ts
@@ -0,0 +1 @@
+export { FileItem } from './FileItem'
diff --git a/src/components/common/FormAlert/FormAlert.test.tsx b/src/components/common/FormAlert/FormAlert.test.tsx
new file mode 100644
index 0000000..f3df005
--- /dev/null
+++ b/src/components/common/FormAlert/FormAlert.test.tsx
@@ -0,0 +1,35 @@
+import { describe, it, expect } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import FormAlert from './FormAlert'
+
+describe('FormAlert', () => {
+ it('renders nothing when no error or success is provided', () => {
+ const { container } = render( )
+ expect(container.firstChild).toBeNull()
+ })
+
+ it('renders nothing when error and success are null', () => {
+ const { container } = render(
+
+ )
+ expect(container.firstChild).toBeNull()
+ })
+
+ it('renders error message', () => {
+ render( )
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument()
+ })
+
+ it('renders success message', () => {
+ render( )
+ expect(screen.getByText('Request submitted')).toBeInTheDocument()
+ })
+
+ it('prioritises error over success when both are provided', () => {
+ render(
+
+ )
+ expect(screen.getByText('Error msg')).toBeInTheDocument()
+ expect(screen.queryByText('Success msg')).not.toBeInTheDocument()
+ })
+})
diff --git a/src/components/common/FormAlert/FormAlert.tsx b/src/components/common/FormAlert/FormAlert.tsx
new file mode 100644
index 0000000..229e59b
--- /dev/null
+++ b/src/components/common/FormAlert/FormAlert.tsx
@@ -0,0 +1,37 @@
+interface FormAlertProps {
+ isDarkMode: boolean
+ error?: string | null
+ success?: string | null
+}
+
+const FormAlert = ({ isDarkMode, error, success }: FormAlertProps) => {
+ const message = error || success
+ if (!message) return null
+
+ return (
+
+ {error && (
+
+ )}
+
{message}
+
+ )
+}
+
+export default FormAlert
diff --git a/src/components/common/FormAlert/index.ts b/src/components/common/FormAlert/index.ts
new file mode 100644
index 0000000..db211e9
--- /dev/null
+++ b/src/components/common/FormAlert/index.ts
@@ -0,0 +1 @@
+export { default } from './FormAlert'
diff --git a/src/components/common/Icons/Icons.test.tsx b/src/components/common/Icons/Icons.test.tsx
new file mode 100644
index 0000000..589a279
--- /dev/null
+++ b/src/components/common/Icons/Icons.test.tsx
@@ -0,0 +1,135 @@
+import { describe, it, expect } from 'vitest'
+import { render } from '@testing-library/react'
+import {
+ CheckCircle,
+ CrossCircle,
+ QRCodeIcon,
+ PrinterIcon,
+ DownloadIcon,
+ FileIcon,
+} from './Icons'
+
+describe('CheckCircle', () => {
+ it('renders an SVG element', () => {
+ const { container } = render( )
+ const svg = container.querySelector('svg')
+ expect(svg).toBeTruthy()
+ expect(svg?.getAttribute('width')).toBe('20')
+ expect(svg?.getAttribute('height')).toBe('20')
+ })
+
+ it('uses green stroke color', () => {
+ const { container } = render( )
+ const path = container.querySelector('path')
+ expect(path?.getAttribute('stroke')).toBe('#3AAF86')
+ })
+})
+
+describe('CrossCircle', () => {
+ it('renders an SVG element', () => {
+ const { container } = render( )
+ const svg = container.querySelector('svg')
+ expect(svg).toBeTruthy()
+ expect(svg?.getAttribute('width')).toBe('20')
+ })
+
+ it('uses red stroke color', () => {
+ const { container } = render( )
+ const circle = container.querySelector('circle')
+ expect(circle?.getAttribute('stroke')).toBe('#ef4444')
+ })
+})
+
+describe('QRCodeIcon', () => {
+ it('renders a 24x24 SVG', () => {
+ const { container } = render( )
+ const svg = container.querySelector('svg')
+ expect(svg?.getAttribute('width')).toBe('24')
+ expect(svg?.getAttribute('height')).toBe('24')
+ })
+})
+
+describe('PrinterIcon', () => {
+ it('renders a 24x24 SVG', () => {
+ const { container } = render( )
+ const svg = container.querySelector('svg')
+ expect(svg?.getAttribute('width')).toBe('24')
+ expect(svg?.getAttribute('height')).toBe('24')
+ })
+})
+
+describe('DownloadIcon', () => {
+ it('renders a 24x24 SVG', () => {
+ const { container } = render( )
+ const svg = container.querySelector('svg')
+ expect(svg?.getAttribute('width')).toBe('24')
+ expect(svg?.getAttribute('height')).toBe('24')
+ })
+})
+
+describe('FileIcon', () => {
+ it('renders an SVG element', () => {
+ const { container } = render(
+
+ )
+ const svg = container.querySelector('svg')
+ expect(svg).toBeTruthy()
+ expect(svg?.getAttribute('width')).toBe('40')
+ expect(svg?.getAttribute('height')).toBe('48')
+ })
+
+ it('shows PDF extension badge for PDF files', () => {
+ const { container } = render(
+
+ )
+ const text = container.querySelector('text')
+ expect(text?.textContent).toBe('PDF')
+ })
+
+ it('shows JSON extension badge for JSON files', () => {
+ const { container } = render(
+
+ )
+ const text = container.querySelector('text')
+ expect(text?.textContent).toBe('JSON')
+ })
+
+ it('uses red color for PDF badge', () => {
+ const { container } = render(
+
+ )
+ const rect = container.querySelectorAll('rect')
+ // The extension badge rect
+ const badge = Array.from(rect).find(
+ r => r.getAttribute('fill') === '#DC2626'
+ )
+ expect(badge).toBeTruthy()
+ })
+
+ it('uses blue color for PNG badge', () => {
+ const { container } = render(
+
+ )
+ const rect = container.querySelectorAll('rect')
+ const badge = Array.from(rect).find(
+ r => r.getAttribute('fill') === '#2563EB'
+ )
+ expect(badge).toBeTruthy()
+ })
+
+ it('falls back to FILE for unknown types', () => {
+ const { container } = render(
+
+ )
+ const text = container.querySelector('text')
+ expect(text?.textContent).toBe('FILE')
+ })
+
+ it('prefers filename extension over mime type', () => {
+ const { container } = render(
+
+ )
+ const text = container.querySelector('text')
+ expect(text?.textContent).toBe('CSV')
+ })
+})
diff --git a/src/components/common/Icons/Icons.tsx b/src/components/common/Icons/Icons.tsx
new file mode 100644
index 0000000..467b6b0
--- /dev/null
+++ b/src/components/common/Icons/Icons.tsx
@@ -0,0 +1,164 @@
+import { getFileExtension } from '../../../utils/helper'
+
+export const CheckCircle = () => (
+
+
+
+
+)
+
+export const CrossCircle = () => (
+
+
+
+
+)
+
+export const QRCodeIcon = () => (
+
+
+
+)
+
+export const PrinterIcon = () => (
+
+
+
+)
+
+export const DownloadIcon = () => (
+
+
+
+)
+
+const FILE_TYPE_COLORS: Record = {
+ PDF: '#DC2626',
+ PNG: '#2563EB',
+ JPG: '#2563EB',
+ JPEG: '#2563EB',
+ JSON: '#D97706',
+ CSV: '#059669',
+ XML: '#7C3AED',
+ TXT: '#4B5563',
+}
+
+export const FileIcon = ({
+ filename,
+ type,
+}: {
+ filename: string
+ type: string
+}) => {
+ const ext = getFileExtension(filename, type?.toLowerCase() || '')
+ const color = FILE_TYPE_COLORS[ext] || '#5B5BB3'
+
+ return (
+
+ {/* File body */}
+
+ {/* Folded corner */}
+
+ {/* Border */}
+
+ {/* Extension badge */}
+
+
+ {ext}
+
+
+ )
+}
diff --git a/src/components/common/Icons/index.ts b/src/components/common/Icons/index.ts
new file mode 100644
index 0000000..b29c6a8
--- /dev/null
+++ b/src/components/common/Icons/index.ts
@@ -0,0 +1,8 @@
+export {
+ CheckCircle,
+ CrossCircle,
+ QRCodeIcon,
+ PrinterIcon,
+ DownloadIcon,
+ FileIcon,
+} from './Icons'
diff --git a/src/components/common/LinkButton/LinkButton.test.tsx b/src/components/common/LinkButton/LinkButton.test.tsx
new file mode 100644
index 0000000..d846e8c
--- /dev/null
+++ b/src/components/common/LinkButton/LinkButton.test.tsx
@@ -0,0 +1,63 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import LinkButton from './LinkButton'
+
+describe('LinkButton', () => {
+ it('renders children correctly', () => {
+ render(
+
+ Click me
+
+ )
+ expect(screen.getByText('Click me')).toBeInTheDocument()
+ })
+
+ it('renders as disabled when no href provided', () => {
+ render(Disabled )
+ const button = screen.getByText('Disabled').closest('a')
+ expect(button).toHaveClass('opacity-50')
+ expect(button).toHaveClass('cursor-not-allowed')
+ })
+
+ it('renders as disabled when isDisabled is true', () => {
+ render(
+
+ Disabled
+
+ )
+ const button = screen.getByText('Disabled').closest('a')
+ expect(button).toHaveClass('opacity-50')
+ })
+
+ it('applies dark mode styles', () => {
+ render(
+
+ Dark Mode
+
+ )
+ const button = screen.getByText('Dark Mode').closest('a')
+ expect(button).toHaveClass('text-black')
+ expect(button).toHaveClass('bg-primary-60')
+ })
+
+ it('applies light mode styles', () => {
+ render(
+
+ Light Mode
+
+ )
+ const button = screen.getByText('Light Mode').closest('a')
+ expect(button).toHaveClass('text-white')
+ expect(button).toHaveClass('bg-primary-60')
+ })
+
+ it('applies custom className', () => {
+ render(
+
+ Custom
+
+ )
+ const button = screen.getByText('Custom').closest('a')
+ expect(button).toHaveClass('custom-class')
+ })
+})
diff --git a/src/components/common/LinkButton/LinkButton.tsx b/src/components/common/LinkButton/LinkButton.tsx
new file mode 100644
index 0000000..0852a0a
--- /dev/null
+++ b/src/components/common/LinkButton/LinkButton.tsx
@@ -0,0 +1,46 @@
+import clsx from 'clsx'
+import { ReactNode } from 'react'
+
+interface LinkButtonProps {
+ className?: string
+ href?: string
+ children: ReactNode
+ isDarkMode: boolean
+ isDisabled?: boolean
+}
+
+const LinkButton = ({
+ className = '',
+ href,
+ children,
+ isDarkMode,
+ isDisabled,
+}: LinkButtonProps) => {
+ const disabled = isDisabled || !href
+
+ return (
+ {
+ if (disabled) {
+ event.preventDefault()
+ }
+ }}
+ className={clsx(
+ 'bg-primary-60',
+ isDarkMode ? 'bg-primary-60 text-black' : 'text-white',
+ 'inline-flex w-fit px-4 py-2 rounded-lg font-bold',
+ disabled ? 'cursor-not-allowed opacity-50 pointer-events-none' : '',
+ className
+ )}
+ >
+ {children}
+
+ )
+}
+
+export default LinkButton
diff --git a/src/components/common/LinkButton/index.ts b/src/components/common/LinkButton/index.ts
new file mode 100644
index 0000000..a6cb95b
--- /dev/null
+++ b/src/components/common/LinkButton/index.ts
@@ -0,0 +1 @@
+export { default } from './LinkButton'
diff --git a/src/components/common/Logo/Logo.test.tsx b/src/components/common/Logo/Logo.test.tsx
new file mode 100644
index 0000000..c323b85
--- /dev/null
+++ b/src/components/common/Logo/Logo.test.tsx
@@ -0,0 +1,52 @@
+import { describe, it, expect } from 'vitest'
+import { render } from '@testing-library/react'
+import Logo from './Logo'
+
+describe('Logo Component', () => {
+ it('renders logo with icon and text', () => {
+ const { container } = render( )
+
+ // Check if logo wrapper exists
+ const logoWrapper = container.querySelector('.logo-wrapper')
+ expect(logoWrapper).toBeInTheDocument()
+
+ // Check if both SVGs are rendered
+ const svgs = container.querySelectorAll('svg')
+ expect(svgs).toHaveLength(2)
+ })
+
+ it('renders with light mode colors', () => {
+ const { container } = render( )
+
+ const logoText = container.querySelector('.logo-light')
+ expect(logoText).toBeInTheDocument()
+ expect(logoText).not.toHaveClass('logo-dark')
+ })
+
+ it('renders with dark mode colors', () => {
+ const { container } = render( )
+
+ const logoText = container.querySelector('.logo-dark')
+ expect(logoText).toBeInTheDocument()
+ expect(logoText).not.toHaveClass('logo-light')
+ })
+
+ it('has a link to homepage', () => {
+ const { container } = render( )
+
+ const link = container.querySelector('a[href="/"]')
+ expect(link).toBeInTheDocument()
+ })
+
+ it('generates unique gradient IDs', () => {
+ const { container: container1 } = render( )
+ const { container: container2 } = render( )
+
+ const gradient1 = container1.querySelector('linearGradient')
+ const gradient2 = container2.querySelector('linearGradient')
+
+ expect(gradient1?.id).toBeDefined()
+ expect(gradient2?.id).toBeDefined()
+ expect(gradient1?.id).not.toBe(gradient2?.id)
+ })
+})
diff --git a/src/components/common/Logo/Logo.tsx b/src/components/common/Logo/Logo.tsx
new file mode 100644
index 0000000..101fda2
--- /dev/null
+++ b/src/components/common/Logo/Logo.tsx
@@ -0,0 +1,58 @@
+interface LogoProps {
+ isDarkMode: boolean
+}
+
+const Logo = ({ isDarkMode }: LogoProps) => {
+ const gradientId = `logo-gradient-${Math.random().toString(36).substr(2, 9)}`
+
+ return (
+
+ )
+}
+
+export default Logo
diff --git a/src/components/common/Logo/index.ts b/src/components/common/Logo/index.ts
new file mode 100644
index 0000000..93dce23
--- /dev/null
+++ b/src/components/common/Logo/index.ts
@@ -0,0 +1 @@
+export { default } from './Logo'
diff --git a/src/components/common/Navbar/Navbar.test.tsx b/src/components/common/Navbar/Navbar.test.tsx
new file mode 100644
index 0000000..e3f0653
--- /dev/null
+++ b/src/components/common/Navbar/Navbar.test.tsx
@@ -0,0 +1,127 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { render, screen, fireEvent } from '@testing-library/react'
+import Navbar from './Navbar'
+
+describe('Navbar Component', () => {
+ const mockSetIsDarkMode = vi.fn()
+
+ beforeEach(() => {
+ mockSetIsDarkMode.mockClear()
+ })
+
+ it('renders navbar with logo', () => {
+ render( )
+
+ // Logo should be present
+ const logos = screen.getAllByRole('link', { name: '' })
+ expect(logos.length).toBeGreaterThan(0)
+ })
+
+ it('renders navigation links', () => {
+ render( )
+
+ expect(screen.getByText('Home')).toBeInTheDocument()
+ expect(screen.getByText('Ecosystem')).toBeInTheDocument()
+ expect(screen.getByText('Gallery')).toBeInTheDocument()
+ expect(screen.getByText('News & Updates')).toBeInTheDocument()
+ })
+
+ it('renders Contact Us button', () => {
+ render( )
+
+ const contactButtons = screen.getAllByText('Contact Us')
+ expect(contactButtons.length).toBeGreaterThan(0)
+ })
+
+ it('toggles dark mode when sun icon is clicked', () => {
+ render( )
+
+ // Find sun icon button (first theme toggle button)
+ const themeButtons = screen.getAllByRole('button')
+ const sunButton = themeButtons.find(btn =>
+ btn.querySelector('svg path[d*="M12 19.3755"]')
+ )
+
+ if (sunButton) {
+ fireEvent.click(sunButton)
+ expect(mockSetIsDarkMode).toHaveBeenCalledWith(false)
+ }
+ })
+
+ it('toggles dark mode when moon icon is clicked', () => {
+ render( )
+
+ // Find moon icon button
+ const themeButtons = screen.getAllByRole('button')
+ const moonButton = themeButtons.find(btn =>
+ btn.querySelector('svg path[d*="M10.0762"]')
+ )
+
+ if (moonButton) {
+ fireEvent.click(moonButton)
+ expect(mockSetIsDarkMode).toHaveBeenCalledWith(true)
+ }
+ })
+
+ it('opens mobile menu when hamburger is clicked', () => {
+ render( )
+
+ // Find hamburger button
+ const hamburgerButton = screen
+ .getAllByRole('button')
+ .find(btn => btn.querySelector('svg path[d*="M20.1694 16.75"]'))
+
+ expect(hamburgerButton).toBeInTheDocument()
+
+ if (hamburgerButton) {
+ fireEvent.click(hamburgerButton)
+
+ // Mobile menu should appear with navigation items
+ const homeLinks = screen.getAllByText('Home')
+ expect(homeLinks.length).toBeGreaterThan(1) // Desktop + Mobile
+ }
+ })
+
+ it('opens ecosystem dropdown on hover', () => {
+ render( )
+
+ const ecosystemButtons = screen.getAllByText('Ecosystem')
+ const desktopEcosystemButton = ecosystemButtons[0]
+
+ // Hover over Ecosystem
+ fireEvent.mouseEnter(desktopEcosystemButton)
+
+ // Dropdown items should appear
+ const placeholders = screen.getAllByText('Placeholder')
+ expect(placeholders.length).toBeGreaterThan(0)
+ })
+
+ it('applies correct CSS classes for light mode', () => {
+ const { container } = render(
+
+ )
+
+ const nav = container.querySelector('nav')
+ expect(nav).toHaveClass('navbar-light')
+ expect(nav).not.toHaveClass('navbar-dark')
+ })
+
+ it('applies correct CSS classes for dark mode', () => {
+ const { container } = render(
+
+ )
+
+ const nav = container.querySelector('nav')
+ expect(nav).toHaveClass('navbar-dark')
+ expect(nav).not.toHaveClass('navbar-light')
+ })
+
+ it('renders contact button with correct CSS class', () => {
+ const { container } = render(
+
+ )
+
+ const contactButtons = container.querySelectorAll('.contact-button')
+ expect(contactButtons.length).toBeGreaterThan(0)
+ })
+})
diff --git a/src/components/common/Navbar/Navbar.tsx b/src/components/common/Navbar/Navbar.tsx
new file mode 100644
index 0000000..13bffa8
--- /dev/null
+++ b/src/components/common/Navbar/Navbar.tsx
@@ -0,0 +1,636 @@
+import { useState, useRef, useEffect, Dispatch, SetStateAction } from 'react'
+import Logo from '../Logo'
+
+interface NavbarProps {
+ isDarkMode: boolean
+ setIsDarkMode: Dispatch>
+}
+
+const Navbar = ({ isDarkMode, setIsDarkMode }: NavbarProps) => {
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
+ const [isEcosystemOpen, setIsEcosystemOpen] = useState(false)
+ const navRef = useRef(null)
+
+ useEffect(() => {
+ const handleClickOutside = (e: MouseEvent) => {
+ if (
+ isMobileMenuOpen &&
+ navRef.current &&
+ !navRef.current.contains(e.target as Node)
+ ) {
+ setIsMobileMenuOpen(false)
+ setIsEcosystemOpen(false)
+ }
+ }
+ document.addEventListener('mousedown', handleClickOutside)
+ return () => document.removeEventListener('mousedown', handleClickOutside)
+ }, [isMobileMenuOpen])
+
+ return (
+
+
+ {/* Tablet View - Centered Logo with Hamburger */}
+
+ {/* Hamburger Menu Button */}
+
+
{
+ setIsMobileMenuOpen(!isMobileMenuOpen)
+ if (isMobileMenuOpen) {
+ setIsEcosystemOpen(false)
+ }
+ }}
+ aria-label="Toggle mobile menu"
+ aria-expanded={isMobileMenuOpen}
+ className="w-10 h-10 flex items-center justify-center rounded-lg transition-colors duration-200"
+ style={{
+ backgroundColor: 'transparent',
+ }}
+ onMouseEnter={e => {
+ e.currentTarget.style.backgroundColor = isDarkMode
+ ? 'rgba(255, 255, 255, 0.1)'
+ : 'rgba(0, 0, 0, 0.05)'
+ }}
+ onMouseLeave={e => {
+ e.currentTarget.style.backgroundColor = 'transparent'
+ }}
+ >
+
+
+
+
+
+
+ {/* Centered Logo */}
+
+
+ {/* Spacer for balance */}
+
+
+
+ {/* Desktop View - Full Navbar */}
+
+ {/* Logo */}
+
+
+ {/* Navigation Tabs */}
+
+
+
+
setIsEcosystemOpen(!isEcosystemOpen)}
+ aria-haspopup="true"
+ aria-expanded={isEcosystemOpen}
+ aria-controls="ecosystem-menu-desktop"
+ className="min-w-[40px] min-h-[40px] flex items-center justify-center px-1 py-[5px] rounded-lg transition-colors duration-200"
+ style={{
+ backgroundColor: 'transparent',
+ }}
+ onMouseEnter={e => {
+ setIsEcosystemOpen(true)
+ e.currentTarget.style.backgroundColor = isDarkMode
+ ? 'rgba(255, 255, 255, 0.1)'
+ : 'rgba(0, 0, 0, 0.05)'
+ }}
+ onMouseLeave={e => {
+ setIsEcosystemOpen(false)
+ e.currentTarget.style.backgroundColor = 'transparent'
+ }}
+ >
+
+ Ecosystem
+
+
+
+
+ {/* Ecosystem Dropdown */}
+ {isEcosystemOpen && (
+
+ )}
+
+
+
+
+
+ {/* Right Side - Theme Toggle & CTA */}
+
+
+ {/* Sun Icon - Primary Color */}
+
+
setIsDarkMode(false)}
+ aria-label="Switch to light mode"
+ aria-pressed={!isDarkMode}
+ className="min-w-[40px] min-h-[40px] flex items-center justify-center p-[5px] rounded-lg overflow-hidden transition-colors duration-200"
+ style={{
+ backgroundColor: 'transparent',
+ }}
+ onMouseEnter={e => {
+ e.currentTarget.style.backgroundColor = isDarkMode
+ ? 'rgba(255, 255, 255, 0.1)'
+ : 'rgba(0, 0, 0, 0.05)'
+ }}
+ onMouseLeave={e => {
+ e.currentTarget.style.backgroundColor = 'transparent'
+ }}
+ >
+
+
+
+
+
+ {/* Moon Icon - Neutral Color */}
+
+
setIsDarkMode(true)}
+ aria-label="Switch to dark mode"
+ aria-pressed={isDarkMode}
+ className="min-w-[40px] min-h-[40px] flex items-center justify-center p-[5px] rounded-lg overflow-hidden transition-colors duration-200"
+ style={{
+ backgroundColor: 'transparent',
+ }}
+ onMouseEnter={e => {
+ e.currentTarget.style.backgroundColor = isDarkMode
+ ? 'rgba(255, 255, 255, 0.1)'
+ : 'rgba(0, 0, 0, 0.05)'
+ }}
+ onMouseLeave={e => {
+ e.currentTarget.style.backgroundColor = 'transparent'
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ {/* Mobile Menu Dropdown */}
+ {isMobileMenuOpen && (
+
+
+
{
+ e.currentTarget.style.backgroundColor = isDarkMode
+ ? 'rgba(255, 255, 255, 0.1)'
+ : 'rgba(0, 0, 0, 0.05)'
+ }}
+ onMouseLeave={e => {
+ e.currentTarget.style.backgroundColor = 'transparent'
+ }}
+ >
+ Home
+
+
+
setIsEcosystemOpen(!isEcosystemOpen)}
+ aria-haspopup="true"
+ aria-expanded={isEcosystemOpen}
+ aria-controls="ecosystem-menu-mobile"
+ className="w-full px-4 py-3 text-left text-sm font-bold font-['Gilroy'] rounded-lg transition-colors duration-200 flex items-center justify-between"
+ style={{
+ color: isDarkMode ? '#808894' : '#5B6571',
+ backgroundColor: 'transparent',
+ }}
+ onMouseEnter={e => {
+ e.currentTarget.style.backgroundColor = isDarkMode
+ ? 'rgba(255, 255, 255, 0.1)'
+ : 'rgba(0, 0, 0, 0.05)'
+ }}
+ onMouseLeave={e => {
+ e.currentTarget.style.backgroundColor = 'transparent'
+ }}
+ >
+ Ecosystem
+
+
+
+
+
+ {/* Ecosystem Sub-items */}
+ {isEcosystemOpen && (
+
+ )}
+
+
{
+ e.currentTarget.style.backgroundColor = isDarkMode
+ ? 'rgba(255, 255, 255, 0.1)'
+ : 'rgba(0, 0, 0, 0.05)'
+ }}
+ onMouseLeave={e => {
+ e.currentTarget.style.backgroundColor = 'transparent'
+ }}
+ >
+ Gallery
+
+
{
+ e.currentTarget.style.backgroundColor = isDarkMode
+ ? 'rgba(255, 255, 255, 0.1)'
+ : 'rgba(0, 0, 0, 0.05)'
+ }}
+ onMouseLeave={e => {
+ e.currentTarget.style.backgroundColor = 'transparent'
+ }}
+ >
+ News & Updates
+
+ {/* Theme Toggle in Mobile */}
+
+ {/* Sun Icon */}
+
setIsDarkMode(false)}
+ aria-label="Switch to light mode"
+ aria-pressed={!isDarkMode}
+ className="min-w-[40px] min-h-[40px] flex items-center justify-center p-[5px] rounded-lg overflow-hidden transition-colors duration-200"
+ style={{
+ backgroundColor: 'transparent',
+ }}
+ onMouseEnter={e => {
+ e.currentTarget.style.backgroundColor = isDarkMode
+ ? 'rgba(255, 255, 255, 0.1)'
+ : 'rgba(0, 0, 0, 0.05)'
+ }}
+ onMouseLeave={e => {
+ e.currentTarget.style.backgroundColor = 'transparent'
+ }}
+ >
+
+
+
+
+ {/* Moon Icon */}
+
setIsDarkMode(true)}
+ aria-label="Switch to dark mode"
+ aria-pressed={isDarkMode}
+ className="min-w-[40px] min-h-[40px] flex items-center justify-center p-[5px] rounded-lg overflow-hidden transition-colors duration-200"
+ style={{
+ backgroundColor: 'transparent',
+ }}
+ onMouseEnter={e => {
+ e.currentTarget.style.backgroundColor = isDarkMode
+ ? 'rgba(255, 255, 255, 0.1)'
+ : 'rgba(0, 0, 0, 0.05)'
+ }}
+ onMouseLeave={e => {
+ e.currentTarget.style.backgroundColor = 'transparent'
+ }}
+ >
+
+
+
+
+
+
+
+
+ )}
+
+ )
+}
+
+export default Navbar
diff --git a/src/components/common/Navbar/index.ts b/src/components/common/Navbar/index.ts
new file mode 100644
index 0000000..e6400ae
--- /dev/null
+++ b/src/components/common/Navbar/index.ts
@@ -0,0 +1 @@
+export { default } from './Navbar'
diff --git a/src/components/common/Overlay/Overlay.test.tsx b/src/components/common/Overlay/Overlay.test.tsx
new file mode 100644
index 0000000..969ebc2
--- /dev/null
+++ b/src/components/common/Overlay/Overlay.test.tsx
@@ -0,0 +1,74 @@
+import { describe, it, expect, vi } from 'vitest'
+import { render, fireEvent } from '@testing-library/react'
+import Overlay from './Overlay'
+
+describe('Overlay', () => {
+ it('renders children', () => {
+ const { getByText } = render(
+
+ Hello
+
+ )
+ expect(getByText('Hello')).toBeTruthy()
+ })
+
+ it('applies custom className', () => {
+ const { container } = render(
+
+ Content
+
+ )
+ const overlay = container.querySelector('.overlay')
+ expect(overlay?.classList.contains('custom-class')).toBe(true)
+ })
+
+ it('has role="dialog" and aria-modal="true"', () => {
+ const { container } = render(
+
+ Content
+
+ )
+ const overlay = container.querySelector('.overlay')
+ expect(overlay?.getAttribute('role')).toBe('dialog')
+ expect(overlay?.getAttribute('aria-modal')).toBe('true')
+ })
+
+ it('calls onClose when clicking overlay background', () => {
+ const onClose = vi.fn()
+ const { container } = render(
+
+ Content
+
+ )
+ const overlay = container.querySelector('.overlay') as HTMLElement
+ fireEvent.click(overlay)
+ expect(onClose).toHaveBeenCalledOnce()
+ })
+
+ it('does not call onClose when clicking children', () => {
+ const onClose = vi.fn()
+ const { getByText } = render(
+
+ Content
+
+ )
+ fireEvent.click(getByText('Content'))
+ expect(onClose).not.toHaveBeenCalled()
+ })
+
+ it('locks body scroll on mount and restores on unmount', () => {
+ document.body.style.overflow = 'auto'
+ try {
+ const { unmount } = render(
+
+ Content
+
+ )
+ expect(document.body.style.overflow).toBe('hidden')
+ unmount()
+ expect(document.body.style.overflow).toBe('auto')
+ } finally {
+ document.body.style.overflow = ''
+ }
+ })
+})
diff --git a/src/components/common/Overlay/Overlay.tsx b/src/components/common/Overlay/Overlay.tsx
new file mode 100644
index 0000000..b42b235
--- /dev/null
+++ b/src/components/common/Overlay/Overlay.tsx
@@ -0,0 +1,39 @@
+import React, { useEffect } from 'react'
+
+interface OverlayProps {
+ children: React.ReactNode
+ className?: string
+ onClose?: () => void
+}
+
+const Overlay: React.FC = ({ children, className, onClose }) => {
+ const handleOverlayClick = (e: React.MouseEvent) => {
+ // Only trigger onClose if clicking the overlay itself, not its children
+ if (e.target === e.currentTarget && onClose) {
+ onClose()
+ }
+ }
+
+ // Lock body scroll when overlay is mounted
+ useEffect(() => {
+ const originalOverflow = document.body.style.overflow
+ document.body.style.overflow = 'hidden'
+
+ return () => {
+ document.body.style.overflow = originalOverflow
+ }
+ }, [])
+
+ return (
+
+ )
+}
+
+export default Overlay
diff --git a/src/components/common/Overlay/index.tsx b/src/components/common/Overlay/index.tsx
new file mode 100644
index 0000000..3638ac7
--- /dev/null
+++ b/src/components/common/Overlay/index.tsx
@@ -0,0 +1 @@
+export { default } from './Overlay'
diff --git a/src/components/common/PrimaryButton/PrimaryButton.test.tsx b/src/components/common/PrimaryButton/PrimaryButton.test.tsx
new file mode 100644
index 0000000..f5dc433
--- /dev/null
+++ b/src/components/common/PrimaryButton/PrimaryButton.test.tsx
@@ -0,0 +1,79 @@
+import { describe, it, expect, vi } from 'vitest'
+import { render, fireEvent } from '@testing-library/react'
+import PrimaryButton from './PrimaryButton'
+
+describe('PrimaryButton', () => {
+ it('renders children text', () => {
+ const { getByText } = render(Click Me )
+ expect(getByText('Click Me')).toBeTruthy()
+ })
+
+ it('renders as a button by default', () => {
+ const { container } = render(Test )
+ const button = container.querySelector('button')
+ expect(button).toBeTruthy()
+ expect(button?.type).toBe('button')
+ })
+
+ it('renders as a label when as="label"', () => {
+ const { container } = render(
+
+ Browse
+
+ )
+ const label = container.querySelector('label')
+ expect(label).toBeTruthy()
+ expect(label?.getAttribute('for')).toBe('file-input')
+ expect(container.querySelector('button')).toBeNull()
+ })
+
+ it('calls onClick when clicked', () => {
+ const onClick = vi.fn()
+ const { getByText } = render(
+ Click
+ )
+ fireEvent.click(getByText('Click'))
+ expect(onClick).toHaveBeenCalledOnce()
+ })
+
+ it('applies custom className', () => {
+ const { container } = render(
+ Test
+ )
+ const button = container.querySelector('button')
+ expect(button?.classList.contains('standard-button-primary')).toBe(true)
+ expect(button?.classList.contains('my-class')).toBe(true)
+ })
+
+ it('renders icon when provided', () => {
+ const { container } = render(
+ *}>
+ With Icon
+
+ )
+ const iconFrame = container.querySelector('.contextual-icon-frame')
+ expect(iconFrame).toBeTruthy()
+ })
+
+ it('does not render icon frame when no icon', () => {
+ const { container } = render(No Icon )
+ const iconFrame = container.querySelector('.contextual-icon-frame')
+ expect(iconFrame).toBeNull()
+ })
+
+ it('supports disabled state', () => {
+ const { container } = render(
+ Disabled
+ )
+ const button = container.querySelector('button')
+ expect(button?.disabled).toBe(true)
+ })
+
+ it('supports submit type', () => {
+ const { container } = render(
+ Submit
+ )
+ const button = container.querySelector('button')
+ expect(button?.type).toBe('submit')
+ })
+})
diff --git a/src/components/common/PrimaryButton/PrimaryButton.tsx b/src/components/common/PrimaryButton/PrimaryButton.tsx
new file mode 100644
index 0000000..e43abb3
--- /dev/null
+++ b/src/components/common/PrimaryButton/PrimaryButton.tsx
@@ -0,0 +1,63 @@
+import clsx from 'clsx'
+import { ReactNode } from 'react'
+
+interface PrimaryButtonProps {
+ className?: string
+ onClick?: () => void
+ children: ReactNode
+ type?: 'button' | 'submit' | 'reset'
+ disabled?: boolean
+ icon?: ReactNode
+ htmlFor?: string
+ as?: 'button' | 'label'
+}
+
+const PrimaryButton = ({
+ className = '',
+ onClick,
+ children,
+ type = 'button',
+ disabled = false,
+ icon,
+ htmlFor,
+ as = 'button',
+}: PrimaryButtonProps) => {
+ const content = (
+
+
+ {icon &&
{icon}
}
+
+
+
+ )
+
+ if (as === 'label') {
+ return (
+
+
+
+ {icon && {icon}
}
+
+
+
+
+ )
+ }
+
+ return (
+
+ {content}
+
+ )
+}
+
+export default PrimaryButton
diff --git a/src/components/common/PrimaryButton/index.ts b/src/components/common/PrimaryButton/index.ts
new file mode 100644
index 0000000..3c61480
--- /dev/null
+++ b/src/components/common/PrimaryButton/index.ts
@@ -0,0 +1 @@
+export { default } from './PrimaryButton'
diff --git a/src/components/common/Recaptcha/Recaptcha.tsx b/src/components/common/Recaptcha/Recaptcha.tsx
new file mode 100644
index 0000000..69d0840
--- /dev/null
+++ b/src/components/common/Recaptcha/Recaptcha.tsx
@@ -0,0 +1,50 @@
+import React, { useImperativeHandle, forwardRef } from 'react'
+import { useRecaptcha } from '@/hooks/useRecaptcha'
+
+declare global {
+ interface Window {
+ grecaptcha?: {
+ /**
+ * reCAPTCHA v2 global helper (loaded from https://www.google.com/recaptcha/api.js)
+ */
+ ready: (cb: () => void) => void
+ render: (container: HTMLElement, params: { sitekey: string }) => number
+ getResponse: (widgetId?: number) => string
+ reset: (widgetId?: number) => void
+ }
+ }
+}
+
+export interface RecaptchaHandle {
+ /** Returns a Promise that resolves with the reCAPTCHA v2 checkbox token. */
+ getToken: () => Promise
+ reset: () => void
+}
+
+interface RecaptchaProps {
+ siteKey: string
+ className?: string
+ onChange?: (token: string) => void
+}
+
+export const Recaptcha = forwardRef(
+ function Recaptcha({ siteKey, className, onChange }, ref) {
+ const { containerRef, getToken, reset } = useRecaptcha({
+ siteKey,
+ onChange,
+ })
+
+ useImperativeHandle(
+ ref,
+ () => ({
+ getToken,
+ reset,
+ }),
+ [getToken, reset]
+ )
+
+ if (!siteKey) return null
+
+ return
+ }
+)
diff --git a/src/components/common/Recaptcha/index.ts b/src/components/common/Recaptcha/index.ts
new file mode 100644
index 0000000..4a1edec
--- /dev/null
+++ b/src/components/common/Recaptcha/index.ts
@@ -0,0 +1,2 @@
+export { Recaptcha } from './Recaptcha'
+export type { RecaptchaHandle } from './Recaptcha'
diff --git a/src/components/common/SelectField/SelectField.tsx b/src/components/common/SelectField/SelectField.tsx
new file mode 100644
index 0000000..e51a1ff
--- /dev/null
+++ b/src/components/common/SelectField/SelectField.tsx
@@ -0,0 +1,182 @@
+import React, { useState } from 'react'
+import type { EnquiryType } from '@/pages/Contact/hooks/useContactForm'
+import { FieldError } from '@/components/common/FieldError'
+
+interface SelectFieldProps {
+ isDarkMode: boolean
+ id: string
+ label: string
+ value: EnquiryType
+ onChange: React.Dispatch>
+ required?: boolean
+ error?: string
+ onBlur?: () => void
+}
+
+const SelectField = ({
+ isDarkMode,
+ id,
+ label,
+ value,
+ onChange,
+ error,
+ onBlur,
+}: SelectFieldProps) => {
+ const options: { value: EnquiryType; label: string }[] = [
+ { value: '', label: 'Select an option.' },
+ { value: 'General_Enquiry', label: 'General Enquiry' },
+ { value: 'OpenCerts', label: 'OpenCerts' },
+ { value: 'TradeTrust', label: 'TradeTrust' },
+ ]
+
+ const selected = options.find(o => o.value === value) ?? options[0]
+ const [isOpen, setIsOpen] = useState(false)
+ const optionItems = options.slice(1)
+
+ const handleTriggerKeyDown: React.KeyboardEventHandler<
+ HTMLButtonElement
+ > = e => {
+ if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault()
+ setIsOpen(true)
+ return
+ }
+ if (e.key === 'Escape') {
+ setIsOpen(false)
+ }
+ }
+
+ const handleOptionKeyDown = (
+ e: React.KeyboardEvent,
+ index: number
+ ) => {
+ if (e.key === 'Escape') {
+ e.preventDefault()
+ setIsOpen(false)
+ return
+ }
+ if (e.key === 'ArrowDown') {
+ e.preventDefault()
+ const nextId = `${id}-option-${(index + 1) % optionItems.length}`
+ document.getElementById(nextId)?.focus()
+ return
+ }
+ if (e.key === 'ArrowUp') {
+ e.preventDefault()
+ const prevId =
+ index === 0
+ ? `${id}-option-${optionItems.length - 1}`
+ : `${id}-option-${index - 1}`
+ document.getElementById(prevId)?.focus()
+ }
+ }
+
+ return (
+
+
+ {label}
+
+
+
{
+ if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {
+ setIsOpen(false)
+ }
+ onBlur?.()
+ }}
+ >
+
setIsOpen(open => !open)}
+ onKeyDown={handleTriggerKeyDown}
+ >
+
+ {selected.label}
+
+
+
+
+
+
+ {isOpen && (
+
+ {optionItems.map((option, index) => (
+ {
+ onChange(option.value)
+ setIsOpen(false)
+ }}
+ onKeyDown={e => handleOptionKeyDown(e, index)}
+ className={`block w-full px-3 py-2 text-left text-sm font-gilroy transition-colors ${
+ value === option.value
+ ? 'bg-primary-60/10 text-primary-60'
+ : isDarkMode
+ ? 'text-neutral-60 hover:bg-white/10'
+ : 'text-neutral-10 hover:bg-neutral-10/5'
+ }`}
+ >
+ {option.label}
+
+ ))}
+
+ )}
+
+ {error &&
}
+
+ )
+}
+
+export default SelectField
diff --git a/src/components/common/SelectField/index.ts b/src/components/common/SelectField/index.ts
new file mode 100644
index 0000000..2026cf8
--- /dev/null
+++ b/src/components/common/SelectField/index.ts
@@ -0,0 +1 @@
+export { default } from './SelectField'
diff --git a/src/components/common/Spinner/Spinner.test.tsx b/src/components/common/Spinner/Spinner.test.tsx
new file mode 100644
index 0000000..d5501e3
--- /dev/null
+++ b/src/components/common/Spinner/Spinner.test.tsx
@@ -0,0 +1,52 @@
+import { describe, it, expect } from 'vitest'
+import { render } from '@testing-library/react'
+import Spinner from './Spinner'
+
+describe('Spinner', () => {
+ it('renders with default small size', () => {
+ const { container } = render( )
+ const spinner = container.querySelector('.spinner')
+ expect(spinner?.classList.contains('spinner-small')).toBe(true)
+ })
+
+ it('renders with medium size', () => {
+ const { container } = render( )
+ const spinner = container.querySelector('.spinner')
+ expect(spinner?.classList.contains('spinner-medium')).toBe(true)
+ })
+
+ it('renders with large size', () => {
+ const { container } = render( )
+ const spinner = container.querySelector('.spinner')
+ expect(spinner?.classList.contains('spinner-large')).toBe(true)
+ })
+
+ it('renders label text', () => {
+ const { getByText } = render( )
+ expect(getByText('Loading...')).toBeTruthy()
+ })
+
+ it('renders without label', () => {
+ const { container } = render( )
+ const label = container.querySelector('.spinner-label')
+ expect(label?.textContent).toBe('')
+ })
+
+ it('wraps in centered container when centered=true', () => {
+ const { container } = render( )
+ const wrapper = container.querySelector('.spinner-centered-wrapper')
+ expect(wrapper).toBeTruthy()
+ })
+
+ it('does not wrap in centered container by default', () => {
+ const { container } = render( )
+ const wrapper = container.querySelector('.spinner-centered-wrapper')
+ expect(wrapper).toBeNull()
+ })
+
+ it('applies custom className to centered wrapper', () => {
+ const { container } = render( )
+ const wrapper = container.querySelector('.spinner-centered-wrapper')
+ expect(wrapper?.classList.contains('my-spinner')).toBe(true)
+ })
+})
diff --git a/src/components/common/Spinner/Spinner.tsx b/src/components/common/Spinner/Spinner.tsx
new file mode 100644
index 0000000..22add72
--- /dev/null
+++ b/src/components/common/Spinner/Spinner.tsx
@@ -0,0 +1,34 @@
+import React from 'react'
+
+export type SpinnerSize = 'small' | 'medium' | 'large'
+
+interface LoaderProps {
+ label?: string
+ size?: SpinnerSize
+ className?: string
+ centered?: boolean
+}
+
+const Spinner: React.FC = ({
+ label,
+ size = 'small',
+ className = '',
+ centered = false,
+}) => {
+ const spinner = (
+
+ )
+
+ if (centered) {
+ return (
+ {spinner}
+ )
+ }
+
+ return spinner
+}
+
+export default Spinner
diff --git a/src/components/common/Spinner/index.ts b/src/components/common/Spinner/index.ts
new file mode 100644
index 0000000..16b5048
--- /dev/null
+++ b/src/components/common/Spinner/index.ts
@@ -0,0 +1,2 @@
+export { default } from './Spinner'
+export type { SpinnerSize } from './Spinner'
diff --git a/src/components/common/SubmitButton/SubmitButton.test.tsx b/src/components/common/SubmitButton/SubmitButton.test.tsx
new file mode 100644
index 0000000..582b9a3
--- /dev/null
+++ b/src/components/common/SubmitButton/SubmitButton.test.tsx
@@ -0,0 +1,47 @@
+import { describe, it, expect } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import SubmitButton from './SubmitButton'
+
+describe('SubmitButton', () => {
+ it('renders "Submit" text when not submitting', () => {
+ render( )
+ expect(screen.getByRole('button')).toHaveTextContent('Submit')
+ })
+
+ it('renders "Submittingβ¦" text when submitting', () => {
+ render( )
+ expect(screen.getByRole('button')).toHaveTextContent('Submittingβ¦')
+ })
+
+ it('is disabled when submitting', () => {
+ render( )
+ expect(screen.getByRole('button')).toBeDisabled()
+ })
+
+ it('is enabled when not submitting', () => {
+ render( )
+ expect(screen.getByRole('button')).toBeEnabled()
+ })
+
+ it('has type="submit"', () => {
+ render( )
+ expect(screen.getByRole('button')).toHaveAttribute('type', 'submit')
+ })
+
+ it('applies submitting styles when isSubmitting is true', () => {
+ render( )
+ const btn = screen.getByRole('button')
+ expect(btn).toHaveClass('opacity-60')
+ expect(btn).toHaveClass('cursor-not-allowed')
+ })
+
+ it('applies light-mode background', () => {
+ render( )
+ expect(screen.getByRole('button')).toHaveClass('bg-primary-50')
+ })
+
+ it('applies dark-mode background', () => {
+ render( )
+ expect(screen.getByRole('button')).toHaveClass('bg-primary-60')
+ })
+})
diff --git a/src/components/common/SubmitButton/SubmitButton.tsx b/src/components/common/SubmitButton/SubmitButton.tsx
new file mode 100644
index 0000000..85b229b
--- /dev/null
+++ b/src/components/common/SubmitButton/SubmitButton.tsx
@@ -0,0 +1,29 @@
+interface SubmitButtonProps {
+ isDarkMode: boolean
+ isSubmitting: boolean
+ /** Optional: disable the button for other reasons (e.g. uploads in progress) */
+ isDisabled?: boolean
+}
+
+const SubmitButton = ({
+ isDarkMode,
+ isSubmitting,
+ isDisabled,
+}: SubmitButtonProps) => {
+ const baseClasses = 'submit-button'
+ const themeClasses = isDarkMode ? 'bg-primary-60' : 'bg-primary-50'
+ const disabled = isSubmitting || isDisabled
+ const stateClasses = disabled ? 'opacity-60 cursor-not-allowed' : ''
+
+ return (
+
+ {isSubmitting ? 'Submittingβ¦' : 'Submit'}
+
+ )
+}
+
+export default SubmitButton
diff --git a/src/components/common/SubmitButton/index.ts b/src/components/common/SubmitButton/index.ts
new file mode 100644
index 0000000..70b801a
--- /dev/null
+++ b/src/components/common/SubmitButton/index.ts
@@ -0,0 +1 @@
+export { default } from './SubmitButton'
diff --git a/src/components/common/TextAreaField/TextAreaField.test.tsx b/src/components/common/TextAreaField/TextAreaField.test.tsx
new file mode 100644
index 0000000..0ed259b
--- /dev/null
+++ b/src/components/common/TextAreaField/TextAreaField.test.tsx
@@ -0,0 +1,84 @@
+import { describe, it, expect, vi } from 'vitest'
+import { render, screen, fireEvent } from '@testing-library/react'
+import TextAreaField from './TextAreaField'
+
+const defaultProps = {
+ isDarkMode: false,
+ id: 'test-textarea',
+ label: 'Description',
+ value: '',
+ onChange: vi.fn(),
+}
+
+describe('TextAreaField', () => {
+ it('renders label and textarea', () => {
+ render( )
+ expect(screen.getByLabelText('Description')).toBeInTheDocument()
+ })
+
+ it('associates label with textarea via htmlFor', () => {
+ render( )
+ const textarea = screen.getByLabelText('Description')
+ expect(textarea).toHaveAttribute('id', 'test-textarea')
+ })
+
+ it('renders with the correct value', () => {
+ render( )
+ expect(screen.getByLabelText('Description')).toHaveValue('some text')
+ })
+
+ it('calls onChange when the user types', () => {
+ const onChange = vi.fn()
+ render( )
+ fireEvent.change(screen.getByLabelText('Description'), {
+ target: { value: 'new' },
+ })
+ expect(onChange).toHaveBeenCalledWith('new')
+ })
+
+ it('renders placeholder text', () => {
+ render( )
+ expect(screen.getByPlaceholderText('Enter details')).toBeInTheDocument()
+ })
+
+ it('defaults rows to 4', () => {
+ render( )
+ expect(screen.getByLabelText('Description')).toHaveAttribute('rows', '4')
+ })
+
+ it('accepts a custom rows value', () => {
+ render( )
+ expect(screen.getByLabelText('Description')).toHaveAttribute('rows', '8')
+ })
+
+ it('supports the required attribute', () => {
+ render( )
+ expect(screen.getByLabelText('Description')).toBeRequired()
+ })
+
+ it('applies light-mode styles', () => {
+ render( )
+ const textarea = screen.getByLabelText('Description')
+ expect(textarea).toHaveClass('bg-white/70')
+ expect(textarea).toHaveClass('border-black/10')
+ })
+
+ it('applies dark-mode styles', () => {
+ render( )
+ const textarea = screen.getByLabelText('Description')
+ expect(textarea).toHaveClass('bg-transparent')
+ expect(textarea).toHaveClass('border-white/10')
+ })
+
+ it('applies dark-mode label styles', () => {
+ render( )
+ const label = screen.getByText('Description')
+ expect(label).toHaveClass('text-neutral-50')
+ })
+
+ it('applies light-mode label styles', () => {
+ render( )
+ const label = screen.getByText('Description')
+ expect(label).toHaveClass('text-neutral-20')
+ })
+})
diff --git a/src/components/common/TextAreaField/TextAreaField.tsx b/src/components/common/TextAreaField/TextAreaField.tsx
new file mode 100644
index 0000000..c04857b
--- /dev/null
+++ b/src/components/common/TextAreaField/TextAreaField.tsx
@@ -0,0 +1,60 @@
+import React from 'react'
+import { FieldError } from '@/components/common/FieldError'
+
+interface TextAreaFieldProps {
+ isDarkMode: boolean
+ id: string
+ label: string
+ value: string
+ onChange: React.Dispatch>
+ placeholder?: string
+ required?: boolean
+ rows?: number
+ error?: string
+ onBlur?: () => void
+}
+
+const TextAreaField = ({
+ isDarkMode,
+ id,
+ label,
+ value,
+ onChange,
+ placeholder,
+ required,
+ rows = 4,
+ error,
+ onBlur,
+}: TextAreaFieldProps) => {
+ return (
+
+
+ {label}
+
+
+ )
+}
+
+export default TextAreaField
diff --git a/src/components/common/TextAreaField/index.ts b/src/components/common/TextAreaField/index.ts
new file mode 100644
index 0000000..232c942
--- /dev/null
+++ b/src/components/common/TextAreaField/index.ts
@@ -0,0 +1 @@
+export { default } from './TextAreaField'
diff --git a/src/components/common/TextField/TextField.test.tsx b/src/components/common/TextField/TextField.test.tsx
new file mode 100644
index 0000000..38a4d70
--- /dev/null
+++ b/src/components/common/TextField/TextField.test.tsx
@@ -0,0 +1,84 @@
+import { describe, it, expect, vi } from 'vitest'
+import { render, screen, fireEvent } from '@testing-library/react'
+import TextField from './TextField'
+
+const defaultProps = {
+ isDarkMode: false,
+ id: 'test-input',
+ label: 'Email',
+ value: '',
+ onChange: vi.fn(),
+}
+
+describe('TextField', () => {
+ it('renders label and input', () => {
+ render( )
+ expect(screen.getByLabelText('Email')).toBeInTheDocument()
+ })
+
+ it('associates label with input via htmlFor', () => {
+ render( )
+ const input = screen.getByLabelText('Email')
+ expect(input).toHaveAttribute('id', 'test-input')
+ })
+
+ it('renders with the correct value', () => {
+ render( )
+ expect(screen.getByLabelText('Email')).toHaveValue('hello@test.com')
+ })
+
+ it('calls onChange when the user types', () => {
+ const onChange = vi.fn()
+ render( )
+ fireEvent.change(screen.getByLabelText('Email'), {
+ target: { value: 'a' },
+ })
+ expect(onChange).toHaveBeenCalledWith('a')
+ })
+
+ it('renders placeholder text', () => {
+ render( )
+ expect(screen.getByPlaceholderText('you@example.com')).toBeInTheDocument()
+ })
+
+ it('defaults input type to text', () => {
+ render( )
+ expect(screen.getByLabelText('Email')).toHaveAttribute('type', 'text')
+ })
+
+ it('accepts a custom input type', () => {
+ render( )
+ expect(screen.getByLabelText('Email')).toHaveAttribute('type', 'email')
+ })
+
+ it('supports the required attribute', () => {
+ render( )
+ expect(screen.getByLabelText('Email')).toBeRequired()
+ })
+
+ it('applies light-mode styles', () => {
+ render( )
+ const input = screen.getByLabelText('Email')
+ expect(input).toHaveClass('bg-white/70')
+ expect(input).toHaveClass('border-black/10')
+ })
+
+ it('applies dark-mode styles', () => {
+ render( )
+ const input = screen.getByLabelText('Email')
+ expect(input).toHaveClass('bg-transparent')
+ expect(input).toHaveClass('border-white/10')
+ })
+
+ it('applies dark-mode label styles', () => {
+ render( )
+ const label = screen.getByText('Email')
+ expect(label).toHaveClass('text-neutral-50')
+ })
+
+ it('applies light-mode label styles', () => {
+ render( )
+ const label = screen.getByText('Email')
+ expect(label).toHaveClass('text-neutral-20')
+ })
+})
diff --git a/src/components/common/TextField/TextField.tsx b/src/components/common/TextField/TextField.tsx
new file mode 100644
index 0000000..af33add
--- /dev/null
+++ b/src/components/common/TextField/TextField.tsx
@@ -0,0 +1,60 @@
+import React from 'react'
+import { FieldError } from '@/components/common/FieldError'
+
+interface TextFieldProps {
+ isDarkMode: boolean
+ id: string
+ label: string
+ value: string
+ onChange: React.Dispatch>
+ placeholder?: string
+ type?: string
+ required?: boolean
+ error?: string
+ onBlur?: () => void
+}
+
+const TextField = ({
+ isDarkMode,
+ id,
+ label,
+ value,
+ onChange,
+ placeholder,
+ type = 'text',
+ required,
+ error,
+ onBlur,
+}: TextFieldProps) => {
+ return (
+
+
+ {label}
+
+ onChange(e.target.value)}
+ onBlur={onBlur}
+ placeholder={placeholder}
+ type={type}
+ required={required}
+ aria-invalid={!!error}
+ aria-describedby={error ? `${id}-error` : undefined}
+ className={`w-full h-10 px-3 rounded-lg border text-sm font-medium font-gilroy outline-none transition-colors ${
+ isDarkMode
+ ? `bg-transparent border-white/10 text-neutral-60 placeholder:text-neutral-30 focus:border-primary-60 ${error ? 'border-red-500 focus:border-red-500' : ''}`
+ : `bg-white/70 border-black/10 text-neutral-10 placeholder:text-neutral-30 focus:border-primary-60 ${error ? 'border-red-500 focus:border-red-500' : ''}`
+ }`}
+ />
+ {error && }
+
+ )
+}
+
+export default TextField
diff --git a/src/components/common/TextField/index.ts b/src/components/common/TextField/index.ts
new file mode 100644
index 0000000..d525c13
--- /dev/null
+++ b/src/components/common/TextField/index.ts
@@ -0,0 +1 @@
+export { default } from './TextField'
diff --git a/src/components/home/BuiltForDev/BuiltForDev.test.tsx b/src/components/home/BuiltForDev/BuiltForDev.test.tsx
new file mode 100644
index 0000000..e221f24
--- /dev/null
+++ b/src/components/home/BuiltForDev/BuiltForDev.test.tsx
@@ -0,0 +1,52 @@
+import { describe, it, expect } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import BuiltForDev from './BuiltForDev'
+
+const FEATURES = [
+ 'Quick Integration: Simple SDK with TypeScript support and comprehensive examples',
+ 'Full Documentation: Step by step guide with real-world examples',
+ 'Open Source: Transparent roadmap and community contributions',
+ 'Backwards-compatible: Verify existing .oa documents while you migrate to W3C VC',
+]
+
+describe('BuiltForDev', () => {
+ it('renders headings and description', () => {
+ render( )
+
+ expect(screen.getByText(/Built for Developers,/i)).toBeInTheDocument()
+ expect(screen.getByText(/Trusted by Enterprises/i)).toBeInTheDocument()
+ expect(
+ screen.getByText(
+ /Get started in minutes with our comprehensive documentation/i
+ )
+ ).toBeInTheDocument()
+ })
+
+ it('renders all feature statements', () => {
+ render( )
+ FEATURES.forEach(feature => {
+ expect(screen.getByText(feature)).toBeInTheDocument()
+ })
+ })
+
+ it('renders CTA buttons with correct destinations', () => {
+ render( )
+
+ const docsLink = screen
+ .getByRole('link', { name: /TrustVC Documentation/i })
+ .getAttribute('href')
+ expect(docsLink).toBe('https://docs.tradetrust.io')
+
+ const githubLink = screen
+ .getByRole('link', { name: /View on GitHub/i })
+ .getAttribute('href')
+ expect(githubLink).toBe('https://github.com/TrustVC/trustvc')
+ })
+
+ it('applies dark mode styling to heading when enabled', () => {
+ render( )
+
+ const heading = screen.getByText(/Built for Developers,/i)
+ expect(heading).toHaveClass('text-neutral-60')
+ })
+})
diff --git a/src/components/home/BuiltForDev/BuiltForDev.tsx b/src/components/home/BuiltForDev/BuiltForDev.tsx
new file mode 100644
index 0000000..e20964e
--- /dev/null
+++ b/src/components/home/BuiltForDev/BuiltForDev.tsx
@@ -0,0 +1,81 @@
+import CodeIcon from '../../icons/CodeIcon'
+import LinkButton from '../../common/LinkButton'
+import clsx from 'clsx'
+
+interface PointFormStatementProps {
+ stmt: string
+}
+
+interface BuiltForDevProps {
+ isDarkMode: boolean
+}
+
+const BUILT_FOR_DEV_FEATURES = [
+ 'Quick Integration: Simple SDK with TypeScript support and comprehensive examples',
+ 'Full Documentation: Step by step guide with real-world examples',
+ 'Open Source: Transparent roadmap and community contributions',
+ 'Backwards-compatible: Verify existing .oa documents while you migrate to W3C VC',
+] as const
+
+const PointFormStatement = ({ stmt }: PointFormStatementProps) => (
+
+)
+
+const BuiltForDev = ({ isDarkMode }: BuiltForDevProps) => (
+
+
+
+
+
+ Built for Developers,
+
+ Trusted by Enterprises
+
+
+
+
+
+ Get started in minutes with our comprehensive documentation. TrustVC
+ abstracts away the complexity while maintaining full control and
+ transparency.
+
+
+
+ {BUILT_FOR_DEV_FEATURES.map(feature => (
+
+
+
+ ))}
+
+
+
+
+ TrustVC Documentation
+
+
+ View on GitHub
+
+
+
+
+)
+
+export default BuiltForDev
diff --git a/src/components/home/BuiltForDev/index.ts b/src/components/home/BuiltForDev/index.ts
new file mode 100644
index 0000000..86ee228
--- /dev/null
+++ b/src/components/home/BuiltForDev/index.ts
@@ -0,0 +1 @@
+export { default } from './BuiltForDev'
diff --git a/src/components/home/Carousel/Carousel.test.tsx b/src/components/home/Carousel/Carousel.test.tsx
new file mode 100644
index 0000000..45645d8
--- /dev/null
+++ b/src/components/home/Carousel/Carousel.test.tsx
@@ -0,0 +1,71 @@
+import { describe, it, expect, vi } from 'vitest'
+import { render, screen, within } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+
+import Carousel from './Carousel'
+import carouselData from '../../../data/carousel.json'
+
+vi.mock('swiper/react', async () => {
+ const { MockSwiper, MockSwiperSlide } =
+ await import('../../../__tests__/__mocks__/swiper')
+
+ return {
+ Swiper: MockSwiper,
+ SwiperSlide: MockSwiperSlide,
+ }
+})
+
+vi.mock('swiper/modules', () => ({
+ Navigation: {},
+ Pagination: {},
+ Autoplay: {},
+}))
+
+describe('Carousel', () => {
+ it('renders the first slide content from carousel data', () => {
+ render( )
+
+ const firstItem = carouselData.items[0]
+ const firstSlide = screen.getByLabelText('carousel-slide-0')
+
+ expect(
+ within(firstSlide).getByText(firstItem.content.title)
+ ).toBeInTheDocument()
+ expect(
+ within(firstSlide).getByText(firstItem.content.subtitle)
+ ).toBeInTheDocument()
+ })
+
+ it('moves to the next slide when clicking the next button', async () => {
+ const user = userEvent.setup()
+ render( )
+
+ await user.click(screen.getByLabelText('carousel-next-button'))
+
+ const secondItem = carouselData.items[1]
+ const secondSlide = screen.getByLabelText('carousel-slide-1')
+
+ expect(
+ within(secondSlide).getByText(secondItem.content.subtitle)
+ ).toBeInTheDocument()
+ })
+
+ it('disables the CTA when a slide does not include a link', async () => {
+ const user = userEvent.setup()
+ render( )
+
+ // Move to the slide without a link (index 2)
+ await user.click(screen.getByLabelText('carousel-next-button'))
+ await user.click(screen.getByLabelText('carousel-next-button'))
+
+ const thirdSlide = screen.getByLabelText('carousel-slide-2')
+ const comingSoonSpans = within(thirdSlide).getAllByText(/Coming Soon/i)
+ const ctaLink = comingSoonSpans
+ .map(span => span.closest('a'))
+ .find(anchor => anchor)
+
+ expect(ctaLink).toBeTruthy()
+ expect(ctaLink).not.toHaveAttribute('href')
+ expect(ctaLink).toHaveAttribute('aria-disabled', 'true')
+ })
+})
diff --git a/src/components/home/Carousel/Carousel.tsx b/src/components/home/Carousel/Carousel.tsx
new file mode 100644
index 0000000..9804b76
--- /dev/null
+++ b/src/components/home/Carousel/Carousel.tsx
@@ -0,0 +1,311 @@
+import { MutableRefObject, useRef } from 'react'
+
+import { Swiper, SwiperSlide } from 'swiper/react'
+import 'swiper/css'
+import 'swiper/css/pagination'
+import 'swiper/css/navigation'
+import { Navigation, Pagination, Autoplay } from 'swiper/modules'
+import type { Swiper as SwiperInstance } from 'swiper'
+import LinkButton from '../../common/LinkButton'
+import RightArrowIcon from '../../icons/RightArrowIcon'
+import carouselData from '../../../data/carousel.json'
+import clsx from 'clsx'
+
+interface Stat {
+ value: string
+ label: string
+}
+
+interface ContentSectionProps {
+ title: string
+ subtitle: string
+ description: string
+ link?: string
+}
+
+interface StatsGridProps {
+ topLeft: Stat
+ topRight: Stat
+ bottomLeft: Stat
+ bottomRight: Stat
+}
+
+interface CarouselSlideProps {
+ isDarkMode: boolean
+ content: ContentSectionProps
+ image: string
+ stats?: StatsGridProps
+}
+
+interface CarouselControlBarProps {
+ isDarkMode: boolean
+ swiperRef: MutableRefObject
+}
+
+interface CarouselProps {
+ isDarkMode: boolean
+}
+
+const StatDisplay = ({ value, label }: Stat) => (
+
+
+ {value}
+
+
+ {label}
+
+
+)
+
+const ContentSection = ({
+ isDarkMode,
+ title,
+ subtitle,
+ description,
+ link,
+}: ContentSectionProps & { isDarkMode: boolean }) => {
+ const isComingSoon = !link
+
+ return (
+
+
+ {title}
+ {subtitle}
+
+
+ {description}
+
+
+
+ {isComingSoon ? 'Coming Soon' : 'Learn More'}
+ {!isComingSoon && (
+
+
+
+ )}
+
+
+
+ )
+}
+
+const ComingSoonPlaceholder = ({ isDarkMode }: { isDarkMode: boolean }) => (
+
+
+ Logo Here
+
+
+
+ Coming Soon
+
+
+ Stay Tuned
+
+
+
+)
+
+const StatsGridDisplay = ({
+ isDarkMode,
+ stats,
+}: {
+ isDarkMode: boolean
+ stats: StatsGridProps
+}) => (
+
+
+ Logo Here
+
+
+
+
+
+
+
+
+ 2025 Growth to date
+
+
+)
+
+const StatsSection = ({
+ isDarkMode,
+ stats,
+}: {
+ isDarkMode: boolean
+ stats?: StatsGridProps
+}) => (
+
+ {stats ? (
+
+ ) : (
+
+ )}
+
+)
+
+const CarouselSlide = ({
+ isDarkMode,
+ content,
+ image,
+ stats,
+}: CarouselSlideProps) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+const CarouselControlBar = ({ swiperRef }: CarouselControlBarProps) => {
+ return (
+
+
swiperRef.current?.slidePrev()}
+ className="flex items-center justify-center h-6 w-6 hover:text-primary-60"
+ >
+
+
+
+
+
+
swiperRef.current?.slideNext()}
+ className="flex items-center justify-center h-6 w-6 hover:text-primary-60"
+ >
+
+
+
+ )
+}
+
+const Carousel = ({ isDarkMode }: CarouselProps) => {
+ const swiperRef = useRef(null)
+
+ return (
+
+
+ {
+ swiperRef.current = swiper
+ }}
+ pagination={{
+ clickable: true,
+ el: '.hero-carousel-pagination',
+ }}
+ >
+ {carouselData.items.map((item, index) => (
+
+
+
+ ))}
+
+
+
+
+ )
+}
+
+export default Carousel
diff --git a/src/components/home/Carousel/index.ts b/src/components/home/Carousel/index.ts
new file mode 100644
index 0000000..0cca671
--- /dev/null
+++ b/src/components/home/Carousel/index.ts
@@ -0,0 +1 @@
+export { default } from './Carousel'
diff --git a/src/components/home/EndorsementChain/EndorsementChain.test.tsx b/src/components/home/EndorsementChain/EndorsementChain.test.tsx
new file mode 100644
index 0000000..5a400f0
--- /dev/null
+++ b/src/components/home/EndorsementChain/EndorsementChain.test.tsx
@@ -0,0 +1,496 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { render, screen, fireEvent } from '@testing-library/react'
+import EndorsementChainLayout from './EndorsementChain'
+import { EndorsementChainStatus } from './useEndorsementChain'
+
+// βββ Mock Data ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+const mockEndorsementChain = [
+ {
+ type: 'INITIAL',
+ owner: '0x1234567890123456789012345678901234567890',
+ holder: '0x1234567890123456789012345678901234567890',
+ timestamp: 1640000000000,
+ transactionHash: '0xabc123',
+ remark: 'Initial issuance',
+ },
+ {
+ type: 'TRANSFER_OWNERS',
+ owner: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
+ holder: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
+ timestamp: 1640100000000,
+ transactionHash: '0xdef456',
+ remark: 'Transfer to new owner',
+ },
+ {
+ type: 'TRANSFER_BENEFICIARY',
+ owner: '0x9876543210987654321098765432109876543210',
+ holder: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
+ timestamp: 1640200000000,
+ transactionHash: '0xghi789',
+ remark: 'Endorsement',
+ },
+]
+
+const mockEndorsementChainStatus: EndorsementChainStatus = {
+ status: 'success',
+}
+
+const defaultProps = {
+ endorsementChain: mockEndorsementChain,
+ onReset: vi.fn(),
+ isDarkMode: false,
+ endorsementChainStatus: mockEndorsementChainStatus,
+}
+
+// βββ Tests ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+describe('EndorsementChain', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ // ββ Rendering ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('rendering', () => {
+ it('renders the header title', () => {
+ render( )
+ expect(screen.getByText('Endorsement Chain')).toBeInTheDocument()
+ })
+
+ it('renders the Dismiss button', () => {
+ render( )
+ expect(
+ screen.getByRole('button', { name: /dismiss/i })
+ ).toBeInTheDocument()
+ })
+
+ it('renders the check icon in the header', () => {
+ const { container } = render( )
+ const svg = container.querySelector('svg')
+ expect(svg).toBeInTheDocument()
+ })
+
+ it('applies dark-mode class when isDarkMode is true', () => {
+ const { container } = render(
+
+ )
+ const endorsementChainDiv = container.querySelector('.endorsement-chain')
+ expect(endorsementChainDiv).toHaveClass('dark-mode')
+ })
+
+ it('does not apply dark-mode class when isDarkMode is false', () => {
+ const { container } = render(
+
+ )
+ const endorsementChainDiv = container.querySelector('.endorsement-chain')
+ expect(endorsementChainDiv).not.toHaveClass('dark-mode')
+ })
+ })
+
+ // ββ Loading State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('loading state', () => {
+ it('shows spinner when status is loading', () => {
+ const loadingStatus: EndorsementChainStatus = { status: 'loading' }
+ const { container } = render(
+
+ )
+ const spinner = container.querySelector('.spinner')
+ expect(spinner).toBeInTheDocument()
+ })
+
+ it('shows loading message when loading', () => {
+ const loadingStatus: EndorsementChainStatus = { status: 'loading' }
+ render(
+
+ )
+ expect(
+ screen.getByText('Loading Endorsement Chain...')
+ ).toBeInTheDocument()
+ })
+
+ it('does not show endorsement chain entries when loading', () => {
+ const loadingStatus: EndorsementChainStatus = { status: 'loading' }
+ render(
+
+ )
+ expect(
+ screen.queryByText('Document has been issued')
+ ).not.toBeInTheDocument()
+ })
+
+ it('centers the spinner when loading', () => {
+ const loadingStatus: EndorsementChainStatus = { status: 'loading' }
+ const { container } = render(
+
+ )
+ const centeredWrapper = container.querySelector(
+ '.spinner-centered-wrapper'
+ )
+ expect(centeredWrapper).toBeInTheDocument()
+ })
+ })
+
+ // ββ Error State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('error state', () => {
+ it('shows error message when status is error', () => {
+ const errorStatus: EndorsementChainStatus = {
+ status: 'error',
+ errorMessage: 'Failed to fetch data',
+ }
+ render(
+
+ )
+ expect(
+ screen.getByText('Failed to load endorsement chain')
+ ).toBeInTheDocument()
+ })
+
+ it('shows specific error message when provided', () => {
+ const errorStatus: EndorsementChainStatus = {
+ status: 'error',
+ errorMessage: 'Network timeout',
+ }
+ render(
+
+ )
+ expect(screen.getByText('Network timeout')).toBeInTheDocument()
+ })
+
+ it('does not show endorsement chain entries when error', () => {
+ const errorStatus: EndorsementChainStatus = {
+ status: 'error',
+ errorMessage: 'Error occurred',
+ }
+ render(
+
+ )
+ expect(
+ screen.queryByText('Document has been issued')
+ ).not.toBeInTheDocument()
+ })
+ })
+
+ // ββ Success State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('success state with endorsement chain data', () => {
+ it('renders all endorsement chain entries', () => {
+ render( )
+ expect(screen.getByText('Document has been issued')).toBeInTheDocument()
+ expect(
+ screen.getByText('Transfer ownership and holdership')
+ ).toBeInTheDocument()
+ expect(screen.getByText('Transfer ownership')).toBeInTheDocument()
+ })
+
+ it('renders formatted timestamps', () => {
+ render( )
+ // Check if date-frame elements exist (format: 'do MMM yyyy, hh:mm aa')
+ const dateFrames = document.querySelectorAll('.date-frame')
+ expect(dateFrames.length).toBe(3)
+ expect(dateFrames[0].textContent).toMatch(/\d{1,2}(st|nd|rd|th)/)
+ })
+
+ it('renders owner addresses for entries with new beneficiary', () => {
+ render( )
+ const walletAddresses = document.querySelectorAll('.wallet-address')
+ expect(walletAddresses.length).toBeGreaterThan(0)
+ })
+
+ it('renders holder addresses for entries with new holder', () => {
+ render( )
+ const columns = document.querySelectorAll('.column')
+ expect(columns.length).toBeGreaterThan(0)
+ })
+
+ it('renders remarks when provided', () => {
+ const { container } = render( )
+ const remarks = container.querySelectorAll('.remarks')
+ const remarksTexts = Array.from(remarks).map(el => el.textContent)
+ expect(
+ remarksTexts.some(text => text?.includes('Initial issuance'))
+ ).toBe(true)
+ expect(
+ remarksTexts.some(text => text?.includes('Transfer to new owner'))
+ ).toBe(true)
+ expect(remarksTexts.some(text => text?.includes('Endorsement'))).toBe(
+ true
+ )
+ })
+
+ it('renders Owner, Holder, and Remarks labels', () => {
+ const { container } = render( )
+ const ownerLabels = container.querySelectorAll('.subheader')
+ const ownerTexts = Array.from(ownerLabels).filter(
+ el => el.textContent === 'Owner'
+ )
+ const holderTexts = Array.from(ownerLabels).filter(
+ el => el.textContent === 'Holder'
+ )
+ const remarksTexts = Array.from(ownerLabels).filter(
+ el => el.textContent === 'Remarks'
+ )
+ expect(ownerTexts.length).toBeGreaterThan(0)
+ expect(holderTexts.length).toBeGreaterThan(0)
+ expect(remarksTexts.length).toBeGreaterThan(0)
+ })
+
+ it('renders line design for each entry', () => {
+ const { container } = render( )
+ const lineDesigns = container.querySelectorAll('.line-design-container')
+ expect(lineDesigns.length).toBe(3)
+ })
+
+ it('renders first entry with special line design', () => {
+ const { container } = render( )
+ const firstLinePath = container.querySelector('.line-design-path.first')
+ expect(firstLinePath).toBeInTheDocument()
+ })
+ })
+
+ // ββ Empty State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('empty endorsement chain', () => {
+ it('renders without errors when endorsementChain is undefined', () => {
+ render(
+
+ )
+ expect(screen.getByText('Endorsement Chain')).toBeInTheDocument()
+ })
+
+ it('renders without errors when endorsementChain is empty array', () => {
+ render( )
+ expect(screen.getByText('Endorsement Chain')).toBeInTheDocument()
+ })
+
+ it('does not render any entries when endorsementChain is empty', () => {
+ const { container } = render(
+
+ )
+ const entities = container.querySelectorAll('.entity')
+ expect(entities.length).toBe(0)
+ })
+ })
+
+ // ββ Different Event Types ββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('different endorsement event types', () => {
+ it('renders TRANSFER_HOLDER event correctly', () => {
+ const chainWithTransferHolder = [
+ {
+ type: 'TRANSFER_HOLDER',
+ owner: '0x1234567890123456789012345678901234567890',
+ holder: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
+ timestamp: 1640000000000,
+ transactionHash: '0xabc123',
+ remark: 'Holder transfer',
+ },
+ ]
+ render(
+
+ )
+ expect(screen.getByText('Transfer holdership')).toBeInTheDocument()
+ })
+
+ it('renders RETURNED_TO_ISSUER event correctly', () => {
+ const chainWithReturn = [
+ {
+ type: 'RETURNED_TO_ISSUER',
+ owner: '0x1234567890123456789012345678901234567890',
+ holder: '0x1234567890123456789012345678901234567890',
+ timestamp: 1640000000000,
+ transactionHash: '0xabc123',
+ remark: 'Returned',
+ },
+ ]
+ render(
+
+ )
+ expect(screen.getByText('ETR returned to issuer')).toBeInTheDocument()
+ })
+
+ it('renders RETURN_TO_ISSUER_ACCEPTED event correctly', () => {
+ const chainWithAccepted = [
+ {
+ type: 'RETURN_TO_ISSUER_ACCEPTED',
+ owner: '0x1234567890123456789012345678901234567890',
+ holder: '0x1234567890123456789012345678901234567890',
+ timestamp: 1640000000000,
+ transactionHash: '0xabc123',
+ remark: 'Accepted',
+ },
+ ]
+ render(
+
+ )
+ expect(
+ screen.getByText('ETR taken out of circulation')
+ ).toBeInTheDocument()
+ })
+
+ it('renders REJECT_TRANSFER_HOLDER event correctly', () => {
+ const chainWithReject = [
+ {
+ type: 'REJECT_TRANSFER_HOLDER',
+ owner: '0x1234567890123456789012345678901234567890',
+ holder: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
+ timestamp: 1640000000000,
+ transactionHash: '0xabc123',
+ remark: 'Rejected',
+ },
+ ]
+ render(
+
+ )
+ expect(screen.getByText('Rejection of holdership')).toBeInTheDocument()
+ })
+
+ it('renders REJECT_TRANSFER_BENEFICIARY event correctly', () => {
+ const chainWithReject = [
+ {
+ type: 'REJECT_TRANSFER_BENEFICIARY',
+ owner: '0x1234567890123456789012345678901234567890',
+ holder: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
+ timestamp: 1640000000000,
+ transactionHash: '0xabc123',
+ remark: 'Rejected',
+ },
+ ]
+ render(
+
+ )
+ expect(screen.getByText('Rejection of ownership')).toBeInTheDocument()
+ })
+ })
+
+ // ββ Callbacks ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('callbacks', () => {
+ it('calls onReset when Dismiss button is clicked', () => {
+ const onReset = vi.fn()
+ render( )
+ fireEvent.click(screen.getByRole('button', { name: /dismiss/i }))
+ expect(onReset).toHaveBeenCalledTimes(1)
+ })
+
+ it('does not call onReset on initial render', () => {
+ const onReset = vi.fn()
+ render( )
+ expect(onReset).not.toHaveBeenCalled()
+ })
+ })
+
+ // ββ Status Prop Handling βββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('endorsementChainStatus prop handling', () => {
+ it('handles undefined endorsementChainStatus gracefully', () => {
+ render(
+
+ )
+ expect(screen.getByText('Endorsement Chain')).toBeInTheDocument()
+ })
+
+ it('handles idle status', () => {
+ const idleStatus: EndorsementChainStatus = { status: 'idle' }
+ render(
+
+ )
+ expect(screen.getByText('Endorsement Chain')).toBeInTheDocument()
+ })
+
+ it('shows content when status is success', () => {
+ const successStatus: EndorsementChainStatus = { status: 'success' }
+ render(
+
+ )
+ expect(screen.getByText('Document has been issued')).toBeInTheDocument()
+ })
+ })
+
+ // ββ Visual Elements ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('visual elements', () => {
+ it('renders dividers between entries', () => {
+ const { container } = render( )
+ const dividers = container.querySelectorAll('.divider')
+ // Dividers are rendered between entries, so n-1 dividers for n entries
+ expect(dividers.length).toBe(2)
+ })
+
+ it('renders dots in line design', () => {
+ const { container } = render( )
+ const dots = container.querySelectorAll('.dot')
+ expect(dots.length).toBe(3)
+ })
+
+ it('renders content sections for each entry', () => {
+ const { container } = render( )
+ const contentSections = container.querySelectorAll('.content')
+ expect(contentSections.length).toBe(3)
+ })
+
+ it('renders three columns per entry', () => {
+ const { container } = render( )
+ const columns = container.querySelectorAll('.column')
+ // 3 entries Γ 3 columns each = 9 columns
+ expect(columns.length).toBe(9)
+ })
+ })
+})
diff --git a/src/components/home/EndorsementChain/EndorsementChain.tsx b/src/components/home/EndorsementChain/EndorsementChain.tsx
new file mode 100644
index 0000000..b29267b
--- /dev/null
+++ b/src/components/home/EndorsementChain/EndorsementChain.tsx
@@ -0,0 +1,338 @@
+import React from 'react'
+import Overlay from '../../common/Overlay'
+import PrimaryButton from '../../common/PrimaryButton'
+import { EndorsementChain } from '@trustvc/trustvc'
+import { format } from 'date-fns'
+import { EndorsementChainStatus } from './useEndorsementChain'
+import Spinner from '../../common/Spinner'
+import { TokenRegistryVersion } from '../VerifySection/useVerify'
+
+interface EndorsementChainProps {
+ endorsementChain?: any
+ onReset: () => void
+ isDarkMode?: boolean
+ endorsementChainStatus?: EndorsementChainStatus
+ tokenRegistryVersion?: TokenRegistryVersion
+}
+enum ActionType {
+ INITIAL = 'Document has been issued',
+ NEW_OWNERS = 'Transfer ownership and holdership',
+ ENDORSE = 'Transfer ownership',
+ TRANSFER = 'Transfer holdership',
+ REJECT_TRANSFER_HOLDER = 'Rejection of holdership',
+ REJECT_TRANSFER_BENEFICIARY = 'Rejection of ownership',
+ RETURNED_TO_ISSUER = 'ETR returned to issuer',
+ RETURN_TO_ISSUER_REJECTED = 'Return of ETR rejected',
+ RETURN_TO_ISSUER_ACCEPTED = 'ETR taken out of circulation', // burnt token
+ TRANSFER_TO_WALLET = 'Transferred to wallet',
+}
+interface HistoryChain {
+ action: ActionType
+ isNewBeneficiary: boolean
+ isNewHolder: boolean
+ beneficiary?: string
+ holder?: string
+ timestamp?: number
+ hash?: string
+ remark?: string
+}
+
+const getHistoryChain = (endorsementChain?: EndorsementChain) => {
+ const historyChain: HistoryChain[] = []
+
+ endorsementChain?.forEach(endorsementChainEvent => {
+ const beneficiary = endorsementChainEvent.owner
+ const holder = endorsementChainEvent.holder
+ const timestamp = endorsementChainEvent.timestamp
+ const hash = endorsementChainEvent.transactionHash
+ const remark = endorsementChainEvent?.remark
+ switch (endorsementChainEvent.type) {
+ case 'TRANSFER_OWNERS':
+ historyChain.push({
+ action: ActionType.NEW_OWNERS,
+ isNewBeneficiary: true,
+ isNewHolder: true,
+ beneficiary,
+ holder,
+ timestamp,
+ hash,
+ remark,
+ })
+ break
+ case 'TRANSFER_BENEFICIARY':
+ historyChain.push({
+ action: ActionType.ENDORSE,
+ isNewBeneficiary: true,
+ isNewHolder: false,
+ beneficiary,
+ holder,
+ timestamp,
+ hash,
+ remark,
+ })
+ break
+ case 'TRANSFER_HOLDER':
+ historyChain.push({
+ action: ActionType.TRANSFER,
+ isNewBeneficiary: false,
+ isNewHolder: true,
+ beneficiary,
+ holder,
+ timestamp,
+ hash,
+ remark,
+ })
+ break
+ case 'RETURNED_TO_ISSUER':
+ case 'SURRENDERED':
+ historyChain.push({
+ action: ActionType.RETURNED_TO_ISSUER,
+ isNewBeneficiary: false,
+ isNewHolder: false,
+ timestamp,
+ remark,
+ })
+ break
+ case 'RETURN_TO_ISSUER_ACCEPTED':
+ case 'SURRENDER_ACCEPTED':
+ historyChain.push({
+ action: ActionType.RETURN_TO_ISSUER_ACCEPTED,
+ isNewBeneficiary: false,
+ isNewHolder: false,
+ timestamp,
+ remark,
+ })
+ break
+ case 'RETURN_TO_ISSUER_REJECTED':
+ case 'SURRENDER_REJECTED':
+ historyChain.push({
+ action: ActionType.RETURN_TO_ISSUER_REJECTED,
+ isNewBeneficiary: true,
+ isNewHolder: true,
+ timestamp,
+ beneficiary,
+ holder: beneficiary,
+ hash,
+ remark,
+ })
+ break
+ case 'INITIAL':
+ historyChain.push({
+ action: ActionType.INITIAL,
+ isNewBeneficiary: true,
+ isNewHolder: true,
+ beneficiary,
+ holder,
+ timestamp,
+ hash,
+ remark,
+ })
+ break
+ case 'REJECT_TRANSFER_HOLDER':
+ historyChain.push({
+ action: ActionType.REJECT_TRANSFER_HOLDER,
+ isNewBeneficiary: false,
+ isNewHolder: true,
+ beneficiary,
+ holder,
+ timestamp,
+ hash,
+ remark,
+ })
+ break
+ case 'REJECT_TRANSFER_BENEFICIARY':
+ historyChain.push({
+ action: ActionType.REJECT_TRANSFER_BENEFICIARY,
+ isNewBeneficiary: true,
+ isNewHolder: false,
+ beneficiary,
+ holder,
+ timestamp,
+ hash,
+ remark,
+ })
+ break
+ case 'REJECT_TRANSFER_OWNERS':
+ historyChain.push({
+ action: ActionType.REJECT_TRANSFER_HOLDER,
+ isNewBeneficiary: false,
+ isNewHolder: true,
+ beneficiary,
+ holder,
+ timestamp,
+ hash,
+ remark,
+ })
+ historyChain.push({
+ action: ActionType.REJECT_TRANSFER_BENEFICIARY,
+ isNewBeneficiary: true,
+ isNewHolder: false,
+ beneficiary,
+ holder,
+ timestamp,
+ hash,
+ remark,
+ })
+ break
+
+ default:
+ console.warn(
+ `Unknown endorsement event type: ${endorsementChainEvent.type}`
+ )
+ break
+ }
+ })
+
+ return historyChain
+}
+
+const CheckIcon = () => (
+
+
+
+
+)
+
+const LineDesign: React.FunctionComponent<{
+ first?: boolean
+ last?: boolean
+}> = ({ first, last }) => {
+ return (
+
+ )
+}
+const EndorsementChainLayout: React.FC = ({
+ endorsementChain,
+ onReset,
+ isDarkMode,
+ endorsementChainStatus,
+ tokenRegistryVersion,
+}) => {
+ const historyChain = getHistoryChain(endorsementChain)
+ const { status, errorMessage } = endorsementChainStatus ?? {}
+
+ return (
+
+
+ {/* First Component - Header Section */}
+
+
+
+
Endorsement Chain
+
+
+
+ {/* Second Component - Main Content Section */}
+
+
+ {status === 'loading' && (
+
+ )}
+ {status === 'error' && (
+
+
Failed to load endorsement chain
+ {errorMessage && (
+
{errorMessage}
+ )}
+
+ )}
+ {status === 'success' &&
+ historyChain?.map((data: any, key: number) => (
+
+
+
+
+
+
+
{data.action}
+
+
+ {format(
+ new Date(data.timestamp ?? 0),
+ 'do MMM yyyy, hh:mm aa'
+ )}
+
+
+
+
+
Owner
+
+ {data.isNewBeneficiary ? data.beneficiary : '_'}
+
+
Organisation A
+
+
+
Holder
+
+ {data.isNewHolder ? data.holder : '_'}
+
+
Organisation B
+
+
+
+ Remarks
+ {tokenRegistryVersion === 'V4'
+ ? ' (Unavailable on TR V4)'
+ : ''}
+
+
{data?.remark ?? '-'}
+
+
+
+ {key !== historyChain.length - 1 && (
+
+ )}
+
+
+ ))}
+
+
+
+ {/* Third Component - Footer Section */}
+
+
+
+ )
+}
+
+export default EndorsementChainLayout
diff --git a/src/components/home/EndorsementChain/index.tsx b/src/components/home/EndorsementChain/index.tsx
new file mode 100644
index 0000000..a85069d
--- /dev/null
+++ b/src/components/home/EndorsementChain/index.tsx
@@ -0,0 +1 @@
+export { default } from './EndorsementChain'
diff --git a/src/components/home/EndorsementChain/useEndorsementChain.test.ts b/src/components/home/EndorsementChain/useEndorsementChain.test.ts
new file mode 100644
index 0000000..c3a74b8
--- /dev/null
+++ b/src/components/home/EndorsementChain/useEndorsementChain.test.ts
@@ -0,0 +1,165 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { renderHook, act, waitFor } from '@testing-library/react'
+import { useEndorsementChain } from './useEndorsementChain'
+
+// Mock dependencies
+vi.mock('@trustvc/trustvc', () => ({
+ fetchEndorsementChain: vi.fn(),
+}))
+
+vi.mock('ethers', () => ({
+ ethers: {
+ providers: {
+ JsonRpcProvider: vi.fn(),
+ },
+ },
+}))
+
+vi.mock('../../../utils/helper', () => ({
+ getRpcUrl: vi.fn(() => 'https://rpc.example.com'),
+}))
+
+const { fetchEndorsementChain } = await import('@trustvc/trustvc')
+const { getRpcUrl } = await import('../../../utils/helper')
+
+describe('useEndorsementChain', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('returns idle status with no params', () => {
+ const { result } = renderHook(() =>
+ useEndorsementChain({
+ tokenRegistryAddress: undefined,
+ tokenId: undefined,
+ verifiedChainId: undefined,
+ })
+ )
+ expect(result.current.endorsementChainStatus.status).toBe('idle')
+ expect(result.current.endorsementChain).toBeUndefined()
+ expect(result.current.showEndorsementChain).toBe(false)
+ })
+
+ it('fetches endorsement chain when all params provided', async () => {
+ const mockChain = { chain: [] }
+ vi.mocked(fetchEndorsementChain).mockResolvedValue(mockChain as any)
+
+ const { result } = renderHook(() =>
+ useEndorsementChain({
+ tokenRegistryAddress: '0x1234',
+ tokenId: '0xabcd',
+ verifiedChainId: '1',
+ })
+ )
+
+ // Should be loading initially
+ expect(result.current.endorsementChainStatus.status).toBe('loading')
+
+ await waitFor(() => {
+ expect(result.current.endorsementChainStatus.status).toBe('success')
+ })
+ expect(result.current.endorsementChain).toBe(mockChain)
+ })
+
+ it('handles fetch error', async () => {
+ vi.mocked(fetchEndorsementChain).mockRejectedValue(
+ new Error('Network error')
+ )
+
+ const { result } = renderHook(() =>
+ useEndorsementChain({
+ tokenRegistryAddress: '0x1234',
+ tokenId: '0xabcd',
+ verifiedChainId: '1',
+ })
+ )
+
+ await waitFor(() => {
+ expect(result.current.endorsementChainStatus.status).toBe('error')
+ })
+ expect(result.current.endorsementChainStatus.errorMessage).toBe(
+ 'Network error'
+ )
+ expect(result.current.endorsementChain).toBeUndefined()
+ })
+
+ it('handles missing RPC URL', async () => {
+ vi.mocked(getRpcUrl).mockReturnValue(null)
+
+ const { result } = renderHook(() =>
+ useEndorsementChain({
+ tokenRegistryAddress: '0x1234',
+ tokenId: '0xabcd',
+ verifiedChainId: '999',
+ })
+ )
+
+ await waitFor(() => {
+ expect(result.current.endorsementChainStatus.status).toBe('error')
+ })
+ expect(result.current.endorsementChainStatus.errorMessage).toContain(
+ 'No RPC URL configured'
+ )
+ })
+
+ it('toggles showEndorsementChain', () => {
+ const { result } = renderHook(() =>
+ useEndorsementChain({
+ tokenRegistryAddress: undefined,
+ tokenId: undefined,
+ verifiedChainId: undefined,
+ })
+ )
+
+ expect(result.current.showEndorsementChain).toBe(false)
+
+ act(() => {
+ result.current.handleShowEndorsementChain()
+ })
+ expect(result.current.showEndorsementChain).toBe(true)
+
+ act(() => {
+ result.current.handleHideEndorsementChain()
+ })
+ expect(result.current.showEndorsementChain).toBe(false)
+ })
+
+ it('resets to idle when params become undefined', async () => {
+ vi.mocked(getRpcUrl).mockReturnValue('https://rpc.example.com')
+ const mockChain = { chain: [] }
+ vi.mocked(fetchEndorsementChain).mockResolvedValue(mockChain as any)
+
+ const { result, rerender } = renderHook(
+ (props: {
+ tokenRegistryAddress?: string
+ tokenId?: string
+ verifiedChainId?: string
+ keyId?: string
+ }) => useEndorsementChain(props),
+ {
+ initialProps: {
+ tokenRegistryAddress: '0x1234' as string | undefined,
+ tokenId: '0xabcd' as string | undefined,
+ verifiedChainId: '1' as string | undefined,
+ keyId: undefined as string | undefined,
+ },
+ }
+ )
+
+ await waitFor(() => {
+ expect(result.current.endorsementChainStatus.status).toBe('success')
+ })
+
+ rerender({
+ tokenRegistryAddress: undefined,
+ tokenId: undefined,
+ verifiedChainId: undefined,
+ keyId: undefined,
+ })
+
+ await waitFor(() => {
+ expect(result.current.endorsementChainStatus.status).toBe('idle')
+ })
+ expect(result.current.endorsementChain).toBeUndefined()
+ })
+})
diff --git a/src/components/home/EndorsementChain/useEndorsementChain.ts b/src/components/home/EndorsementChain/useEndorsementChain.ts
new file mode 100644
index 0000000..22d09a2
--- /dev/null
+++ b/src/components/home/EndorsementChain/useEndorsementChain.ts
@@ -0,0 +1,102 @@
+import { useEffect, useState } from 'react'
+import { fetchEndorsementChain, EndorsementChain } from '@trustvc/trustvc'
+import { ethers } from 'ethers'
+import { getRpcUrl } from '../../../utils/helper'
+
+export interface EndorsementChainStatus {
+ status: 'idle' | 'loading' | 'success' | 'error'
+ errorMessage?: string
+}
+
+interface UseEndorsementChainParams {
+ tokenRegistryAddress?: string
+ tokenId?: string
+ verifiedChainId?: string
+ keyId?: string
+}
+
+interface UseEndorsementChainReturn {
+ endorsementChain?: EndorsementChain
+ endorsementChainStatus: EndorsementChainStatus
+ showEndorsementChain: boolean
+ handleShowEndorsementChain: () => void
+ handleHideEndorsementChain: () => void
+}
+
+export const useEndorsementChain = ({
+ tokenRegistryAddress,
+ tokenId,
+ verifiedChainId,
+ keyId,
+}: UseEndorsementChainParams): UseEndorsementChainReturn => {
+ const [endorsementChain, setEndorsementChain] = useState<
+ EndorsementChain | undefined
+ >(undefined)
+ const [endorsementChainStatus, setEndorsementChainStatus] =
+ useState({ status: 'idle' })
+ const [showEndorsementChain, setShowEndorsementChain] = useState(false)
+
+ const handleShowEndorsementChain = () => {
+ setShowEndorsementChain(true)
+ }
+
+ const handleHideEndorsementChain = () => {
+ setShowEndorsementChain(false)
+ }
+
+ useEffect(() => {
+ let cancelled = false
+
+ const fetchEndorsementData = async () => {
+ if (tokenRegistryAddress && tokenId && verifiedChainId) {
+ setEndorsementChainStatus({ status: 'loading' })
+ try {
+ const rpcUrl = getRpcUrl(verifiedChainId)
+ if (!rpcUrl) {
+ throw new Error(
+ `No RPC URL configured for chain ${verifiedChainId}`
+ )
+ }
+
+ const provider = new ethers.providers.JsonRpcProvider(rpcUrl as any)
+ const _endorsementChain = await fetchEndorsementChain(
+ tokenRegistryAddress,
+ tokenId,
+ provider,
+ keyId
+ )
+ if (cancelled) return
+ setEndorsementChain(_endorsementChain)
+ setEndorsementChainStatus({ status: 'success' })
+ } catch (error) {
+ if (cancelled) return
+ console.error('Failed to fetch endorsement chain:', error)
+ const errorMessage =
+ error instanceof Error
+ ? error.message
+ : 'Failed to load endorsement chain'
+ setEndorsementChain(undefined)
+ setEndorsementChainStatus({ status: 'error', errorMessage })
+ }
+ } else {
+ // Reset to idle if required params are missing
+ if (cancelled) return
+ setEndorsementChainStatus({ status: 'idle' })
+ setEndorsementChain(undefined)
+ }
+ }
+ fetchEndorsementData()
+
+ return () => {
+ cancelled = true
+ }
+ }, [tokenRegistryAddress, tokenId, verifiedChainId, keyId])
+
+ return {
+ endorsementChain,
+ endorsementChainStatus,
+ showEndorsementChain,
+ handleShowEndorsementChain,
+ handleHideEndorsementChain,
+ }
+}
diff --git a/src/components/home/HeroSection/HeroSection.test.tsx b/src/components/home/HeroSection/HeroSection.test.tsx
new file mode 100644
index 0000000..21b5e52
--- /dev/null
+++ b/src/components/home/HeroSection/HeroSection.test.tsx
@@ -0,0 +1,39 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import HeroSection from './HeroSection'
+
+describe('HeroSection', () => {
+ it('renders the hero title correctly', () => {
+ render( )
+ expect(screen.getByText(/Simple,/i)).toBeInTheDocument()
+ expect(screen.getByText(/Trustworthy/i)).toBeInTheDocument()
+ expect(screen.getByText(/Verifiable/i)).toBeInTheDocument()
+ expect(screen.getByText(/Credentials/i)).toBeInTheDocument()
+ })
+
+ it('renders the hero description', () => {
+ render( )
+ expect(
+ screen.getByText(/One SDK, multiple verification systems./i)
+ ).toBeInTheDocument()
+ })
+
+ it('applies dark mode class when isDarkMode is true', () => {
+ const { container } = render( )
+ expect(container.querySelector('.hero-section')).toHaveClass('dark-mode')
+ })
+
+ it('does not apply dark mode class when isDarkMode is false', () => {
+ const { container } = render( )
+ expect(container.querySelector('.hero-section')).not.toHaveClass(
+ 'dark-mode'
+ )
+ })
+
+ it('renders gradient text for Trustworthy', () => {
+ const { container } = render( )
+ const gradientText = container.querySelector('.hero-gradient-text')
+ expect(gradientText).toBeInTheDocument()
+ expect(gradientText).toHaveTextContent('Trustworthy')
+ })
+})
diff --git a/src/components/home/HeroSection/HeroSection.tsx b/src/components/home/HeroSection/HeroSection.tsx
new file mode 100644
index 0000000..29fc7d3
--- /dev/null
+++ b/src/components/home/HeroSection/HeroSection.tsx
@@ -0,0 +1,31 @@
+import React from 'react'
+
+interface HeroSectionProps {
+ isDarkMode: boolean
+}
+
+const HeroSection: React.FC = ({ isDarkMode }) => {
+ return (
+
+
+
+
+ Simple,
+ Trustworthy
+
+
+ Verifiable
+ Credentials
+
+
+
+ One SDK, multiple verification systems. Instantly verify trade
+ documents, academic certificates, and legal apostilles powered by
+ decentralized ledger technology and open standards for digital trust.
+
+
+
+ )
+}
+
+export default HeroSection
diff --git a/src/components/home/HeroSection/index.ts b/src/components/home/HeroSection/index.ts
new file mode 100644
index 0000000..22de459
--- /dev/null
+++ b/src/components/home/HeroSection/index.ts
@@ -0,0 +1 @@
+export { default } from './HeroSection'
diff --git a/src/components/home/VerifySection/DocumentRenderer.test.tsx b/src/components/home/VerifySection/DocumentRenderer.test.tsx
new file mode 100644
index 0000000..73fe5ea
--- /dev/null
+++ b/src/components/home/VerifySection/DocumentRenderer.test.tsx
@@ -0,0 +1,130 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import DocumentRenderer from './DocumentRenderer'
+
+// Mock FrameConnector
+vi.mock('@trustvc/decentralized-renderer-react-components', () => ({
+ FrameConnector: vi.fn(({ style }: any) => (
+
+ )),
+ renderDocument: vi.fn(() => ({ type: 'RENDER_DOCUMENT' })),
+ selectTemplate: vi.fn((id: string) => ({
+ type: 'SELECT_TEMPLATE',
+ payload: id,
+ })),
+ print: vi.fn(() => ({ type: 'PRINT' })),
+ FrameActions: {},
+}))
+
+// Mock helper functions
+vi.mock('../../../utils/helper', async () => {
+ const actual = await vi.importActual('../../../utils/helper')
+ return {
+ ...actual,
+ getTemplateSourceUrl: vi.fn(() => 'https://renderer.example.com'),
+ getOpenAttestationData: vi.fn((doc: any) => doc),
+ getQRCodeLink: vi.fn(() => undefined),
+ getAttachments: vi.fn(() => []),
+ }
+})
+
+// Mock Spinner
+vi.mock('../../common/Spinner', () => ({
+ default: ({ label }: { label: string }) => (
+ {label}
+ ),
+}))
+
+// Mock QRCodeSVG
+vi.mock('qrcode.react', () => ({
+ QRCodeSVG: () =>
,
+}))
+
+// Mock Icons
+vi.mock('../../common/Icons', () => ({
+ QRCodeIcon: () => ,
+ PrinterIcon: () => ,
+ DownloadIcon: () => ,
+ FileIcon: ({ filename }: { filename: string }) => (
+
+ ),
+}))
+
+const helperModule = await import('../../../utils/helper')
+
+describe('DocumentRenderer', () => {
+ const defaultProps = {
+ rawDocument: { some: 'document' },
+ fileName: 'test-file.json',
+ }
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ vi.mocked(helperModule.getTemplateSourceUrl).mockReturnValue(
+ 'https://renderer.example.com'
+ )
+ vi.mocked(helperModule.getQRCodeLink).mockReturnValue(undefined)
+ vi.mocked(helperModule.getAttachments).mockReturnValue([])
+ })
+
+ it('renders nothing when no templateSource', () => {
+ vi.mocked(helperModule.getTemplateSourceUrl).mockReturnValue(undefined)
+ const { container } = render( )
+ expect(container.innerHTML).toBe('')
+ })
+
+ it('renders FrameConnector when templateSource exists', () => {
+ render( )
+ expect(screen.getByTestId('frame-connector')).toBeTruthy()
+ })
+
+ it('shows loading spinner initially', () => {
+ render( )
+ expect(screen.getByTestId('spinner')).toBeTruthy()
+ expect(screen.getByText('Loading document preview...')).toBeTruthy()
+ })
+
+ it('hides tabs wrapper before renderer is ready', () => {
+ const { container } = render( )
+ const tabsWrapper = container.querySelector('.vr-template-tabs-wrapper')
+ expect(tabsWrapper).toBeTruthy()
+ expect((tabsWrapper as HTMLElement).style.display).toBe('none')
+ })
+
+ it('renders FrameConnector with correct source', () => {
+ const { container } = render( )
+ const frame = container.querySelector('[data-testid="frame-connector"]')
+ expect(frame).toBeTruthy()
+ })
+
+ describe('with attachments', () => {
+ beforeEach(() => {
+ vi.mocked(helperModule.getAttachments).mockReturnValue([
+ { filename: 'doc.pdf', data: 'base64data', type: 'application/pdf' },
+ { filename: 'image.png', data: 'imgdata', type: 'image/png' },
+ ])
+ })
+
+ it('does not show attachment tab before renderer is ready', () => {
+ const { container } = render( )
+ // Tabs wrapper is hidden until ready
+ const tabsWrapper = container.querySelector('.vr-template-tabs-wrapper')
+ expect((tabsWrapper as HTMLElement).style.display).toBe('none')
+ })
+ })
+
+ describe('with QR code', () => {
+ beforeEach(() => {
+ vi.mocked(helperModule.getQRCodeLink).mockReturnValue(
+ 'https://qr.example.com'
+ )
+ })
+
+ it('passes QR code URL to helper', () => {
+ render( )
+ expect(helperModule.getQRCodeLink).toHaveBeenCalledWith(
+ defaultProps.rawDocument
+ )
+ })
+ })
+})
diff --git a/src/components/home/VerifySection/DocumentRenderer.tsx b/src/components/home/VerifySection/DocumentRenderer.tsx
new file mode 100644
index 0000000..a3372fb
--- /dev/null
+++ b/src/components/home/VerifySection/DocumentRenderer.tsx
@@ -0,0 +1,391 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import {
+ FrameConnector,
+ renderDocument,
+ selectTemplate,
+ print as framePrint,
+ FrameActions,
+} from '@trustvc/decentralized-renderer-react-components'
+import { QRCodeSVG } from 'qrcode.react'
+import Spinner from '../../common/Spinner'
+import {
+ getTemplateSourceUrl,
+ getOpenAttestationData,
+ getQRCodeLink,
+ getAttachments,
+ formatFileSize,
+ DocumentAttachment,
+} from '../../../utils/helper'
+import {
+ QRCodeIcon,
+ PrinterIcon,
+ DownloadIcon,
+ FileIcon,
+} from '../../common/Icons'
+
+interface TemplateTab {
+ id: string
+ label: string
+}
+
+const SCROLLBAR_WIDTH = 20
+
+interface DocumentRendererProps {
+ rawDocument: unknown
+ fileName: string
+}
+
+const DocumentRenderer: React.FC = ({
+ rawDocument,
+ fileName,
+}) => {
+ const toFrame = useRef()
+ const [templates, setTemplates] = useState([])
+ const [selectedTemplate, setSelectedTemplate] = useState('')
+ const [rendererHeight, setRendererHeight] = useState(250)
+ const [isRendererReady, setIsRendererReady] = useState(false)
+ const [qrCodePopover, setQrCodePopover] = useState(false)
+ const qrWrapperRef = useRef(null)
+
+ const document = useMemo(
+ () => (rawDocument ? getOpenAttestationData(rawDocument) : undefined),
+ [rawDocument]
+ )
+ const templateSource = useMemo(
+ () => (rawDocument ? getTemplateSourceUrl(rawDocument) : undefined),
+ [rawDocument]
+ )
+ const qrCodeUrl = useMemo(
+ () => (rawDocument ? getQRCodeLink(rawDocument) : undefined),
+ [rawDocument]
+ )
+ const attachments = useMemo(
+ () => (rawDocument ? getAttachments(rawDocument) : []),
+ [rawDocument]
+ )
+
+ const onConnected = useCallback(
+ (frame: any) => {
+ toFrame.current = frame
+ if (toFrame.current) {
+ toFrame.current(
+ renderDocument({ document, rawDocument: rawDocument as any })
+ )
+ }
+ },
+ [document, rawDocument]
+ )
+
+ const handleFrameDispatch = useCallback((action: FrameActions): void => {
+ if (action.type === 'UPDATE_HEIGHT') {
+ setRendererHeight((action as any).payload + SCROLLBAR_WIDTH)
+ }
+ if (action.type === 'UPDATE_TEMPLATES') {
+ const newTemplates = (action as any).payload as TemplateTab[]
+ const filtered = newTemplates.filter(
+ (t: any) =>
+ t.type === 'custom-template' ||
+ t.type === 'application/pdf' ||
+ !t.type
+ )
+ setTemplates(filtered)
+ if (filtered.length > 0) {
+ setSelectedTemplate(filtered[0].id)
+ }
+ setIsRendererReady(true)
+ }
+ }, [])
+
+ useEffect(() => {
+ if (toFrame.current && document) {
+ toFrame.current(renderDocument({ document }))
+ }
+ }, [document])
+
+ useEffect(() => {
+ if (
+ toFrame.current &&
+ selectedTemplate &&
+ selectedTemplate !== 'attachmentTab'
+ ) {
+ toFrame.current(selectTemplate(selectedTemplate))
+ }
+ }, [selectedTemplate])
+
+ useEffect(() => {
+ if (!qrCodePopover) return
+ const handleClickOutside = (e: MouseEvent) => {
+ if (
+ qrWrapperRef.current &&
+ !qrWrapperRef.current.contains(e.target as Node)
+ ) {
+ setQrCodePopover(false)
+ }
+ }
+ const handleEscape = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ setQrCodePopover(false)
+ }
+ }
+ window.document.addEventListener('mousedown', handleClickOutside)
+ window.document.addEventListener('keydown', handleEscape)
+ return () => {
+ window.document.removeEventListener('mousedown', handleClickOutside)
+ window.document.removeEventListener('keydown', handleEscape)
+ }
+ }, [qrCodePopover])
+
+ const handlePrint = () => {
+ if (toFrame.current) {
+ toFrame.current(framePrint())
+ }
+ }
+
+ const downloadHref = useMemo(
+ () =>
+ rawDocument
+ ? `data:application/json;charset=utf-8,${encodeURIComponent(JSON.stringify(rawDocument, null, 2))}`
+ : undefined,
+ [rawDocument]
+ )
+
+ if (!templateSource) return null
+
+ return (
+
+ {/* Template tabs β hidden until renderer is ready */}
+
+
+ {templates.map(({ id, label }) => (
+ setSelectedTemplate(id)}
+ >
+ {label}
+
+ ))}
+ {attachments.length > 0 && (
+ setSelectedTemplate('attachmentTab')}
+ >
+ Attachments
+ {attachments.length}
+
+ )}
+
+
+
+ {/* Content card */}
+
+ {/* Attachments pane */}
+ {selectedTemplate === 'attachmentTab' && (
+
+ )}
+
+ {/* Document utility toolbar */}
+ {templates.length > 0 && selectedTemplate !== 'attachmentTab' && (
+
+
+ {selectedTemplate && selectedTemplate !== 'default-template' && (
+
+
Rendered View:
+
+ {(
+ templates.find(t => t.id === selectedTemplate)?.label ??
+ selectedTemplate
+ )
+ .trim()
+ .toUpperCase()}{' '}
+ rendered from{' '}
+
+ {templateSource}
+
+
+
+ )}
+
+ {qrCodeUrl && (
+
+
setQrCodePopover(!qrCodePopover)}
+ >
+
+
+
+
+ {qrCodePopover && (
+
+
+
+ )}
+
+ )}
+
+
+
+ {downloadHref && (
+
+
+
+
+
+ )}
+
+
+
+ )}
+
+ {/* Loading spinner */}
+ {!isRendererReady && selectedTemplate !== 'attachmentTab' && (
+
+
+
+ )}
+
+ {/* Renderer iframe */}
+
+
+
+
+
+ )
+}
+
+// ββ Attachments sub-component ββ
+
+const SAFE_MIME_TYPES = new Set([
+ 'application/pdf',
+ 'application/json',
+ 'application/xml',
+ 'application/octet-stream',
+ 'image/png',
+ 'image/jpeg',
+ 'image/gif',
+ 'text/plain',
+ 'text/csv',
+ 'text/xml',
+])
+
+const getSafeDownloadHref = (type: string, data: string): string => {
+ const mimeType = SAFE_MIME_TYPES.has(type) ? type : 'application/octet-stream'
+ return `data:${mimeType};base64,${data}`
+}
+
+const AttachmentsPane: React.FC<{ attachments: DocumentAttachment[] }> = ({
+ attachments,
+}) => (
+
+ {attachments.map((att, idx) => (
+
+
+
+
+
+ {att.filename}
+
+ {att.type}
+ {att.data ? ` Β· ${formatFileSize(att.data)}` : ''}
+
+
+
+
+
+
+ ))}
+
+)
+
+export default DocumentRenderer
diff --git a/src/components/home/VerifySection/NetworkModal.test.tsx b/src/components/home/VerifySection/NetworkModal.test.tsx
new file mode 100644
index 0000000..881bdfa
--- /dev/null
+++ b/src/components/home/VerifySection/NetworkModal.test.tsx
@@ -0,0 +1,309 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { render, screen, fireEvent } from '@testing-library/react'
+import NetworkModal from './NetworkModal'
+
+// βββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+const defaultProps = {
+ isDarkMode: false,
+ fileName: 'document.tt',
+ onConfirm: vi.fn(),
+ onCancel: vi.fn(),
+ networkType: 'testnet' as const,
+}
+
+// The dropdown toggle button has class nm-dropdown-btn
+const getDropdownToggle = () =>
+ document.querySelector('button.nm-dropdown-btn') as HTMLElement
+
+// βββ Tests ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+describe('NetworkModal', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ // ββ Rendering ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('rendering', () => {
+ it('renders the header title', () => {
+ render( )
+ expect(screen.getByText('TrustVC Document Uploaded')).toBeInTheDocument()
+ })
+
+ it('renders the fileName in the header', () => {
+ render( )
+ expect(screen.getByText('my-doc.tt')).toBeInTheDocument()
+ })
+
+ it('does not render the fileName when it is empty', () => {
+ render( )
+ // Only the title should appear, not an empty filename
+ expect(screen.getByText('TrustVC Document Uploaded')).toBeInTheDocument()
+ })
+
+ it('renders the subtitle text', () => {
+ render( )
+ expect(
+ screen.getByText('Select network for document verification.')
+ ).toBeInTheDocument()
+ })
+
+ it('renders the Select Network label', () => {
+ render( )
+ expect(screen.getByText('Select Network:')).toBeInTheDocument()
+ })
+
+ it('renders Cancel and Proceed buttons', () => {
+ render( )
+ expect(
+ screen.getByRole('button', { name: /cancel/i })
+ ).toBeInTheDocument()
+ expect(
+ screen.getByRole('button', { name: /proceed/i })
+ ).toBeInTheDocument()
+ })
+
+ it('renders the info button with aria-label "Network selector info"', () => {
+ render( )
+ expect(
+ screen.getByRole('button', { name: /network selector info/i })
+ ).toBeInTheDocument()
+ })
+
+ it('shows Sepolia as the default selected network in testnet mode', () => {
+ render( )
+ expect(getDropdownToggle().textContent).toContain('Sepolia')
+ })
+ })
+
+ // ββ Scroll lock ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('scroll lock', () => {
+ it('sets body overflow to hidden on mount', () => {
+ render( )
+ expect(document.body.style.overflow).toBe('hidden')
+ })
+
+ it('restores the previous body overflow on unmount', () => {
+ document.body.style.overflow = 'scroll'
+ const { unmount } = render( )
+ expect(document.body.style.overflow).toBe('hidden')
+ unmount()
+ expect(document.body.style.overflow).toBe('scroll')
+ })
+ })
+
+ // ββ Network filtering ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('network filtering by networkType prop', () => {
+ describe('testnet mode', () => {
+ it('shows only Testnet group header', () => {
+ render( )
+ fireEvent.click(getDropdownToggle())
+ expect(screen.getAllByText('Testnet').length).toBeGreaterThan(0)
+ expect(screen.queryByText('Mainnet')).not.toBeInTheDocument()
+ })
+
+ it('shows testnet networks but not mainnet networks', () => {
+ render( )
+ fireEvent.click(getDropdownToggle())
+ expect(screen.getAllByText('Sepolia').length).toBeGreaterThan(0)
+ expect(screen.getByText('Polygon Amoy')).toBeInTheDocument()
+ expect(screen.getByText('Apothem')).toBeInTheDocument()
+ expect(screen.queryByText('Polygon')).not.toBeInTheDocument()
+ expect(screen.queryByText('XDC Network')).not.toBeInTheDocument()
+ })
+
+ it('defaults to Sepolia (chainId 11155111)', () => {
+ render( )
+ expect(getDropdownToggle().textContent).toContain('Sepolia')
+ })
+
+ it('calls onConfirm with testnet chainId when Proceed is clicked', () => {
+ const onConfirm = vi.fn()
+ render(
+
+ )
+ fireEvent.click(screen.getByRole('button', { name: /proceed/i }))
+ expect(onConfirm).toHaveBeenCalledWith('11155111')
+ })
+ })
+
+ describe('mainnet mode', () => {
+ it('shows only Mainnet group header', () => {
+ render( )
+ fireEvent.click(getDropdownToggle())
+ expect(screen.getAllByText('Mainnet').length).toBeGreaterThan(0)
+ expect(screen.queryByText('Testnet')).not.toBeInTheDocument()
+ })
+
+ it('shows mainnet networks but not testnet networks', () => {
+ render( )
+ fireEvent.click(getDropdownToggle())
+ expect(screen.getAllByText('Ethereum').length).toBeGreaterThan(0)
+ expect(screen.getByText('Polygon')).toBeInTheDocument()
+ expect(screen.getByText('XDC Network')).toBeInTheDocument()
+ expect(screen.queryByText('Sepolia')).not.toBeInTheDocument()
+ expect(screen.queryByText('Polygon Amoy')).not.toBeInTheDocument()
+ })
+
+ it('defaults to Ethereum (chainId 1)', () => {
+ render( )
+ expect(getDropdownToggle().textContent).toContain('Ethereum')
+ })
+
+ it('calls onConfirm with mainnet chainId when Proceed is clicked', () => {
+ const onConfirm = vi.fn()
+ render(
+
+ )
+ fireEvent.click(screen.getByRole('button', { name: /proceed/i }))
+ expect(onConfirm).toHaveBeenCalledWith('1')
+ })
+
+ it('allows selecting a different mainnet network before confirming', () => {
+ const onConfirm = vi.fn()
+ render(
+
+ )
+ fireEvent.click(getDropdownToggle())
+ fireEvent.click(screen.getByText('Polygon'))
+ fireEvent.click(screen.getByRole('button', { name: /proceed/i }))
+ expect(onConfirm).toHaveBeenCalledWith('137')
+ })
+ })
+ })
+
+ // ββ Dropdown βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('dropdown', () => {
+ it('is closed by default β network options are not visible', () => {
+ render( )
+ expect(screen.queryByText('Polygon Amoy')).not.toBeInTheDocument()
+ expect(screen.queryByText('Apothem')).not.toBeInTheDocument()
+ })
+
+ it('opens and shows all testnet options when the toggle is clicked', () => {
+ render( )
+ fireEvent.click(getDropdownToggle())
+ expect(screen.getAllByText('Sepolia').length).toBeGreaterThan(0)
+ expect(screen.getByText('Polygon Amoy')).toBeInTheDocument()
+ expect(screen.getByText('Apothem')).toBeInTheDocument()
+ expect(screen.getByText('Astron Testnet')).toBeInTheDocument()
+ })
+
+ it('closes when the toggle is clicked a second time', () => {
+ render( )
+ fireEvent.click(getDropdownToggle())
+ expect(screen.getByText('Polygon Amoy')).toBeInTheDocument()
+ fireEvent.click(getDropdownToggle())
+ expect(screen.queryByText('Polygon Amoy')).not.toBeInTheDocument()
+ })
+
+ it('selects the clicked network and closes the dropdown', () => {
+ render( )
+ fireEvent.click(getDropdownToggle())
+ fireEvent.click(screen.getByText('Polygon Amoy'))
+ // Dropdown should be closed
+ expect(screen.queryByText('Apothem')).not.toBeInTheDocument()
+ // Polygon Amoy should now appear in the toggle
+ expect(getDropdownToggle().textContent).toContain('Polygon Amoy')
+ })
+
+ it('closes when the backdrop overlay is clicked', () => {
+ render( )
+ fireEvent.click(getDropdownToggle())
+ expect(screen.getByText('Polygon Amoy')).toBeInTheDocument()
+ const backdrop = document.querySelector(
+ '.fixed.inset-0.z-\\[9\\]'
+ ) as HTMLElement
+ fireEvent.click(backdrop)
+ expect(screen.queryByText('Polygon Amoy')).not.toBeInTheDocument()
+ })
+ })
+
+ // ββ Tooltip ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('tooltip', () => {
+ it('is hidden by default', () => {
+ render( )
+ expect(screen.queryByText('Network Selector')).not.toBeInTheDocument()
+ })
+
+ it('appears when hovering over the info button', () => {
+ render( )
+ fireEvent.mouseEnter(
+ screen.getByRole('button', { name: /network selector info/i })
+ )
+ expect(screen.getByText('Network Selector')).toBeInTheDocument()
+ expect(
+ screen.getByText(/A document can only be successfully verified/)
+ ).toBeInTheDocument()
+ expect(
+ screen.getByText(/If unsure, do check with the document issuer/)
+ ).toBeInTheDocument()
+ })
+
+ it('disappears when the mouse leaves the info button', () => {
+ render( )
+ const infoBtn = screen.getByRole('button', {
+ name: /network selector info/i,
+ })
+ fireEvent.mouseEnter(infoBtn)
+ expect(screen.getByText('Network Selector')).toBeInTheDocument()
+ fireEvent.mouseLeave(infoBtn)
+ expect(screen.queryByText('Network Selector')).not.toBeInTheDocument()
+ })
+ })
+
+ // ββ Callbacks ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('callbacks', () => {
+ it('calls onCancel when the Cancel button is clicked', () => {
+ render( )
+ fireEvent.click(screen.getByRole('button', { name: /cancel/i }))
+ expect(defaultProps.onCancel).toHaveBeenCalledTimes(1)
+ })
+
+ it('calls onConfirm with chainId "11155111" (Sepolia) by default', () => {
+ render( )
+ fireEvent.click(screen.getByRole('button', { name: /proceed/i }))
+ expect(defaultProps.onConfirm).toHaveBeenCalledWith('11155111')
+ })
+
+ it('calls onConfirm with the newly selected chainId after changing network', () => {
+ render( )
+ fireEvent.click(getDropdownToggle())
+ fireEvent.click(screen.getByText('Polygon Amoy'))
+ fireEvent.click(screen.getByRole('button', { name: /proceed/i }))
+ expect(defaultProps.onConfirm).toHaveBeenCalledWith('80002')
+ })
+
+ it('calls onCancel when clicking the overlay backdrop', () => {
+ render( )
+ const overlay = document.querySelector('.bg-black\\/50') as HTMLElement
+ fireEvent.click(overlay)
+ expect(defaultProps.onCancel).toHaveBeenCalledTimes(1)
+ })
+
+ it('does not call onCancel when clicking inside the modal card', () => {
+ render( )
+ const card = document.querySelector('.nm-card') as HTMLElement
+ fireEvent.click(card)
+ expect(defaultProps.onCancel).not.toHaveBeenCalled()
+ })
+ })
+})
diff --git a/src/components/home/VerifySection/NetworkModal.tsx b/src/components/home/VerifySection/NetworkModal.tsx
new file mode 100644
index 0000000..434b58e
--- /dev/null
+++ b/src/components/home/VerifySection/NetworkModal.tsx
@@ -0,0 +1,413 @@
+import React, { useState, useEffect } from 'react'
+import NetworkTooltip from './NetworkTooltip'
+
+interface NetworkOption {
+ chainId: string
+ label: string
+ group: 'Mainnet' | 'Testnet'
+ logo: string
+}
+
+const NETWORK_OPTIONS: NetworkOption[] = [
+ {
+ chainId: '1',
+ label: 'Ethereum',
+ group: 'Mainnet',
+ logo: '/images/networks/ethereum.gif',
+ },
+ {
+ chainId: '137',
+ label: 'Polygon',
+ group: 'Mainnet',
+ logo: '/images/networks/polygon.gif',
+ },
+ {
+ chainId: '50',
+ label: 'XDC Network',
+ group: 'Mainnet',
+ logo: '/images/networks/xdc.png',
+ },
+ {
+ chainId: '101010',
+ label: 'Stability (Beta)',
+ group: 'Mainnet',
+ logo: '/images/networks/stability.png',
+ },
+ {
+ chainId: '1338',
+ label: 'Astron',
+ group: 'Mainnet',
+ logo: '/images/networks/astron.png',
+ },
+ {
+ chainId: '11155111',
+ label: 'Sepolia',
+ group: 'Testnet',
+ logo: '/images/networks/ethereum.gif',
+ },
+ {
+ chainId: '80002',
+ label: 'Polygon Amoy',
+ group: 'Testnet',
+ logo: '/images/networks/polygon.gif',
+ },
+ {
+ chainId: '51',
+ label: 'Apothem',
+ group: 'Testnet',
+ logo: '/images/networks/xdc.png',
+ },
+ {
+ chainId: '20180427',
+ label: 'Stability Testnet (Beta)',
+ group: 'Testnet',
+ logo: '/images/networks/stability.png',
+ },
+ {
+ chainId: '21002',
+ label: 'Astron Testnet',
+ group: 'Testnet',
+ logo: '/images/networks/astron.png',
+ },
+]
+
+interface NetworkModalProps {
+ isDarkMode: boolean
+ fileName: string
+ onConfirm: (_chainId: string) => void
+ onCancel: () => void
+ networkType?: 'mainnet' | 'testnet'
+}
+
+const NetworkModal: React.FC = ({
+ fileName,
+ onConfirm,
+ onCancel,
+ networkType: networkTypeProp,
+}) => {
+ const envNetworkType = (import.meta.env.VITE_NETWORK_TYPE || '').toLowerCase()
+ const networkType =
+ networkTypeProp || (envNetworkType === 'mainnet' ? 'mainnet' : 'testnet')
+ const initialChainId = networkType === 'mainnet' ? '1' : '11155111'
+
+ const [selectedChainId, setSelectedChainId] = useState(initialChainId)
+ const [dropdownOpen, setDropdownOpen] = useState(false)
+ const [isTooltipVisible, setIsTooltipVisible] = useState(false)
+ const [tooltipPosition, setTooltipPosition] = useState({
+ top: 0,
+ left: 0,
+ width: 0,
+ })
+
+ const handleInfoMouseEnter = (e: React.MouseEvent) => {
+ const rect = e.currentTarget.getBoundingClientRect()
+ const tooltipWidth = 280
+ const padding = 8
+
+ // Calculate left position with boundary checking
+ let left = rect.left - tooltipWidth + rect.width
+
+ // If tooltip would go off left edge, add padding from left
+ if (left < padding) {
+ left = padding
+ }
+
+ // If tooltip would go off right edge, align to right with padding
+ if (left + tooltipWidth > window.innerWidth - padding) {
+ left = window.innerWidth - tooltipWidth - padding
+ }
+
+ setTooltipPosition({
+ top: rect.bottom + 8,
+ left,
+ width: tooltipWidth,
+ })
+ setIsTooltipVisible(true)
+ }
+
+ const handleInfoFocus = (e: React.FocusEvent) => {
+ const rect = e.currentTarget.getBoundingClientRect()
+ const tooltipWidth = 280
+ const padding = 8
+
+ let left = rect.left - tooltipWidth + rect.width
+
+ if (left < padding) {
+ left = padding
+ }
+
+ if (left + tooltipWidth > window.innerWidth - padding) {
+ left = window.innerWidth - tooltipWidth - padding
+ }
+
+ setTooltipPosition({
+ top: rect.bottom + 8,
+ left,
+ width: tooltipWidth,
+ })
+ setIsTooltipVisible(true)
+ }
+
+ useEffect(() => {
+ const prev = document.body.style.overflow
+ document.body.style.overflow = 'hidden'
+
+ const handleEscape = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ onCancel()
+ }
+ }
+
+ document.addEventListener('keydown', handleEscape)
+
+ return () => {
+ document.body.style.overflow = prev
+ document.removeEventListener('keydown', handleEscape)
+ }
+ }, [onCancel])
+
+ const allowedGroups: ('Mainnet' | 'Testnet')[] =
+ networkType === 'mainnet' ? ['Mainnet'] : ['Testnet']
+ const visibleOptions = NETWORK_OPTIONS.filter(n =>
+ allowedGroups.includes(n.group)
+ )
+ const selected =
+ visibleOptions.find(n => n.chainId === selectedChainId) ?? visibleOptions[0]
+
+ return (
+ {
+ if (e.target === e.currentTarget) onCancel()
+ }}
+ >
+ {/* Card */}
+
+ {/* Header */}
+
+ {/* Green check-circle icon */}
+
+
+ {/* Title + fileName */}
+
+
+ TrustVC Document Uploaded
+
+ {fileName &&
{fileName}
}
+
+
+
+ {/* Content */}
+
+ {/* Description */}
+
+
+ Select network for document verification.
+
+
+
+ {/* Network selector row */}
+
+ {/* Label */}
+
+ Select Network:
+
+
+ {/* Dropdown + Info */}
+
+ {/* Dropdown */}
+
+
+
setDropdownOpen(v => !v)}
+ >
+ {selected && (
+
+ )}
+
+ {selected?.label ?? 'Select network'}
+
+ {/* Divider + chevron */}
+
+
+
+ {/* Dropdown overlay */}
+ {dropdownOpen && (
+
setDropdownOpen(false)}
+ />
+ )}
+
+ {/* Dropdown list */}
+ {dropdownOpen && (
+
+ {allowedGroups.map(group => (
+
+
{group}
+ {visibleOptions
+ .filter(n => n.group === group)
+ .map(n => (
+
{
+ setSelectedChainId(n.chainId)
+ setDropdownOpen(false)
+ }}
+ role="option"
+ aria-selected={n.chainId === selectedChainId}
+ >
+
+ {n.label}
+
+ ))}
+
+ ))}
+
+ )}
+
+
+
+ {/* Info button */}
+
+
setIsTooltipVisible(false)}
+ onFocus={handleInfoFocus}
+ onBlur={() => setIsTooltipVisible(false)}
+ aria-label="Network selector info"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Tooltip */}
+
+
+ {/* Footer */}
+
+
+ {/* Cancel */}
+
+ Cancel
+
+
+ {/* Proceed */}
+ selected && onConfirm(selectedChainId)}
+ disabled={!selected}
+ >
+ Proceed
+
+
+
+
+
+ )
+}
+
+export default NetworkModal
diff --git a/src/components/home/VerifySection/NetworkTooltip.test.tsx b/src/components/home/VerifySection/NetworkTooltip.test.tsx
new file mode 100644
index 0000000..7c37e10
--- /dev/null
+++ b/src/components/home/VerifySection/NetworkTooltip.test.tsx
@@ -0,0 +1,91 @@
+import React from 'react'
+import { describe, it, expect } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import NetworkTooltip from './NetworkTooltip'
+
+// βββ Tests ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+describe('NetworkTooltip', () => {
+ const position = { top: 100, left: 200, width: 280 }
+
+ // ββ Visibility βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('visibility', () => {
+ it('renders nothing when isVisible is false', () => {
+ const { container } = render(
+
+ )
+ expect(container).toBeEmptyDOMElement()
+ })
+
+ it('renders the tooltip when isVisible is true', () => {
+ render(
)
+ expect(screen.getByText('Network Selector')).toBeInTheDocument()
+ })
+ })
+
+ // ββ Content ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('content', () => {
+ it('renders the title "Network Selector"', () => {
+ render(
)
+ expect(screen.getByText('Network Selector')).toBeInTheDocument()
+ })
+
+ it('renders the first body paragraph about same-network verification', () => {
+ render(
)
+ expect(
+ screen.getByText(
+ /A document can only be successfully verified on the same network/
+ )
+ ).toBeInTheDocument()
+ })
+
+ it('renders the second body paragraph about checking with the issuer', () => {
+ render(
)
+ expect(
+ screen.getByText(/If unsure, do check with the document issuer/)
+ ).toBeInTheDocument()
+ })
+ })
+
+ // ββ Positioning ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('positioning', () => {
+ it('applies top, left, and width from the position prop', () => {
+ const { container } = render(
+
+ )
+ const wrapper = container.firstChild as HTMLElement
+ expect(wrapper.style.top).toBe('50px')
+ expect(wrapper.style.left).toBe('120px')
+ expect(wrapper.style.width).toBe('300px')
+ })
+
+ it('has the "fixed" positioning class on the outer wrapper', () => {
+ const { container } = render(
+
+ )
+ const wrapper = container.firstChild as HTMLElement
+ expect(wrapper.className).toContain('fixed')
+ })
+
+ it('has the "pointer-events-none" class on the outer wrapper', () => {
+ const { container } = render(
+
+ )
+ const wrapper = container.firstChild as HTMLElement
+ expect(wrapper.className).toContain('pointer-events-none')
+ })
+
+ it('renders the inner content with the nm-tooltip-inner class', () => {
+ const { container } = render(
+
+ )
+ expect(container.querySelector('.nm-tooltip-inner')).toBeInTheDocument()
+ })
+ })
+})
diff --git a/src/components/home/VerifySection/NetworkTooltip.tsx b/src/components/home/VerifySection/NetworkTooltip.tsx
new file mode 100644
index 0000000..62ef530
--- /dev/null
+++ b/src/components/home/VerifySection/NetworkTooltip.tsx
@@ -0,0 +1,41 @@
+import React from 'react'
+
+interface NetworkTooltipProps {
+ isVisible: boolean
+ position: {
+ top: number
+ left: number
+ width: number
+ }
+}
+
+const NetworkTooltip: React.FC
= ({
+ isVisible,
+ position,
+}) => {
+ if (!isVisible) return null
+
+ return (
+
+
+
Network Selector
+
+ A document can only be successfully verified on the same network where
+ the document was created in.
+
+
+ If unsure, do check with the document issuer.
+
+
+
+ )
+}
+
+export default NetworkTooltip
diff --git a/src/components/home/VerifySection/VerifyError.test.tsx b/src/components/home/VerifySection/VerifyError.test.tsx
new file mode 100644
index 0000000..f85fc49
--- /dev/null
+++ b/src/components/home/VerifySection/VerifyError.test.tsx
@@ -0,0 +1,47 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { render, fireEvent } from '@testing-library/react'
+import VerifyError from './VerifyError'
+
+describe('VerifyError', () => {
+ const defaultProps = {
+ errorMessage: 'Something went wrong',
+ onReset: vi.fn(),
+ }
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('renders the error message', () => {
+ const { getByText } = render( )
+ expect(getByText('Something went wrong')).toBeTruthy()
+ })
+
+ it('renders Try again button', () => {
+ const { getByText } = render( )
+ expect(getByText('Try again')).toBeTruthy()
+ })
+
+ it('calls onReset when Try again is clicked', () => {
+ const onReset = vi.fn()
+ const { getByText } = render(
+
+ )
+ fireEvent.click(getByText('Try again'))
+ expect(onReset).toHaveBeenCalledOnce()
+ })
+
+ it('renders error icon SVG', () => {
+ const { container } = render( )
+ const svg = container.querySelector('svg')
+ expect(svg).toBeTruthy()
+ const circle = container.querySelector('circle')
+ expect(circle?.getAttribute('fill')).toBe('#ef4444')
+ })
+
+ it('renders with correct container classes', () => {
+ const { container } = render( )
+ expect(container.querySelector('.frame-dropbox')).toBeTruthy()
+ expect(container.querySelector('.dropbox-area--centered')).toBeTruthy()
+ })
+})
diff --git a/src/components/home/VerifySection/VerifyError.tsx b/src/components/home/VerifySection/VerifyError.tsx
new file mode 100644
index 0000000..54e8b9a
--- /dev/null
+++ b/src/components/home/VerifySection/VerifyError.tsx
@@ -0,0 +1,28 @@
+import React from 'react'
+
+interface VerifyErrorProps {
+ errorMessage: string
+ onReset: () => void
+}
+
+const VerifyError: React.FC = ({ errorMessage, onReset }) => (
+
+
+
+
+
+
+
{errorMessage}
+
+ Try again
+
+
+
+)
+
+export default VerifyError
diff --git a/src/components/home/VerifySection/VerifyResult.test.tsx b/src/components/home/VerifySection/VerifyResult.test.tsx
new file mode 100644
index 0000000..9df5075
--- /dev/null
+++ b/src/components/home/VerifySection/VerifyResult.test.tsx
@@ -0,0 +1,386 @@
+import React from 'react'
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { render, screen, fireEvent } from '@testing-library/react'
+import VerifyResult from './VerifyResult'
+
+// βββ Mocks ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+// Stub NetworkTooltip to avoid jsdom getBoundingClientRect side-effects
+vi.mock('./NetworkTooltip', () => ({
+ default: ({ isVisible }: { isVisible: boolean }) =>
+ isVisible ? Tooltip visible
: null,
+}))
+
+// Stub makeExplorerAddressURL so tests control the returned URL
+vi.mock('./useVerify', async () => {
+ const actual =
+ await vi.importActual('./useVerify')
+ return { ...actual, makeExplorerAddressURL: vi.fn() }
+})
+
+import { makeExplorerAddressURL } from './useVerify'
+
+// βββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+const defaultProps = {
+ fileName: 'test.tt',
+ getGroupStatus: vi.fn().mockReturnValue('VALID' as const),
+ onReset: vi.fn(),
+}
+
+// βββ Tests ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+describe('VerifyResult', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ vi.mocked(defaultProps.getGroupStatus).mockReturnValue('VALID')
+ vi.mocked(makeExplorerAddressURL).mockReturnValue(undefined)
+ })
+
+ // ββ Core content βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('core content', () => {
+ it('renders the Upload New File button', () => {
+ render( )
+ expect(
+ screen.getByRole('button', { name: /upload new file/i })
+ ).toBeInTheDocument()
+ })
+
+ it('renders the "Issued by:" label', () => {
+ render( )
+ expect(screen.getByText('Issued by:')).toBeInTheDocument()
+ })
+
+ it('shows fileName when no issuer is provided', () => {
+ render( )
+ expect(screen.getByText('my-doc.tt')).toBeInTheDocument()
+ })
+
+ it('shows issuer instead of fileName when issuer is provided', () => {
+ render(
+
+ )
+ expect(screen.getByText('EXAMPLE.COM')).toBeInTheDocument()
+ expect(screen.queryByText('doc.tt')).not.toBeInTheDocument()
+ })
+
+ it('renders all three verification check labels', () => {
+ render( )
+ expect(screen.getByText('Document has been issued')).toBeInTheDocument()
+ expect(
+ screen.getByText("Document's issuer has been identified")
+ ).toBeInTheDocument()
+ expect(
+ screen.getByText('Document has not been tampered with')
+ ).toBeInTheDocument()
+ })
+ })
+
+ // ββ onReset ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('onReset', () => {
+ it('calls onReset when Upload New File is clicked', () => {
+ const onReset = vi.fn()
+ render( )
+ fireEvent.click(screen.getByRole('button', { name: /upload new file/i }))
+ expect(onReset).toHaveBeenCalledTimes(1)
+ })
+ })
+
+ // ββ Verification check icons βββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('verification check icons', () => {
+ it('renders CheckCircle (green) icons when all checks are VALID', () => {
+ vi.mocked(defaultProps.getGroupStatus).mockReturnValue('VALID')
+ const { container } = render( )
+ expect(
+ container.querySelectorAll('path[stroke="#3AAF86"]').length
+ ).toBeGreaterThan(0)
+ })
+
+ it('renders CrossCircle (red) icons when checks are INVALID', () => {
+ vi.mocked(defaultProps.getGroupStatus).mockReturnValue('INVALID')
+ const { container } = render( )
+ expect(
+ container.querySelectorAll('circle[stroke="#ef4444"]').length
+ ).toBeGreaterThan(0)
+ })
+ })
+
+ // ββ Network card βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('network card', () => {
+ it('does not render the network card when networkName is not provided', () => {
+ render( )
+ expect(
+ screen.queryByText('Document verified on:')
+ ).not.toBeInTheDocument()
+ })
+
+ it('renders the "Document verified on:" label when networkName is provided', () => {
+ render( )
+ expect(screen.getByText('Document verified on:')).toBeInTheDocument()
+ })
+
+ it('renders the network name in the field', () => {
+ render( )
+ expect(screen.getByText('Ethereum')).toBeInTheDocument()
+ })
+
+ it('renders the info (?) button when networkName is provided', () => {
+ render( )
+ expect(
+ screen.getByRole('button', { name: /network info/i })
+ ).toBeInTheDocument()
+ })
+ })
+
+ // ββ Tooltip ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('tooltip', () => {
+ it('is hidden by default', () => {
+ render( )
+ expect(screen.queryByTestId('network-tooltip')).not.toBeInTheDocument()
+ })
+
+ it('shows when hovering over the info button', () => {
+ render( )
+ fireEvent.mouseEnter(
+ screen.getByRole('button', { name: /network info/i })
+ )
+ expect(screen.getByTestId('network-tooltip')).toBeInTheDocument()
+ })
+
+ it('hides when mouse leaves the info button', () => {
+ render( )
+ const infoBtn = screen.getByRole('button', { name: /network info/i })
+ fireEvent.mouseEnter(infoBtn)
+ expect(screen.getByTestId('network-tooltip')).toBeInTheDocument()
+ fireEvent.mouseLeave(infoBtn)
+ expect(screen.queryByTestId('network-tooltip')).not.toBeInTheDocument()
+ })
+ })
+
+ // ββ Tags βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('tags', () => {
+ it('does not render tags section when tags array is empty', () => {
+ render( )
+ expect(screen.queryByText('OA')).not.toBeInTheDocument()
+ })
+
+ it('does not render tags section when tags prop is absent', () => {
+ render( )
+ // No tag chips rendered
+ const { container } = render( )
+ expect(container.querySelector('.vr-issue-tags')).not.toBeInTheDocument()
+ })
+
+ it('renders Transferable and Negotiable as primary tags', () => {
+ const { container } = render(
+
+ )
+ expect(screen.getByText('Transferable')).toBeInTheDocument()
+ expect(screen.getByText('Negotiable')).toBeInTheDocument()
+ expect(container.querySelectorAll('.vr-tag--primary')).toHaveLength(2)
+ })
+
+ it('renders OA / TR V4 / TR V5 / W3C tags as secondary tags', () => {
+ const { container } = render(
+
+ )
+ expect(screen.getByText('OA')).toBeInTheDocument()
+ expect(screen.getByText('TR V4')).toBeInTheDocument()
+ expect(screen.getByText('W3C VC V1.1')).toBeInTheDocument()
+ expect(container.querySelectorAll('.vr-tag--secondary')).toHaveLength(3)
+ })
+
+ it('renders a mix of primary and secondary tags', () => {
+ const { container } = render(
+
+ )
+ expect(container.querySelectorAll('.vr-tag--primary')).toHaveLength(1)
+ expect(container.querySelectorAll('.vr-tag--secondary')).toHaveLength(2)
+ })
+ })
+
+ // ββ NFT links section (isTransferable) ββββββββββββββββββββββββββββββββββββ
+
+ describe('NFT links section', () => {
+ it('does not render NFT links when isTransferable is false', () => {
+ render( )
+ expect(screen.queryByText('View NFT Registry')).not.toBeInTheDocument()
+ expect(
+ screen.queryByText('View Endorsement Chain')
+ ).not.toBeInTheDocument()
+ })
+
+ it('renders View NFT Registry and View Endorsement Chain when isTransferable is true', () => {
+ render( )
+ expect(screen.getByText('View NFT Registry')).toBeInTheDocument()
+ expect(screen.getByText('View Endorsement Chain')).toBeInTheDocument()
+ })
+
+ it('renders View NFT Registry as a plain div when no explorerUrl is resolved', () => {
+ vi.mocked(makeExplorerAddressURL).mockReturnValue(undefined)
+ render(
+
+ )
+ const el = screen.getByText('View NFT Registry')
+ expect(el.tagName).not.toBe('A')
+ })
+
+ it('renders View NFT Registry as an link when explorerUrl is resolved', () => {
+ vi.mocked(makeExplorerAddressURL).mockReturnValue(
+ 'https://etherscan.io/address/0xabc'
+ )
+ render(
+
+ )
+ const link = screen.getByText('View NFT Registry')
+ expect(link.tagName).toBe('A')
+ expect(link).toHaveAttribute('href', 'https://etherscan.io/address/0xabc')
+ expect(link).toHaveAttribute('target', '_blank')
+ expect(link).toHaveAttribute('rel', 'noopener noreferrer')
+ })
+
+ it('calls onViewNftRegistry when clicking the plain-div NFT link', () => {
+ const onViewNftRegistry = vi.fn()
+ vi.mocked(makeExplorerAddressURL).mockReturnValue(undefined)
+ render(
+
+ )
+ fireEvent.click(screen.getByText('View NFT Registry'))
+ expect(onViewNftRegistry).toHaveBeenCalledTimes(1)
+ })
+
+ it('calls onViewEndorsementChain when clicking View Endorsement Chain', () => {
+ const onViewEndorsementChain = vi.fn()
+ render(
+
+ )
+ fireEvent.click(screen.getByText('View Endorsement Chain'))
+ expect(onViewEndorsementChain).toHaveBeenCalledTimes(1)
+ })
+ })
+
+ // ββ Divider ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('divider', () => {
+ it('does not render the divider when isTransferable is false', () => {
+ const { container } = render(
+
+ )
+ expect(container.querySelector('.vr-divider')).not.toBeInTheDocument()
+ })
+
+ it('renders the divider when isTransferable is true', () => {
+ const { container } = render(
+
+ )
+ expect(container.querySelector('.vr-divider')).toBeInTheDocument()
+ })
+ })
+
+ // ββ Owner / Holder section βββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('Owner / Holder section', () => {
+ it('does not render Owner/Holder when isTransferable is false', () => {
+ render( )
+ expect(screen.queryByText('Owner:')).not.toBeInTheDocument()
+ expect(screen.queryByText('Holder:')).not.toBeInTheDocument()
+ })
+
+ it('renders Owner and Holder labels when isTransferable is true', () => {
+ render( )
+ expect(screen.getByText('Owner:')).toBeInTheDocument()
+ expect(screen.getByText('Holder:')).toBeInTheDocument()
+ })
+
+ it('shows fallback "Organisation A" for owner and holder when not provided', () => {
+ render( )
+ expect(screen.getAllByText('Organisation A')).toHaveLength(2)
+ })
+
+ it('shows the provided owner name and address', () => {
+ render(
+
+ )
+ expect(screen.getByText('Acme Corp')).toBeInTheDocument()
+ expect(screen.getByText('0x1234abcd')).toBeInTheDocument()
+ })
+
+ it('shows the provided holder name and address', () => {
+ render(
+
+ )
+ expect(screen.getByText('Bob Ltd')).toBeInTheDocument()
+ expect(screen.getByText('0x5678ef')).toBeInTheDocument()
+ })
+ })
+
+ // ββ Connect Wallet footer ββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('Connect Wallet footer', () => {
+ it('does not render the Connect Wallet button when isTransferable is false', () => {
+ render( )
+ expect(
+ screen.queryByRole('button', { name: /connect wallet/i })
+ ).not.toBeInTheDocument()
+ })
+
+ it('renders the Connect Wallet button when isTransferable is true', () => {
+ render( )
+ expect(
+ screen.getByRole('button', { name: /connect wallet/i })
+ ).toBeInTheDocument()
+ })
+
+ it('calls onConnectWallet when Connect Wallet is clicked', () => {
+ const onConnectWallet = vi.fn()
+ render(
+
+ )
+ fireEvent.click(screen.getByRole('button', { name: /connect wallet/i }))
+ expect(onConnectWallet).toHaveBeenCalledTimes(1)
+ })
+ })
+})
diff --git a/src/components/home/VerifySection/VerifyResult.tsx b/src/components/home/VerifySection/VerifyResult.tsx
new file mode 100644
index 0000000..026b2d6
--- /dev/null
+++ b/src/components/home/VerifySection/VerifyResult.tsx
@@ -0,0 +1,312 @@
+import React, { useState } from 'react'
+import NetworkTooltip from './NetworkTooltip'
+import DocumentRenderer from './DocumentRenderer'
+import { makeExplorerAddressURL } from './useVerify'
+import { CheckCircle, CrossCircle } from '../../common/Icons'
+
+interface VerifyResultProps {
+ fileName: string
+ networkName?: string
+ chainId?: string
+ issuer?: string
+ isTransferable?: boolean
+ tokenRegistryAddress?: string
+ tags?: string[]
+ owner?: { name?: string; address?: string }
+ holder?: { name?: string; address?: string }
+ rawDocument?: unknown
+ getGroupStatus: (_type: string) => 'VALID' | 'INVALID'
+ onReset: () => void
+ onViewNftRegistry?: () => void
+ onViewEndorsementChain?: () => void
+ onConnectWallet?: () => void
+}
+
+const VERIFICATION_CHECKS = [
+ { type: 'DOCUMENT_STATUS', label: 'Document has been issued' },
+ { type: 'ISSUER_IDENTITY', label: "Document's issuer has been identified" },
+ { type: 'DOCUMENT_INTEGRITY', label: 'Document has not been tampered with' },
+]
+
+const VerifyResult: React.FC = ({
+ fileName,
+ networkName,
+ chainId,
+ issuer,
+ isTransferable,
+ tokenRegistryAddress,
+ tags,
+ owner,
+ holder,
+ rawDocument,
+ getGroupStatus,
+ onReset,
+ onViewNftRegistry,
+ onViewEndorsementChain,
+ onConnectWallet,
+}) => {
+ const showNftLinks = !!isTransferable
+
+ const [isTooltipVisible, setIsTooltipVisible] = useState(false)
+ const [tooltipPosition, setTooltipPosition] = useState({
+ top: 0,
+ left: 0,
+ width: 0,
+ })
+
+ const handleInfoMouseEnter = (e: React.MouseEvent) => {
+ const rect = e.currentTarget.getBoundingClientRect()
+ const tooltipWidth = 280
+ const padding = 8
+
+ // Calculate left position with boundary checking
+ let left = rect.left - tooltipWidth + rect.width
+
+ // If tooltip would go off left edge, add padding from left
+ if (left < padding) {
+ left = padding
+ }
+
+ // If tooltip would go off right edge, align to right with padding
+ if (left + tooltipWidth > window.innerWidth - padding) {
+ left = window.innerWidth - tooltipWidth - padding
+ }
+
+ setTooltipPosition({
+ top: rect.bottom + 8,
+ left,
+ width: tooltipWidth,
+ })
+ setIsTooltipVisible(true)
+ }
+
+ const handleInfoFocus = (e: React.FocusEvent) => {
+ const rect = e.currentTarget.getBoundingClientRect()
+ const tooltipWidth = 280
+ const padding = 8
+
+ let left = rect.left - tooltipWidth + rect.width
+
+ if (left < padding) {
+ left = padding
+ }
+
+ if (left + tooltipWidth > window.innerWidth - padding) {
+ left = window.innerWidth - tooltipWidth - padding
+ }
+
+ setTooltipPosition({
+ top: rect.bottom + 8,
+ left,
+ width: tooltipWidth,
+ })
+ setIsTooltipVisible(true)
+ }
+
+ return (
+
+ {/* ββ Network info card ββ */}
+ {networkName && (
+
+
Document verified on:
+
+
+
{networkName}
+
+
+
+
+
+
+
+
+
+
setIsTooltipVisible(false)}
+ onFocus={handleInfoFocus}
+ onBlur={() => setIsTooltipVisible(false)}
+ aria-label="Network info"
+ >
+
+
+
+
+
+
+ )}
+
+ {/* ββ Main result card ββ */}
+
+ {/* Header */}
+
+
+ Upload New File
+
+
+
+ {/* Body: 3 columns */}
+
+ {/* Left: Issued by + tags */}
+
+
+ Issued by:
+ {issuer || fileName}
+
+ {tags && tags.length > 0 && (
+
+ {tags.map(tag => (
+
+ {tag}
+
+ ))}
+
+ )}
+
+
+ {/* Middle: Verification checks */}
+
+
+ {VERIFICATION_CHECKS.map(({ type, label }) => {
+ const status = getGroupStatus(type)
+ return (
+
+ {status === 'VALID' ? : }
+ {label}
+
+ )
+ })}
+
+
+
+ {/* Right: NFT links */}
+ {showNftLinks && (
+
+
+ {(() => {
+ const explorerUrl =
+ tokenRegistryAddress && chainId
+ ? makeExplorerAddressURL(tokenRegistryAddress, chainId)
+ : undefined
+ return explorerUrl ? (
+
+ View NFT Registry
+
+ ) : (
+
+ View NFT Registry
+
+ )
+ })()}
+
+ View Endorsement Chain
+
+
+
+ )}
+
+
+ {/* Divider */}
+ {showNftLinks &&
}
+
+ {/* Owner + Holder */}
+ {showNftLinks && (
+
+
+ Owner:
+
+ {owner?.name ?? 'Organisation A'}
+
+
+ {owner?.address ?? '0x28F7aB32C521D13F2E6980d072Ca7CA493020145'}
+
+
+
+ Holder:
+
+ {holder?.name ?? 'Organisation A'}
+
+
+ {holder?.address ??
+ '0x28F7aB32C521D13F2E6980d072Ca7CA493020145'}
+
+
+
+
+ )}
+
+ {/* Footer: Connect Wallet */}
+ {showNftLinks && (
+
+
+ Connect Wallet
+
+
+ )}
+
+
+ {/* ββ Template Renderer ββ */}
+ {rawDocument ? (
+
+ ) : null}
+
+ {/* Tooltip */}
+
+
+ )
+}
+
+export default VerifyResult
diff --git a/src/components/home/VerifySection/VerifySection.test.tsx b/src/components/home/VerifySection/VerifySection.test.tsx
new file mode 100644
index 0000000..09e887a
--- /dev/null
+++ b/src/components/home/VerifySection/VerifySection.test.tsx
@@ -0,0 +1,228 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { render, screen, fireEvent } from '@testing-library/react'
+import VerifySection from './VerifySection'
+import type { UseVerifyReturn } from './useVerify'
+
+// βββ Mocks ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+const mockNavigate = vi.fn()
+
+vi.mock('react-router-dom', async () => {
+ const actual =
+ await vi.importActual('react-router-dom')
+ return { ...actual, useNavigate: () => mockNavigate }
+})
+
+// Mock NetworkModal to avoid VITE_NETWORK_TYPE env dependency
+vi.mock('./NetworkModal', () => ({
+ default: ({ fileName }: { fileName: string }) => (
+ {fileName}
+ ),
+}))
+
+vi.mock('./useVerify', () => ({ useVerify: vi.fn() }))
+
+import { useVerify } from './useVerify'
+
+// βββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+const defaultHook: UseVerifyReturn = {
+ verifyStatus: 'idle',
+ fileName: '',
+ errorMessage: '',
+ dragActive: false,
+ isTransferable: false,
+ tokenRegistryVersion: null,
+ tags: [],
+ getGroupStatus: vi.fn().mockReturnValue('VALID' as const),
+ handleDrag: vi.fn(),
+ handleDrop: vi.fn(),
+ handleFileInput: vi.fn(),
+ handleReset: vi.fn(),
+ handleNetworkConfirm: vi.fn(),
+ handleNetworkCancel: vi.fn(),
+}
+
+const setStatus = (overrides: Partial) => {
+ vi.mocked(useVerify).mockReturnValue({ ...defaultHook, ...overrides })
+}
+
+// βββ Tests ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+describe('VerifySection', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ setStatus({})
+ })
+
+ // ββ Idle βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('idle state', () => {
+ it('renders the dropzone text', () => {
+ render( )
+ expect(
+ screen.getByText(/Drop TrustVC files here to verify/i)
+ ).toBeInTheDocument()
+ })
+
+ it('renders the Browse Files button', () => {
+ render( )
+ expect(screen.getByText(/Browse Files/i)).toBeInTheDocument()
+ })
+
+ it('does not render the spinner or result', () => {
+ render( )
+ expect(screen.queryByText(/Verifying/i)).not.toBeInTheDocument()
+ expect(screen.queryByText('Document Verified')).not.toBeInTheDocument()
+ })
+ })
+
+ // ββ Verifying ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('verifying state', () => {
+ it('shows spinner with the fileName', () => {
+ setStatus({ verifyStatus: 'verifying', fileName: 'doc.tt' })
+ render( )
+ expect(screen.getByText('Verifying doc.tt...')).toBeInTheDocument()
+ })
+
+ it('does not show the dropzone', () => {
+ setStatus({ verifyStatus: 'verifying', fileName: 'doc.tt' })
+ render( )
+ expect(
+ screen.queryByText(/Drop TrustVC files here to verify/i)
+ ).not.toBeInTheDocument()
+ })
+ })
+
+ // ββ Valid ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('valid state', () => {
+ it('renders VerifyResult with the fileName', () => {
+ setStatus({ verifyStatus: 'valid', fileName: 'valid-doc.tt' })
+ render( )
+ expect(
+ screen.getByRole('button', { name: /upload new file/i })
+ ).toBeInTheDocument()
+ expect(screen.getByText('valid-doc.tt')).toBeInTheDocument()
+ })
+
+ it('does not render VerifyError', () => {
+ setStatus({ verifyStatus: 'valid' })
+ render( )
+ expect(screen.queryByText('Try again')).not.toBeInTheDocument()
+ })
+ })
+
+ // ββ Invalid ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('invalid state', () => {
+ it('renders VerifyError instead of VerifyResult', () => {
+ setStatus({
+ verifyStatus: 'invalid',
+ errorMessage: 'Verification Failed',
+ })
+ render( )
+ expect(screen.queryByText('Document Verified')).not.toBeInTheDocument()
+ expect(screen.getByText('Try again')).toBeInTheDocument()
+ })
+
+ it('shows the errorMessage from the hook', () => {
+ setStatus({
+ verifyStatus: 'invalid',
+ errorMessage: 'Verification Failed',
+ })
+ render( )
+ expect(screen.getByText('Verification Failed')).toBeInTheDocument()
+ })
+
+ it('falls back to "Verification Failed" when errorMessage is empty', () => {
+ setStatus({ verifyStatus: 'invalid', errorMessage: '' })
+ render( )
+ expect(screen.getByText('Verification Failed')).toBeInTheDocument()
+ })
+ })
+
+ // ββ Error ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('error state', () => {
+ it('renders VerifyError with the errorMessage', () => {
+ setStatus({
+ verifyStatus: 'error',
+ errorMessage:
+ 'Invalid file format. Please upload a valid TrustVC document.',
+ })
+ render( )
+ expect(
+ screen.getByText(
+ 'Invalid file format. Please upload a valid TrustVC document.'
+ )
+ ).toBeInTheDocument()
+ expect(screen.getByText('Try again')).toBeInTheDocument()
+ })
+
+ it('falls back to "Verification Failed" when errorMessage is empty', () => {
+ setStatus({ verifyStatus: 'error', errorMessage: '' })
+ render( )
+ expect(screen.getByText('Verification Failed')).toBeInTheDocument()
+ })
+ })
+
+ // ββ Network select βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('network-select state', () => {
+ it('renders the dropzone and the NetworkModal', () => {
+ setStatus({ verifyStatus: 'network-select', fileName: 'pending.tt' })
+ render( )
+ expect(
+ screen.getByText(/Drop TrustVC files here to verify/i)
+ ).toBeInTheDocument()
+ expect(screen.getByTestId('network-modal')).toBeInTheDocument()
+ })
+
+ it('passes the fileName to NetworkModal', () => {
+ setStatus({ verifyStatus: 'network-select', fileName: 'pending.tt' })
+ render( )
+ expect(screen.getByTestId('network-modal').textContent).toBe('pending.tt')
+ })
+ })
+
+ // ββ Dark mode ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('dark mode', () => {
+ it('applies dark-mode class when isDarkMode is true', () => {
+ const { container } = render( )
+ expect(container.querySelector('.verify-section')).toHaveClass(
+ 'dark-mode'
+ )
+ })
+
+ it('does not apply dark-mode class when isDarkMode is false', () => {
+ const { container } = render( )
+ expect(container.querySelector('.verify-section')).not.toHaveClass(
+ 'dark-mode'
+ )
+ })
+ })
+
+ // ββ Demo section βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('demo section', () => {
+ it('renders the demo heading and description', () => {
+ render( )
+ expect(screen.getByText(/Try our demo document!/i)).toBeInTheDocument()
+ expect(
+ screen.getByText(/Experience the interoperability of our documents/i)
+ ).toBeInTheDocument()
+ })
+
+ it('navigates to root when Visit Document Gallery is clicked', () => {
+ render( )
+ const ctaButton = screen
+ .getByText(/Visit Document Gallery/i)
+ .closest('.cta-button')
+ fireEvent.click(ctaButton as HTMLElement)
+ expect(mockNavigate).toHaveBeenCalledWith('/')
+ })
+ })
+})
diff --git a/src/components/home/VerifySection/VerifySection.tsx b/src/components/home/VerifySection/VerifySection.tsx
new file mode 100644
index 0000000..aaef0a2
--- /dev/null
+++ b/src/components/home/VerifySection/VerifySection.tsx
@@ -0,0 +1,219 @@
+import React from 'react'
+import { useNavigate } from 'react-router-dom'
+import { useVerify } from './useVerify'
+import NetworkModal from './NetworkModal'
+import VerifyResult from './VerifyResult'
+import VerifyError from './VerifyError'
+import PrimaryButton from '../../common/PrimaryButton'
+import EndorsementChain from '../EndorsementChain'
+import { useEndorsementChain } from '../EndorsementChain/useEndorsementChain'
+import Spinner from '../../common/Spinner'
+
+interface VerifySectionProps {
+ isDarkMode: boolean
+}
+
+const CHAIN_NAMES: Record = {
+ '1': 'Ethereum',
+ '137': 'Polygon',
+ '50': 'XDC Network',
+ '101010': 'Stability (Beta)',
+ '1338': 'Astron',
+ '11155111': 'Sepolia',
+ '80002': 'Polygon Amoy',
+ '51': 'Apothem',
+ '20180427': 'Stability Testnet (Beta)',
+ '21002': 'Astron Testnet',
+}
+
+const VerifySection: React.FC = ({ isDarkMode }) => {
+ const {
+ verifyStatus,
+ fileName,
+ errorMessage,
+ dragActive,
+ verifiedChainId,
+ issuerName,
+ isTransferable,
+ tokenRegistryAddress,
+ tokenRegistryVersion,
+ tags,
+ tokenId,
+ keyId,
+ rawDocument,
+ getGroupStatus,
+ handleDrag,
+ handleDrop,
+ handleFileInput,
+ handleReset,
+ handleNetworkConfirm,
+ handleNetworkCancel,
+ } = useVerify()
+
+ // Fetch endorsement chain data only when file is verified as valid and transferable
+ const isValidTransferable =
+ verifyStatus === 'valid' && isTransferable === true
+ const {
+ endorsementChain,
+ endorsementChainStatus,
+ showEndorsementChain,
+ handleShowEndorsementChain,
+ handleHideEndorsementChain,
+ } = useEndorsementChain({
+ tokenRegistryAddress: isValidTransferable
+ ? tokenRegistryAddress
+ : undefined,
+ tokenId: isValidTransferable ? tokenId : undefined,
+ verifiedChainId: isValidTransferable ? verifiedChainId : undefined,
+ keyId: isValidTransferable ? keyId : undefined,
+ })
+
+ const networkName = verifiedChainId
+ ? (CHAIN_NAMES[verifiedChainId] ?? `Chain ${verifiedChainId}`)
+ : undefined
+
+ const navigate = useNavigate()
+
+ const renderDropzone = () => (
+
+
+
+
Drop TrustVC files here to verify
+
+
+
+
+
+ }
+ >
+ Browse Files
+
+
+
+
+
+ Maximum 10 MB. Supported files include .tt, .oa, and .json.
+
+
+
+ )
+
+ const renderVerifying = () => (
+
+ )
+
+ return (
+
+
+ {showEndorsementChain && (
+
+ )}
+
+
+ {verifyStatus === 'idle' && renderDropzone()}
+ {verifyStatus === 'verifying' && renderVerifying()}
+ {verifyStatus === 'network-select' && renderDropzone()}
+ {verifyStatus === 'valid' && (
+
+ )}
+
+ {(verifyStatus === 'invalid' || verifyStatus === 'error') && (
+
+ )}
+ {verifyStatus === 'network-select' && (
+
+ )}
+
+
+
+
Try our demo document!
+
+
+
+ Experience the interoperability of our documents from the
+ documents gallery!
+
+
+
+
+
navigate('/')}
+ >
+
+
+
+
Visit Document Gallery
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default VerifySection
diff --git a/src/components/home/VerifySection/index.ts b/src/components/home/VerifySection/index.ts
new file mode 100644
index 0000000..4bace3f
--- /dev/null
+++ b/src/components/home/VerifySection/index.ts
@@ -0,0 +1 @@
+export { default } from './VerifySection'
diff --git a/src/components/home/VerifySection/useVerify.test.ts b/src/components/home/VerifySection/useVerify.test.ts
new file mode 100644
index 0000000..1a03243
--- /dev/null
+++ b/src/components/home/VerifySection/useVerify.test.ts
@@ -0,0 +1,700 @@
+import React from 'react'
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { renderHook, act, waitFor } from '@testing-library/react'
+import { useVerify, makeExplorerAddressURL } from './useVerify'
+
+// βββ Mocks ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+// Mock import.meta.env
+Object.defineProperty(import.meta, 'env', {
+ value: {
+ VITE_RPC_URL_1: 'https://eth-mainnet.example.com',
+ VITE_RPC_URL_137: 'https://polygon.example.com',
+ },
+ writable: true,
+})
+
+// Polyfill File.prototype.text for test environment
+if (!File.prototype.text) {
+ File.prototype.text = function () {
+ return new Promise(resolve => {
+ const reader = new FileReader()
+ reader.onload = () => resolve(reader.result as string)
+ reader.readAsText(this)
+ })
+ }
+}
+
+vi.mock('@trustvc/trustvc', () => ({
+ verifyDocument: vi.fn(),
+ getChainId: vi.fn(),
+ isTransferableRecord: vi.fn(),
+ isDocumentRevokable: vi.fn(),
+ SUPPORTED_CHAINS: {
+ '1': {
+ rpcUrl: 'https://eth-mainnet.example.com',
+ explorerUrl: 'https://etherscan.io',
+ },
+ '137': {
+ rpcUrl: 'https://polygon.example.com',
+ explorerUrl: 'https://polygonscan.com',
+ },
+ },
+ // Document type predicates used in getIssuerName / getDocumentTags
+ isWrappedV2Document: vi.fn().mockReturnValue(false),
+ isWrappedV3Document: vi.fn().mockReturnValue(false),
+ isRawV2Document: vi.fn().mockReturnValue(false),
+ isSignedWrappedV2Document: vi.fn().mockReturnValue(false),
+ isRawV3Document: vi.fn().mockReturnValue(false),
+ isSignedWrappedV3Document: vi.fn().mockReturnValue(false),
+ // Title escrow helpers used in detectTokenRegistryVersion
+ isTitleEscrowVersion: vi.fn().mockResolvedValue(false),
+ TitleEscrowInterface: { V4: 'V4', V5: 'V5' },
+ getTokenRegistryAddress: vi.fn().mockReturnValue(undefined),
+ getTokenId: vi.fn().mockReturnValue(undefined),
+ getDocumentData: vi.fn().mockReturnValue({ id: 'test-key-id' }),
+ // Namespace objects (only methods used in the source are stubbed)
+ utils: {},
+ v2: {},
+ v3: {},
+ vc: {
+ isSignedDocument: vi.fn().mockReturnValue(false),
+ isRawDocument: vi.fn().mockReturnValue(false),
+ isSignedDocumentV2_0: vi.fn().mockReturnValue(false),
+ },
+}))
+
+import {
+ verifyDocument,
+ getChainId,
+ isTransferableRecord,
+ isDocumentRevokable,
+} from '@trustvc/trustvc'
+
+// βββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+const makeFile = (content: object, name = 'doc.tt') =>
+ new File([JSON.stringify(content)], name, { type: 'application/json' })
+
+const makeDragEvent = (type: string, files: File[] = []) =>
+ ({
+ type,
+ preventDefault: vi.fn(),
+ stopPropagation: vi.fn(),
+ dataTransfer: { files },
+ }) as unknown as React.DragEvent
+
+const triggerFileInput = (
+ result: ReturnType<
+ typeof renderHook, unknown>
+ >['result'],
+ file: File
+) => {
+ result.current.handleFileInput({
+ target: { files: [file], value: '' },
+ } as unknown as React.ChangeEvent)
+}
+
+// βββ Tests ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+describe('useVerify', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ vi.spyOn(console, 'error').mockImplementation(() => {})
+ })
+
+ // ββ Initial state ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('initial state', () => {
+ it('starts idle with empty fields', () => {
+ const { result } = renderHook(() => useVerify())
+ expect(result.current.verifyStatus).toBe('idle')
+ expect(result.current.fileName).toBe('')
+ expect(result.current.errorMessage).toBe('')
+ expect(result.current.dragActive).toBe(false)
+ })
+ })
+
+ // ββ handleReset ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('handleReset', () => {
+ it('resets all state back to initial values', async () => {
+ vi.mocked(verifyDocument).mockResolvedValue([
+ {
+ name: 'OpenAttestationHash',
+ status: 'VALID',
+ type: 'DOCUMENT_INTEGRITY',
+ },
+ ])
+ vi.mocked(getChainId).mockReturnValue('1' as any)
+ vi.mocked(isTransferableRecord).mockReturnValue(false)
+ vi.mocked(isDocumentRevokable).mockReturnValue(false)
+
+ const { result } = renderHook(() => useVerify())
+
+ await act(async () => {
+ triggerFileInput(result, makeFile({ test: true }))
+ })
+ await waitFor(() => expect(result.current.verifyStatus).toBe('valid'))
+
+ act(() => {
+ result.current.handleReset()
+ })
+
+ expect(result.current.verifyStatus).toBe('idle')
+ expect(result.current.fileName).toBe('')
+ expect(result.current.errorMessage).toBe('')
+ expect(result.current.dragActive).toBe(false)
+ })
+ })
+
+ // ββ handleDrag βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('handleDrag', () => {
+ it('sets dragActive true on dragenter', () => {
+ const { result } = renderHook(() => useVerify())
+ act(() => {
+ result.current.handleDrag(makeDragEvent('dragenter'))
+ })
+ expect(result.current.dragActive).toBe(true)
+ })
+
+ it('sets dragActive true on dragover', () => {
+ const { result } = renderHook(() => useVerify())
+ act(() => {
+ result.current.handleDrag(makeDragEvent('dragover'))
+ })
+ expect(result.current.dragActive).toBe(true)
+ })
+
+ it('sets dragActive false on dragleave', () => {
+ const { result } = renderHook(() => useVerify())
+ act(() => {
+ result.current.handleDrag(makeDragEvent('dragenter'))
+ })
+ act(() => {
+ result.current.handleDrag(makeDragEvent('dragleave'))
+ })
+ expect(result.current.dragActive).toBe(false)
+ })
+
+ it('calls preventDefault and stopPropagation', () => {
+ const { result } = renderHook(() => useVerify())
+ const event = makeDragEvent('dragenter')
+ act(() => {
+ result.current.handleDrag(event)
+ })
+ expect(event.preventDefault).toHaveBeenCalled()
+ expect(event.stopPropagation).toHaveBeenCalled()
+ })
+ })
+
+ // ββ handleFileInput ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('handleFileInput', () => {
+ it('does nothing when no file is provided', () => {
+ const { result } = renderHook(() => useVerify())
+ act(() => {
+ result.current.handleFileInput({
+ target: { files: null, value: '' },
+ } as unknown as React.ChangeEvent)
+ })
+ expect(result.current.verifyStatus).toBe('idle')
+ })
+
+ it('sets fileName immediately', async () => {
+ vi.mocked(verifyDocument).mockResolvedValue([])
+ vi.mocked(getChainId).mockReturnValue('1' as any)
+ vi.mocked(isTransferableRecord).mockReturnValue(false)
+ vi.mocked(isDocumentRevokable).mockReturnValue(false)
+
+ const { result } = renderHook(() => useVerify())
+ await act(async () => {
+ triggerFileInput(result, makeFile({ test: true }, 'my-doc.tt'))
+ })
+ expect(result.current.fileName).toBe('my-doc.tt')
+ })
+
+ it('resolves to valid when all fragment groups are VALID', async () => {
+ vi.mocked(verifyDocument).mockResolvedValue([
+ {
+ name: 'OpenAttestationHash',
+ status: 'VALID',
+ type: 'DOCUMENT_INTEGRITY',
+ },
+ {
+ name: 'OpenAttestationEthereumDocumentStoreStatus',
+ status: 'VALID',
+ type: 'DOCUMENT_STATUS',
+ },
+ ])
+ vi.mocked(getChainId).mockReturnValue('1' as any)
+ vi.mocked(isTransferableRecord).mockReturnValue(false)
+ vi.mocked(isDocumentRevokable).mockReturnValue(false)
+
+ const { result } = renderHook(() => useVerify())
+ await act(async () => {
+ triggerFileInput(result, makeFile({ test: true }))
+ })
+ await waitFor(() => expect(result.current.verifyStatus).toBe('valid'))
+ })
+
+ it('resolves to invalid when any fragment group has INVALID status', async () => {
+ vi.mocked(verifyDocument).mockResolvedValue([
+ {
+ name: 'OpenAttestationHash',
+ status: 'VALID',
+ type: 'DOCUMENT_INTEGRITY',
+ },
+ {
+ name: 'OpenAttestationEthereumDocumentStoreStatus',
+ status: 'INVALID',
+ type: 'DOCUMENT_STATUS',
+ },
+ ])
+ vi.mocked(getChainId).mockReturnValue('1' as any)
+ vi.mocked(isTransferableRecord).mockReturnValue(false)
+ vi.mocked(isDocumentRevokable).mockReturnValue(false)
+
+ const { result } = renderHook(() => useVerify())
+ await act(async () => {
+ triggerFileInput(result, makeFile({ test: true }))
+ })
+ await waitFor(() => expect(result.current.verifyStatus).toBe('invalid'))
+ })
+
+ it('resolves to invalid when no fragments are returned', async () => {
+ vi.mocked(verifyDocument).mockResolvedValue([])
+ vi.mocked(getChainId).mockReturnValue('1' as any)
+ vi.mocked(isTransferableRecord).mockReturnValue(false)
+ vi.mocked(isDocumentRevokable).mockReturnValue(false)
+
+ const { result } = renderHook(() => useVerify())
+ await act(async () => {
+ triggerFileInput(result, makeFile({ test: true }))
+ })
+ await waitFor(() => expect(result.current.verifyStatus).toBe('invalid'))
+ })
+
+ it('sets error with SyntaxError message on invalid JSON', async () => {
+ const { result } = renderHook(() => useVerify())
+ const badFile = new File(['not { valid json'], 'bad.tt', {
+ type: 'text/plain',
+ })
+ await act(async () => {
+ triggerFileInput(result, badFile)
+ })
+ await waitFor(() => expect(result.current.verifyStatus).toBe('error'))
+ expect(result.current.errorMessage).toBe(
+ 'Invalid file format. Please upload a valid TrustVC document.'
+ )
+ })
+
+ it('sets error with the thrown Error message when verifyDocument rejects', async () => {
+ vi.mocked(verifyDocument).mockRejectedValue(new Error('RPC unavailable'))
+ vi.mocked(getChainId).mockReturnValue('1' as any)
+ vi.mocked(isTransferableRecord).mockReturnValue(false)
+ vi.mocked(isDocumentRevokable).mockReturnValue(false)
+
+ const { result } = renderHook(() => useVerify())
+ await act(async () => {
+ triggerFileInput(result, makeFile({ test: true }))
+ })
+ await waitFor(() => expect(result.current.verifyStatus).toBe('error'))
+ expect(result.current.errorMessage).toBe('RPC unavailable')
+ })
+
+ it('sets error with fallback message for non-Error throws', async () => {
+ vi.mocked(verifyDocument).mockRejectedValue('something weird')
+ vi.mocked(getChainId).mockReturnValue('1' as any)
+ vi.mocked(isTransferableRecord).mockReturnValue(false)
+ vi.mocked(isDocumentRevokable).mockReturnValue(false)
+
+ const { result } = renderHook(() => useVerify())
+ await act(async () => {
+ triggerFileInput(result, makeFile({ test: true }))
+ })
+ await waitFor(() => expect(result.current.verifyStatus).toBe('error'))
+ expect(result.current.errorMessage).toBe(
+ 'Verification failed. Please try again.'
+ )
+ })
+
+ it('transitions to network-select for a transferable record with no chainId', async () => {
+ vi.mocked(getChainId).mockReturnValue(null as any)
+ vi.mocked(isTransferableRecord).mockReturnValue(true)
+ vi.mocked(isDocumentRevokable).mockReturnValue(false)
+
+ const { result } = renderHook(() => useVerify())
+ await act(async () => {
+ triggerFileInput(result, makeFile({ test: true }, 'tr.tt'))
+ })
+ await waitFor(() =>
+ expect(result.current.verifyStatus).toBe('network-select')
+ )
+ expect(result.current.fileName).toBe('tr.tt')
+ })
+
+ it('transitions to network-select for a revokable document with no chainId', async () => {
+ vi.mocked(getChainId).mockReturnValue(null as any)
+ vi.mocked(isTransferableRecord).mockReturnValue(false)
+ vi.mocked(isDocumentRevokable).mockReturnValue(true)
+
+ const { result } = renderHook(() => useVerify())
+ await act(async () => {
+ triggerFileInput(result, makeFile({ test: true }))
+ })
+ await waitFor(() =>
+ expect(result.current.verifyStatus).toBe('network-select')
+ )
+ })
+
+ it('proceeds to verify (not network-select) for a plain doc with no chainId', async () => {
+ vi.mocked(getChainId).mockReturnValue(null as any)
+ vi.mocked(isTransferableRecord).mockReturnValue(false)
+ vi.mocked(isDocumentRevokable).mockReturnValue(false)
+ vi.mocked(verifyDocument).mockResolvedValue([
+ {
+ name: 'OpenAttestationHash',
+ status: 'VALID',
+ type: 'DOCUMENT_INTEGRITY',
+ },
+ ])
+
+ const { result } = renderHook(() => useVerify())
+ await act(async () => {
+ triggerFileInput(result, makeFile({ test: true }))
+ })
+ await waitFor(() => expect(result.current.verifyStatus).toBe('valid'))
+ })
+ })
+
+ // ββ handleDrop βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('handleDrop', () => {
+ it('clears dragActive and processes the dropped file', async () => {
+ vi.mocked(verifyDocument).mockResolvedValue([
+ {
+ name: 'OpenAttestationHash',
+ status: 'VALID',
+ type: 'DOCUMENT_INTEGRITY',
+ },
+ ])
+ vi.mocked(getChainId).mockReturnValue('1' as any)
+ vi.mocked(isTransferableRecord).mockReturnValue(false)
+ vi.mocked(isDocumentRevokable).mockReturnValue(false)
+
+ const { result } = renderHook(() => useVerify())
+ act(() => {
+ result.current.handleDrag(makeDragEvent('dragenter'))
+ })
+ expect(result.current.dragActive).toBe(true)
+
+ const file = makeFile({ test: true })
+ await act(async () => {
+ result.current.handleDrop(makeDragEvent('drop', [file]))
+ })
+
+ expect(result.current.dragActive).toBe(false)
+ await waitFor(() => expect(result.current.verifyStatus).toBe('valid'))
+ })
+
+ it('does nothing when drop contains no files', () => {
+ const { result } = renderHook(() => useVerify())
+ act(() => {
+ result.current.handleDrop(makeDragEvent('drop', []))
+ })
+ expect(result.current.verifyStatus).toBe('idle')
+ })
+ })
+
+ // ββ handleNetworkCancel ββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('handleNetworkCancel', () => {
+ it('returns to idle and clears fileName', async () => {
+ vi.mocked(getChainId).mockReturnValue(null as any)
+ vi.mocked(isTransferableRecord).mockReturnValue(true)
+ vi.mocked(isDocumentRevokable).mockReturnValue(false)
+
+ const { result } = renderHook(() => useVerify())
+ await act(async () => {
+ triggerFileInput(result, makeFile({ test: true }, 'pending.tt'))
+ })
+ await waitFor(() =>
+ expect(result.current.verifyStatus).toBe('network-select')
+ )
+
+ act(() => {
+ result.current.handleNetworkCancel()
+ })
+
+ expect(result.current.verifyStatus).toBe('idle')
+ expect(result.current.fileName).toBe('')
+ })
+ })
+
+ // ββ handleNetworkConfirm βββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('handleNetworkConfirm', () => {
+ it('does nothing when called with no pending document', async () => {
+ const { result } = renderHook(() => useVerify())
+ await act(async () => {
+ result.current.handleNetworkConfirm('1')
+ })
+ expect(result.current.verifyStatus).toBe('idle')
+ expect(verifyDocument).not.toHaveBeenCalled()
+ })
+
+ it('verifies with the selected chainId and resolves to valid', async () => {
+ vi.mocked(getChainId).mockReturnValue(null as any)
+ vi.mocked(isTransferableRecord).mockReturnValue(true)
+ vi.mocked(isDocumentRevokable).mockReturnValue(false)
+ vi.mocked(verifyDocument).mockResolvedValue([
+ {
+ name: 'OpenAttestationHash',
+ status: 'VALID',
+ type: 'DOCUMENT_INTEGRITY',
+ },
+ ])
+
+ const { result } = renderHook(() => useVerify())
+ await act(async () => {
+ triggerFileInput(result, makeFile({ test: true }))
+ })
+ await waitFor(() =>
+ expect(result.current.verifyStatus).toBe('network-select')
+ )
+
+ await act(async () => {
+ result.current.handleNetworkConfirm('137')
+ })
+ await waitFor(() => expect(result.current.verifyStatus).toBe('valid'))
+ expect(verifyDocument).toHaveBeenCalledTimes(1)
+ })
+
+ it('resolves to invalid when verification returns invalid fragments', async () => {
+ vi.mocked(getChainId).mockReturnValue(null as any)
+ vi.mocked(isTransferableRecord).mockReturnValue(true)
+ vi.mocked(isDocumentRevokable).mockReturnValue(false)
+ vi.mocked(verifyDocument).mockResolvedValue([
+ {
+ name: 'OpenAttestationHash',
+ status: 'INVALID',
+ type: 'DOCUMENT_INTEGRITY',
+ },
+ ])
+
+ const { result } = renderHook(() => useVerify())
+ await act(async () => {
+ triggerFileInput(result, makeFile({ test: true }))
+ })
+ await waitFor(() =>
+ expect(result.current.verifyStatus).toBe('network-select')
+ )
+
+ await act(async () => {
+ result.current.handleNetworkConfirm('1')
+ })
+ await waitFor(() => expect(result.current.verifyStatus).toBe('invalid'))
+ })
+
+ it('sets error state when verification throws', async () => {
+ vi.mocked(getChainId).mockReturnValue(null as any)
+ vi.mocked(isTransferableRecord).mockReturnValue(true)
+ vi.mocked(isDocumentRevokable).mockReturnValue(false)
+ vi.mocked(verifyDocument).mockRejectedValue(new Error('Network timeout'))
+
+ const { result } = renderHook(() => useVerify())
+ await act(async () => {
+ triggerFileInput(result, makeFile({ test: true }))
+ })
+ await waitFor(() =>
+ expect(result.current.verifyStatus).toBe('network-select')
+ )
+
+ await act(async () => {
+ result.current.handleNetworkConfirm('1')
+ })
+ await waitFor(() => expect(result.current.verifyStatus).toBe('error'))
+ expect(result.current.errorMessage).toBe('Network timeout')
+ })
+ })
+
+ // ββ getGroupStatus βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('getGroupStatus', () => {
+ const setup = async (
+ fragments: {
+ name: string
+ status: 'VALID' | 'INVALID' | 'SKIPPED'
+ type: 'DOCUMENT_INTEGRITY' | 'DOCUMENT_STATUS' | 'ISSUER_IDENTITY'
+ }[]
+ ) => {
+ vi.mocked(verifyDocument).mockResolvedValue(fragments)
+ vi.mocked(getChainId).mockReturnValue('1' as any)
+ vi.mocked(isTransferableRecord).mockReturnValue(false)
+ vi.mocked(isDocumentRevokable).mockReturnValue(false)
+
+ const { result } = renderHook(() => useVerify())
+ await act(async () => {
+ triggerFileInput(result, makeFile({ test: true }))
+ })
+ await waitFor(() =>
+ expect(['valid', 'invalid']).toContain(result.current.verifyStatus)
+ )
+ return result
+ }
+
+ it('returns VALID when all fragments of a type are VALID', async () => {
+ const result = await setup([
+ { name: 'a', status: 'VALID', type: 'DOCUMENT_INTEGRITY' },
+ { name: 'b', status: 'VALID', type: 'DOCUMENT_INTEGRITY' },
+ ])
+ expect(result.current.getGroupStatus('DOCUMENT_INTEGRITY')).toBe('VALID')
+ })
+
+ it('returns INVALID when any fragment of a type is INVALID', async () => {
+ const result = await setup([
+ { name: 'a', status: 'VALID', type: 'DOCUMENT_INTEGRITY' },
+ { name: 'b', status: 'INVALID', type: 'DOCUMENT_INTEGRITY' },
+ ])
+ expect(result.current.getGroupStatus('DOCUMENT_INTEGRITY')).toBe(
+ 'INVALID'
+ )
+ })
+
+ it('returns INVALID when all fragments of a type are SKIPPED', async () => {
+ const result = await setup([
+ { name: 'a', status: 'SKIPPED', type: 'DOCUMENT_INTEGRITY' },
+ { name: 'b', status: 'SKIPPED', type: 'DOCUMENT_INTEGRITY' },
+ ])
+ expect(result.current.getGroupStatus('DOCUMENT_INTEGRITY')).toBe(
+ 'INVALID'
+ )
+ })
+
+ it('returns INVALID for an unknown type with no matching fragments', async () => {
+ const result = await setup([
+ { name: 'a', status: 'VALID', type: 'DOCUMENT_INTEGRITY' },
+ ])
+ expect(result.current.getGroupStatus('UNKNOWN_TYPE')).toBe('INVALID')
+ })
+
+ it('returns INVALID before any file has been verified', () => {
+ const { result } = renderHook(() => useVerify())
+ expect(result.current.getGroupStatus('DOCUMENT_INTEGRITY')).toBe(
+ 'INVALID'
+ )
+ })
+ })
+
+ // ββ New state values (issuerName, isTransferable, tags) βββββββββββββββββββ
+
+ describe('new state values', () => {
+ it('starts with empty issuerName', () => {
+ const { result } = renderHook(() => useVerify())
+ expect(result.current.issuerName).toBe('')
+ })
+
+ it('starts with isTransferable false', () => {
+ const { result } = renderHook(() => useVerify())
+ expect(result.current.isTransferable).toBe(false)
+ })
+
+ it('starts with an empty tags array', () => {
+ const { result } = renderHook(() => useVerify())
+ expect(result.current.tags).toEqual([])
+ })
+
+ it('sets isTransferable to true when document is a transferable record', async () => {
+ vi.mocked(verifyDocument).mockResolvedValue([
+ {
+ name: 'OpenAttestationHash',
+ status: 'VALID',
+ type: 'DOCUMENT_INTEGRITY',
+ },
+ ])
+ vi.mocked(getChainId).mockReturnValue('1' as any)
+ vi.mocked(isTransferableRecord).mockReturnValue(true)
+ vi.mocked(isDocumentRevokable).mockReturnValue(false)
+
+ const { result } = renderHook(() => useVerify())
+ await act(async () => {
+ triggerFileInput(result, makeFile({ test: true }))
+ })
+ await waitFor(() =>
+ expect(['valid', 'invalid']).toContain(result.current.verifyStatus)
+ )
+ expect(result.current.isTransferable).toBe(true)
+ })
+
+ it('sets isTransferable to false when document is not transferable', async () => {
+ vi.mocked(verifyDocument).mockResolvedValue([
+ {
+ name: 'OpenAttestationHash',
+ status: 'VALID',
+ type: 'DOCUMENT_INTEGRITY',
+ },
+ ])
+ vi.mocked(getChainId).mockReturnValue('1' as any)
+ vi.mocked(isTransferableRecord).mockReturnValue(false)
+ vi.mocked(isDocumentRevokable).mockReturnValue(false)
+
+ const { result } = renderHook(() => useVerify())
+ await act(async () => {
+ triggerFileInput(result, makeFile({ test: true }))
+ })
+ await waitFor(() =>
+ expect(['valid', 'invalid']).toContain(result.current.verifyStatus)
+ )
+ expect(result.current.isTransferable).toBe(false)
+ })
+
+ it('resets issuerName, isTransferable, and tags on handleReset', async () => {
+ vi.mocked(verifyDocument).mockResolvedValue([
+ {
+ name: 'OpenAttestationHash',
+ status: 'VALID',
+ type: 'DOCUMENT_INTEGRITY',
+ },
+ ])
+ vi.mocked(getChainId).mockReturnValue('1' as any)
+ vi.mocked(isTransferableRecord).mockReturnValue(false)
+ vi.mocked(isDocumentRevokable).mockReturnValue(false)
+
+ const { result } = renderHook(() => useVerify())
+ await act(async () => {
+ triggerFileInput(result, makeFile({ test: true }))
+ })
+ await waitFor(() => expect(result.current.verifyStatus).toBe('valid'))
+
+ act(() => {
+ result.current.handleReset()
+ })
+
+ expect(result.current.issuerName).toBe('')
+ expect(result.current.isTransferable).toBe(false)
+ expect(result.current.tags).toEqual([])
+ })
+ })
+
+ // ββ makeExplorerAddressURL βββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ describe('makeExplorerAddressURL', () => {
+ it('returns undefined for an unknown chainId', () => {
+ expect(makeExplorerAddressURL('0xabc', '9999')).toBeUndefined()
+ })
+
+ it('builds the correct explorer URL for a known chain with an explorerUrl', () => {
+ // chainId '1' mock has explorerUrl: 'https://etherscan.io'
+ const url = makeExplorerAddressURL('0xdeadbeef', '1')
+ expect(url).toBe('https://etherscan.io/address/0xdeadbeef')
+ })
+
+ it('builds the correct explorer URL for another known chain', () => {
+ // chainId '137' mock has explorerUrl: 'https://polygonscan.com'
+ const url = makeExplorerAddressURL('0xcafe', '137')
+ expect(url).toBe('https://polygonscan.com/address/0xcafe')
+ })
+ })
+})
diff --git a/src/components/home/VerifySection/useVerify.ts b/src/components/home/VerifySection/useVerify.ts
new file mode 100644
index 0000000..33a1def
--- /dev/null
+++ b/src/components/home/VerifySection/useVerify.ts
@@ -0,0 +1,500 @@
+import React, { useState } from 'react'
+import {
+ verifyDocument,
+ getChainId,
+ SUPPORTED_CHAINS,
+ isTransferableRecord,
+ isDocumentRevokable,
+ vc,
+ isWrappedV2Document,
+ isWrappedV3Document,
+ isRawV2Document,
+ isSignedWrappedV2Document,
+ isRawV3Document,
+ isSignedWrappedV3Document,
+ isTitleEscrowVersion,
+ TitleEscrowInterface,
+ getTokenRegistryAddress,
+ getTokenId,
+ getDocumentData as getDocumentDataFromWrappedDocument,
+} from '@trustvc/trustvc'
+import { toErrorMessage } from '../../../utils/helper'
+
+export type VerifyStatus =
+ | 'idle'
+ | 'verifying'
+ | 'valid'
+ | 'invalid'
+ | 'error'
+ | 'network-select'
+
+export type VerificationFragmentType =
+ | 'DOCUMENT_INTEGRITY'
+ | 'DOCUMENT_STATUS'
+ | 'ISSUER_IDENTITY'
+
+export interface VerificationFragment {
+ name: string
+ status: 'VALID' | 'INVALID' | 'SKIPPED'
+ type: VerificationFragmentType
+ reason?: unknown
+ data?: any
+}
+
+export type TokenRegistryVersion = 'V4' | 'V5' | null
+
+export interface UseVerifyReturn {
+ verifyStatus: VerifyStatus
+ fileName: string
+ errorMessage: string
+ dragActive: boolean
+ verifiedChainId?: string
+ issuerName?: string
+ isTransferable: boolean
+ tokenRegistryVersion: TokenRegistryVersion
+ tokenRegistryAddress?: string
+ tags: string[]
+ tokenId?: string
+ keyId?: string
+ rawDocument?: unknown
+ getGroupStatus: (_type: string) => 'VALID' | 'INVALID'
+ handleDrag: (_e: React.DragEvent) => void
+ handleDrop: (_e: React.DragEvent) => void
+ handleFileInput: (_e: React.ChangeEvent) => void
+ handleReset: () => void
+ handleNetworkConfirm: (_chainId: string) => void
+ handleNetworkCancel: () => void
+}
+
+const computeGroupStatus = (
+ frags: VerificationFragment[],
+ type: string
+): 'VALID' | 'INVALID' => {
+ const group = frags.filter(f => f.type === type)
+ if (group.length === 0) return 'INVALID'
+ if (group.some(f => f.status === 'INVALID')) return 'INVALID'
+ if (group.some(f => f.status === 'VALID')) return 'VALID'
+ return 'INVALID'
+}
+
+const getV2FormattedDomainNames = (
+ verificationStatus: VerificationFragment[]
+): string => {
+ const joinIssuers = (issuers: string[] | undefined): string => {
+ if (!issuers) return 'Unknown'
+ const issuerNames = issuers.join(', ')
+ return issuerNames?.replace(/,(?=[^,]*$)/, ' and') // regex to find last comma, replace with and
+ }
+
+ const formatIdentifier = (
+ fragment: VerificationFragment
+ ): string | undefined => {
+ switch (fragment.name) {
+ case 'OpencertsRegistryVerifier': {
+ const issuerNames = Array.isArray(fragment?.data)
+ ? (fragment.data as Array<{ name?: string }>).reduce(
+ (acc, issuer) => {
+ const name = issuer?.name
+ if (typeof name === 'string' && name.trim().length > 0) {
+ acc.push(name)
+ }
+ return acc
+ },
+ []
+ )
+ : undefined
+ return joinIssuers(issuerNames)
+ }
+ case 'OpenAttestationDnsTxtIdentityProof':
+ case 'OpenAttestationDnsDidIdentityProof':
+ return joinIssuers(
+ fragment.data?.map((issuer: any) => issuer.location.toUpperCase())
+ )
+ case 'OpenAttestationDidIdentityProof':
+ return joinIssuers(
+ fragment.data?.map((issuer: any) => issuer.did.toUpperCase())
+ )
+ default:
+ return 'Unknown'
+ }
+ }
+
+ const identityProofFragment = verificationStatus
+ .filter(f => f.type === 'ISSUER_IDENTITY')
+ .find(f => f.status === 'VALID')
+
+ const dataFragment = identityProofFragment?.data
+ const fragmentValidity =
+ dataFragment?.length > 0 &&
+ dataFragment?.every(
+ (issuer: { status: string; verified: boolean }) =>
+ issuer.status === 'VALID' || issuer.verified === true
+ )
+
+ return fragmentValidity && identityProofFragment
+ ? formatIdentifier(identityProofFragment) || 'Unknown'
+ : 'Unknown'
+}
+
+const getV3IdentityVerificationText = (document: any): string => {
+ return (
+ document.openAttestationMetadata?.identityProof?.identifier?.toUpperCase() ||
+ 'Unknown'
+ )
+}
+
+const getW3CIdentityVerificationText = (document: any): string => {
+ const issuer =
+ typeof document?.issuer === 'string'
+ ? document?.issuer
+ : document?.issuer?.id
+ return issuer?.toUpperCase() || 'Unknown'
+}
+
+const getIssuerName = (
+ document: any,
+ verificationStatus: VerificationFragment[]
+): string => {
+ if (!document) return 'Unknown'
+
+ if (isWrappedV2Document(document)) {
+ return getV2FormattedDomainNames(verificationStatus)
+ } else if (isWrappedV3Document(document)) {
+ return getV3IdentityVerificationText(document)
+ } else if (vc.isSignedDocument(document)) {
+ return getW3CIdentityVerificationText(document)
+ }
+
+ return 'Unknown'
+}
+
+const detectTokenRegistryVersion = async (
+ document: any,
+ provider: any
+): Promise => {
+ if (!document || !isTransferableRecord(document) || !provider) {
+ return null
+ }
+
+ try {
+ // Extract registry address and token ID from document using trustvc utilities
+ const registryAddress = getTokenRegistryAddress(document)
+ const tokenId = getTokenId(document)
+
+ if (!registryAddress) {
+ return null
+ }
+
+ // Check if it's Title Escrow V4
+ const isTitleEscrowV4 = await isTitleEscrowVersion({
+ tokenRegistryAddress: registryAddress,
+ tokenId,
+ versionInterface: TitleEscrowInterface.V4,
+ provider,
+ })
+
+ if (isTitleEscrowV4) {
+ return 'V4'
+ }
+
+ // If not V4, assume V5 for transferable documents
+ return 'V5'
+ } catch (error) {
+ console.error('Error detecting token registry version:', error)
+ return null
+ }
+}
+
+const getDocumentTags = (
+ document: any,
+ tokenRegistryVersion: TokenRegistryVersion
+): string[] => {
+ if (!document) return []
+
+ const tags: string[] = []
+
+ // Check if transferable - adds both Transferable and Negotiable tags
+ const isTransferableDocument = isTransferableRecord(document)
+ if (isTransferableDocument) {
+ tags.push('Transferable')
+ tags.push('Negotiable')
+ }
+
+ // Determine document schema type
+ const isOAV2 =
+ isRawV2Document(document) ||
+ isSignedWrappedV2Document(document) ||
+ isWrappedV2Document(document)
+ const isOAV3 =
+ isRawV3Document(document) ||
+ isSignedWrappedV3Document(document) ||
+ isWrappedV3Document(document)
+ const isW3CVC = vc.isSignedDocument(document) || vc.isRawDocument(document)
+ const isW3CVCVersion2_0 = isW3CVC ? vc.isSignedDocumentV2_0(document) : null
+
+ // Add document schema tag
+ if (isOAV2 || isOAV3) {
+ tags.push('OA')
+ } else if (isW3CVC) {
+ if (isW3CVCVersion2_0) {
+ tags.push('W3C VC V2.0')
+ } else {
+ tags.push('W3C VC V1.1')
+ }
+ }
+
+ // Add token registry version tag if available
+ if (tokenRegistryVersion === 'V4') {
+ tags.push('TR V4')
+ } else if (tokenRegistryVersion === 'V5') {
+ tags.push('TR V5')
+ }
+
+ return tags
+}
+
+const getRpcUrl = (chainId: string): string | null => {
+ const chainEnvUrl = import.meta.env[`VITE_RPC_URL_${chainId}`]
+ if (chainEnvUrl) return chainEnvUrl
+
+ const chainDefaultUrl =
+ SUPPORTED_CHAINS[chainId as keyof typeof SUPPORTED_CHAINS]?.rpcUrl
+ const safeChainUrl = chainDefaultUrl?.includes('undefined')
+ ? null
+ : chainDefaultUrl
+ if (safeChainUrl) return safeChainUrl
+
+ // Chain not recognised β return null to surface the issue
+ return null
+}
+
+const getDocumentData = (wrappedDocument: any) => {
+ if (
+ vc.isSignedDocument(wrappedDocument) ||
+ vc.isRawDocument(wrappedDocument)
+ ) {
+ return wrappedDocument as any
+ }
+ return getDocumentDataFromWrappedDocument(wrappedDocument)
+}
+export const makeExplorerAddressURL = (
+ address: string,
+ chainId: string
+): string | undefined => {
+ const chainInfo = SUPPORTED_CHAINS[chainId as keyof typeof SUPPORTED_CHAINS]
+ if (!chainInfo?.explorerUrl) {
+ return undefined
+ }
+ return new URL(`/address/${address}`, chainInfo.explorerUrl).href
+}
+
+export const useVerify = (): UseVerifyReturn => {
+ const [verifyStatus, setVerifyStatus] = useState('idle')
+ const [fragments, setFragments] = useState([])
+ const [fileName, setFileName] = useState('')
+ const [errorMessage, setErrorMessage] = useState('')
+ const [dragActive, setDragActive] = useState(false)
+ const [pendingDoc, setPendingDoc] = useState(null)
+ const [verifiedChainId, setVerifiedChainId] = useState('')
+ const [issuerName, setIssuerName] = useState('')
+ const [isTransferable, setIsTransferable] = useState(false)
+ const [tokenRegistryVersion, setTokenRegistryVersion] =
+ useState(null)
+ const [tokenRegistryAddress, setTokenRegistryAddress] = useState<
+ string | undefined
+ >(undefined)
+ const [tags, setTags] = useState([])
+ const [tokenId, setTokenId] = useState(undefined)
+ const [keyId, setKeyId] = useState(undefined)
+ const [rawDocument, setRawDocument] = useState(undefined)
+ const runVerification = async (
+ doc: unknown,
+ chainId: string | null | undefined
+ ) => {
+ const options: { rpcProviderUrl?: string } = {}
+ const rpcUrl = getRpcUrl(chainId ?? '1')
+ if (rpcUrl) options.rpcProviderUrl = rpcUrl
+
+ const results = (await verifyDocument(
+ doc as any,
+ options
+ )) as VerificationFragment[]
+ setFragments(results)
+
+ const types = [...new Set(results.map(f => f.type))]
+ const groupStatuses = types.map(type => computeGroupStatus(results, type))
+ const hasAtLeastOneValid = groupStatuses.some(s => s === 'VALID')
+ const hasNoInvalid = groupStatuses.every(s => s !== 'INVALID')
+ const isValid = hasAtLeastOneValid && hasNoInvalid
+ if (!isValid) setErrorMessage('Verification Failed')
+
+ // Compute issuer name
+ const issuer = getIssuerName(doc, results)
+ setIssuerName(issuer)
+
+ // Check if document is transferable
+ const transferable = isTransferableRecord(doc as any)
+ setIsTransferable(transferable)
+
+ // Extract token registry address
+ const registryAddress = transferable
+ ? getTokenRegistryAddress(doc as any)
+ : undefined
+ setTokenRegistryAddress(registryAddress)
+ //add code to fetch TokenId , keyId from the document
+
+ const _keyId = getDocumentData(doc as any)?.id
+ setKeyId(_keyId)
+
+ if (transferable) {
+ const _tokenId = getTokenId(doc as any)
+ setTokenId(_tokenId)
+ }
+
+ // Detect token registry version (async)
+ let trVersion: TokenRegistryVersion = null
+ if (transferable && rpcUrl) {
+ try {
+ // Create a simple provider for the detection
+ const { ethers } = await import('ethers')
+ const provider = new ethers.providers.JsonRpcProvider(rpcUrl)
+ trVersion = await detectTokenRegistryVersion(doc, provider)
+ } catch (error) {
+ console.error('Failed to detect token registry version:', error)
+ }
+ }
+ setTokenRegistryVersion(trVersion)
+
+ // Compute document tags
+ const documentTags = getDocumentTags(doc, trVersion)
+ setTags(documentTags)
+
+ setRawDocument(doc)
+ setVerifiedChainId(chainId ?? '')
+ setVerifyStatus(isValid ? 'valid' : 'invalid')
+ }
+
+ const clearVerificationMetadata = () => {
+ setVerifiedChainId('')
+ setIssuerName('')
+ setIsTransferable(false)
+ setTokenRegistryVersion(null)
+ setTokenRegistryAddress(undefined)
+ setTags([])
+ setTokenId(undefined)
+ setKeyId(undefined)
+ setRawDocument(undefined)
+ }
+
+ const processFile = async (file: File) => {
+ setFileName(file.name)
+ setVerifyStatus('verifying')
+ setFragments([])
+ setErrorMessage('')
+ setPendingDoc(null)
+ clearVerificationMetadata()
+
+ try {
+ const text = await file.text()
+ const doc = JSON.parse(text)
+ const chainId = getChainId(doc)
+
+ if (!chainId && (isTransferableRecord(doc) || isDocumentRevokable(doc))) {
+ // Document needs blockchain verification but has no embedded chain β ask the user
+ setPendingDoc(doc)
+ setVerifyStatus('network-select')
+ return
+ }
+
+ await runVerification(doc, chainId)
+ } catch (err) {
+ clearVerificationMetadata()
+ setErrorMessage(
+ toErrorMessage(err, 'Verification failed. Please try again.')
+ )
+ setVerifyStatus('error')
+ }
+ }
+
+ const handleNetworkConfirm = async (chainId: string) => {
+ if (!pendingDoc) return
+ setVerifyStatus('verifying')
+ try {
+ await runVerification(pendingDoc, chainId)
+ } catch (err) {
+ setErrorMessage(
+ toErrorMessage(err, 'Verification failed. Please try again.')
+ )
+ setVerifyStatus('error')
+ } finally {
+ setPendingDoc(null)
+ }
+ }
+
+ const handleNetworkCancel = () => {
+ setVerifyStatus('idle')
+ setFileName('')
+ setPendingDoc(null)
+ clearVerificationMetadata()
+ }
+
+ const handleDrag = (e: React.DragEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+ if (e.type === 'dragenter' || e.type === 'dragover') {
+ setDragActive(true)
+ } else if (e.type === 'dragleave') {
+ setDragActive(false)
+ }
+ }
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+ setDragActive(false)
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
+ processFile(e.dataTransfer.files[0])
+ }
+ }
+
+ const handleFileInput = (e: React.ChangeEvent) => {
+ if (e.target.files && e.target.files[0]) {
+ processFile(e.target.files[0])
+ e.target.value = ''
+ }
+ }
+
+ const handleReset = () => {
+ setVerifyStatus('idle')
+ setFragments([])
+ setFileName('')
+ setErrorMessage('')
+ setPendingDoc(null)
+ clearVerificationMetadata()
+ }
+
+ const getGroupStatus = (type: string) => computeGroupStatus(fragments, type)
+
+ return {
+ verifyStatus,
+ fileName,
+ errorMessage,
+ dragActive,
+ verifiedChainId,
+ issuerName,
+ isTransferable,
+ tokenRegistryVersion,
+ tokenRegistryAddress,
+ tags,
+ tokenId,
+ keyId,
+ rawDocument,
+ getGroupStatus,
+ handleDrag,
+ handleDrop,
+ handleFileInput,
+ handleReset,
+ handleNetworkConfirm,
+ handleNetworkCancel,
+ }
+}
diff --git a/src/components/icons/CodeIcon.test.tsx b/src/components/icons/CodeIcon.test.tsx
new file mode 100644
index 0000000..aba84c3
--- /dev/null
+++ b/src/components/icons/CodeIcon.test.tsx
@@ -0,0 +1,39 @@
+import { describe, it, expect } from 'vitest'
+import { render } from '@testing-library/react'
+import CodeIcon from './CodeIcon'
+
+describe('CodeIcon', () => {
+ it('renders an SVG element', () => {
+ const { container } = render( )
+ const svg = container.querySelector('svg')
+ expect(svg).toBeTruthy()
+ })
+
+ it('defaults to 24x24 size', () => {
+ const { container } = render( )
+ const svg = container.querySelector('svg')
+ expect(svg?.getAttribute('width')).toBe('24')
+ expect(svg?.getAttribute('height')).toBe('24')
+ })
+
+ it('accepts custom fontSize', () => {
+ const { container } = render( )
+ const svg = container.querySelector('svg')
+ expect(svg?.getAttribute('width')).toBe('32')
+ expect(svg?.getAttribute('height')).toBe('32')
+ })
+
+ it('accepts custom stroke color', () => {
+ const { container } = render( )
+ const paths = container.querySelectorAll('path')
+ paths.forEach(path => {
+ expect(path.getAttribute('stroke')).toBe('#ff0000')
+ })
+ })
+
+ it('defaults to currentColor stroke', () => {
+ const { container } = render( )
+ const path = container.querySelector('path')
+ expect(path?.getAttribute('stroke')).toBe('currentColor')
+ })
+})
diff --git a/src/components/icons/CodeIcon.tsx b/src/components/icons/CodeIcon.tsx
new file mode 100644
index 0000000..760eb3e
--- /dev/null
+++ b/src/components/icons/CodeIcon.tsx
@@ -0,0 +1,33 @@
+import { SVGProps } from 'react'
+
+const CodeIcon = ({
+ fontSize = 24,
+ stroke = 'currentColor',
+ ...props
+}: SVGProps) => (
+
+
+
+
+)
+
+export default CodeIcon
diff --git a/src/components/icons/RightArrowIcon.test.tsx b/src/components/icons/RightArrowIcon.test.tsx
new file mode 100644
index 0000000..d667eed
--- /dev/null
+++ b/src/components/icons/RightArrowIcon.test.tsx
@@ -0,0 +1,33 @@
+import { describe, it, expect } from 'vitest'
+import { render } from '@testing-library/react'
+import RightArrowIcon from './RightArrowIcon'
+
+describe('RightArrowIcon', () => {
+ it('renders an SVG element', () => {
+ const { container } = render( )
+ const svg = container.querySelector('svg')
+ expect(svg).toBeTruthy()
+ })
+
+ it('has correct default dimensions', () => {
+ const { container } = render( )
+ const svg = container.querySelector('svg')
+ expect(svg?.getAttribute('width')).toBe('24px')
+ expect(svg?.getAttribute('height')).toBe('24px')
+ })
+
+ it('uses currentColor as default fill', () => {
+ const { container } = render( )
+ const svg = container.querySelector('svg')
+ expect(svg?.getAttribute('fill')).toBe('currentColor')
+ })
+
+ it('passes through additional SVG props', () => {
+ const { container } = render(
+
+ )
+ const svg = container.querySelector('svg')
+ expect(svg?.getAttribute('data-testid')).toBe('arrow')
+ expect(svg?.classList.contains('my-icon')).toBe(true)
+ })
+})
diff --git a/src/components/icons/RightArrowIcon.tsx b/src/components/icons/RightArrowIcon.tsx
new file mode 100644
index 0000000..a195896
--- /dev/null
+++ b/src/components/icons/RightArrowIcon.tsx
@@ -0,0 +1,16 @@
+import { SVGProps } from 'react'
+
+const RightArrowIcon = ({ ...props }: SVGProps) => (
+
+
+
+)
+
+export default RightArrowIcon
diff --git a/src/data/carousel.json b/src/data/carousel.json
new file mode 100644
index 0000000..b7922c4
--- /dev/null
+++ b/src/data/carousel.json
@@ -0,0 +1,74 @@
+{
+ "items": [
+ {
+ "content": {
+ "title": "Driving Progress",
+ "subtitle": "Across Industries",
+ "description": "When trust is paramount, our secure and transparent framework makes digital verification effortless. Our numbers tell the story of a growing network of users and organizations who rely on us with their most important data.",
+ "link": "/learn-more"
+ },
+ "image": "/images/carousel/carousel-1.png",
+ "stats": {
+ "topLeft": {
+ "value": "500K+",
+ "label": "Documents Verified"
+ },
+ "topRight": {
+ "value": "10K+",
+ "label": "Active Users"
+ },
+ "bottomLeft": {
+ "value": "###",
+ "label": "Organizations"
+ },
+ "bottomRight": {
+ "value": "50+",
+ "label": "Countries"
+ }
+ }
+ },
+ {
+ "content": {
+ "title": "Driving Progress",
+ "subtitle": "in Trade Industry",
+ "description": "An open-source framework for digital trade documents. It allows trading partners to create, exchange, verify digitised documents, and transfer ownership of documents across different digital platforms seamlessly",
+ "link": "/trade"
+ },
+ "image": "/images/carousel/carousel-2.png",
+ "stats": {
+ "topLeft": {
+ "value": "500K+",
+ "label": "Documents Verified"
+ },
+ "topRight": {
+ "value": "10K+",
+ "label": "Active Users"
+ },
+ "bottomLeft": {
+ "value": "###",
+ "label": "Organizations"
+ },
+ "bottomRight": {
+ "value": "50+",
+ "label": "Countries"
+ }
+ }
+ },
+ {
+ "content": {
+ "title": "Driving Progress",
+ "subtitle": "In Academic Industry",
+ "description": "OpenCerts is an open-source framework which education institutions can adopt for issuing certificates. Verify academic certificates, diplomas, and professional certifications instantly."
+ },
+ "image": "/images/carousel/carousel-3.png"
+ },
+ {
+ "content": {
+ "title": "Driving Progress",
+ "subtitle": "In Legal Industry",
+ "description": "A digital certificate of authenticity for public documents, launched by the Singapore Academy of Law (SAL) in partnership with IMDA in June 2025 to simplify and secure cross-border legal notarisation."
+ },
+ "image": "/images/carousel/carousel-4.png"
+ }
+ ]
+}
diff --git a/src/hooks/useContactForm.ts b/src/hooks/useContactForm.ts
new file mode 100644
index 0000000..b04c514
--- /dev/null
+++ b/src/hooks/useContactForm.ts
@@ -0,0 +1,520 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import {
+ getPresignedUrls,
+ uploadToPresignedUrl,
+ createServiceRequestWithKeys,
+} from '@/utils/upload'
+import {
+ MAX_TOTAL_UPLOAD_BYTES,
+ MAX_FILES,
+ isValidFileType,
+ getFileConstraintText,
+} from '@/utils/attachmentConfig'
+import type { AttachmentItem } from '@/types/attachment'
+
+export type EnquiryType = '' | 'General_Enquiry' | 'OpenCerts' | 'TradeTrust'
+
+const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/
+function isValidEmail(value: string): boolean {
+ return EMAIL_REGEX.test(value.trim())
+}
+
+let idCounter = 0
+function nextId() {
+ return `att-${++idCounter}-${Date.now()}`
+}
+
+export type UseContactFormOptions = {
+ getRecaptchaToken: () => string | Promise
+ resetRecaptcha?: () => void
+ /** When true, submit is only enabled after the user completes reCAPTCHA */
+ recaptchaRequired?: boolean
+}
+
+export const useContactForm = (options: UseContactFormOptions) => {
+ const {
+ getRecaptchaToken,
+ resetRecaptcha,
+ recaptchaRequired = false,
+ } = options
+ const [email, setEmail] = useState('')
+ const [typeOfEnquiry, setTypeOfEnquiry] = useState('')
+ const [description, setDescription] = useState('')
+ const [attachments, setAttachments] = useState([])
+ const [dragActive, setDragActive] = useState(false)
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const [submitError, setSubmitError] = useState(null)
+ const [submitSuccess, setSubmitSuccess] = useState(null)
+ const [recaptchaCompleted, setRecaptchaCompleted] = useState(false)
+ const [fieldErrors, setFieldErrors] = useState<{
+ email?: string
+ typeOfEnquiry?: string
+ description?: string
+ attachments?: string
+ recaptcha?: string
+ }>({})
+ const uploadControllersRef = useRef>(new Map())
+
+ const fileInfoText = useMemo(() => getFileConstraintText(), [])
+
+ const allUploaded = useMemo(
+ () =>
+ attachments.length === 0 ||
+ attachments.every(a => a.status === 'uploaded'),
+ [attachments]
+ )
+ const hasError = useMemo(
+ () => attachments.some(a => a.status === 'error'),
+ [attachments]
+ )
+ const isUploading = useMemo(
+ () => attachments.some(a => a.status === 'uploading'),
+ [attachments]
+ )
+
+ const isFormValid = useMemo(() => {
+ const emailTrimmed = email.trim()
+ const descriptionTrimmed = description.trim()
+ const requiredFilled =
+ !!emailTrimmed && !!typeOfEnquiry && !!descriptionTrimmed && !hasError
+ const captchaOk = !recaptchaRequired || recaptchaCompleted
+ return requiredFilled && captchaOk
+ }, [
+ email,
+ typeOfEnquiry,
+ description,
+ hasError,
+ recaptchaRequired,
+ recaptchaCompleted,
+ ])
+
+ const setAttachmentStatus = useCallback(
+ (
+ id: string,
+ update: Partial<
+ Pick<
+ AttachmentItem,
+ 'status' | 'progress' | 'error' | 'key' | 'filename' | 'previewUrl'
+ >
+ >
+ ) => {
+ setAttachments(prev =>
+ prev.map(a => (a.id === id ? { ...a, ...update } : a))
+ )
+ },
+ []
+ )
+
+ const clearRecaptchaError = useCallback(() => {
+ setFieldErrors(prev =>
+ prev.recaptcha ? { ...prev, recaptcha: undefined } : prev
+ )
+ setRecaptchaCompleted(true)
+ }, [])
+
+ const addFiles = useCallback(
+ (newFiles: File[]) => {
+ const nonEmpty = newFiles.filter(file => file.size > 0)
+ const emptyCount = newFiles.length - nonEmpty.length
+ const valid = nonEmpty.filter(isValidFileType)
+ const invalidCount = newFiles.length - emptyCount - valid.length
+ if (emptyCount > 0) {
+ setFieldErrors(prev => ({
+ ...prev,
+ attachments: 'Empty files are not allowed.',
+ }))
+ }
+ if (invalidCount > 0) {
+ const msg =
+ 'Some files were rejected. Only JPG, JPEG, and PNG files are allowed.'
+ setFieldErrors(prev => ({ ...prev, attachments: msg }))
+ }
+ if (valid.length === 0) return
+
+ const currentTotal = attachments.reduce((s, a) => s + a.file.size, 0)
+ const addedTotal = valid.reduce((s, f) => s + f.size, 0)
+ if (currentTotal + addedTotal > MAX_TOTAL_UPLOAD_BYTES) {
+ const msg = 'Total file size exceeded 10 MB limit.'
+ setFieldErrors(prev => ({ ...prev, attachments: msg }))
+ return
+ }
+ if (attachments.length + valid.length > MAX_FILES) {
+ const msg = `Maximum ${MAX_FILES} files allowed.`
+ setFieldErrors(prev => ({ ...prev, attachments: msg }))
+ return
+ }
+
+ const items: AttachmentItem[] = valid.map(file => ({
+ id: nextId(),
+ file,
+ filename: file.name,
+ status: 'pending',
+ progress: 0,
+ previewUrl: globalThis.URL?.createObjectURL
+ ? globalThis.URL.createObjectURL(file)
+ : undefined,
+ }))
+ setAttachments(prev => [...prev, ...items])
+ setSubmitError(null)
+ setFieldErrors(prev => ({ ...prev, attachments: undefined }))
+ },
+ [attachments]
+ )
+
+ const removeAttachment = useCallback((id: string) => {
+ uploadControllersRef.current.get(id)?.abort()
+ uploadControllersRef.current.delete(id)
+ setAttachments(prev => {
+ const toRemove = prev.find(a => a.id === id)
+ const canRevoke = typeof globalThis.URL?.revokeObjectURL === 'function'
+ if (toRemove?.previewUrl && canRevoke) {
+ globalThis.URL.revokeObjectURL(toRemove.previewUrl)
+ }
+ return prev.filter(a => a.id !== id)
+ })
+ }, [])
+
+ const clearAllAttachments = useCallback(() => {
+ uploadControllersRef.current.forEach(controller => controller.abort())
+ uploadControllersRef.current.clear()
+ setAttachments(prev => {
+ const canRevoke = typeof globalThis.URL?.revokeObjectURL === 'function'
+ if (canRevoke) {
+ prev.forEach(a => {
+ if (a.previewUrl) globalThis.URL.revokeObjectURL(a.previewUrl)
+ })
+ }
+ return []
+ })
+ setSubmitError(null)
+ setFieldErrors(prev =>
+ prev.attachments ? { ...prev, attachments: undefined } : prev
+ )
+ }, [])
+
+ useEffect(() => {
+ const uploadControllers = uploadControllersRef.current
+ return () => {
+ uploadControllers.forEach(controller => controller.abort())
+ uploadControllers.clear()
+ }
+ }, [])
+
+ const handleDrag = useCallback((e: React.DragEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+ if (e.type === 'dragenter' || e.type === 'dragover') setDragActive(true)
+ else if (e.type === 'dragleave') setDragActive(false)
+ }, [])
+
+ const handleDrop = useCallback(
+ (e: React.DragEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+ setDragActive(false)
+ const files = Array.from(e.dataTransfer.files || [])
+ addFiles(files)
+ },
+ [addFiles]
+ )
+
+ const handleFileInput = useCallback(
+ (e: React.ChangeEvent) => {
+ const files = Array.from(e.target.files || [])
+ addFiles(files)
+ e.target.value = ''
+ },
+ [addFiles]
+ )
+
+ const handleEmailChange = useCallback(
+ (value: React.SetStateAction) => {
+ setEmail(value)
+ setFieldErrors(prev =>
+ prev.email ? { ...prev, email: undefined } : prev
+ )
+ },
+ []
+ )
+ const handleTypeOfEnquiryChange = useCallback(
+ (value: React.SetStateAction) => {
+ setTypeOfEnquiry(value)
+ setFieldErrors(prev =>
+ prev.typeOfEnquiry ? { ...prev, typeOfEnquiry: undefined } : prev
+ )
+ },
+ []
+ )
+ const handleDescriptionChange = useCallback(
+ (value: React.SetStateAction) => {
+ setDescription(value)
+ setFieldErrors(prev =>
+ prev.description ? { ...prev, description: undefined } : prev
+ )
+ },
+ []
+ )
+
+ const validateEmail = useCallback(() => {
+ const emailTrimmed = email.trim()
+ setFieldErrors(prev => {
+ const next = { ...prev }
+ if (!emailTrimmed)
+ next.email = 'Please enter your email address before submitting.'
+ else if (!isValidEmail(emailTrimmed))
+ next.email = 'Please enter a valid email address.'
+ else next.email = undefined
+ return next
+ })
+ }, [email])
+
+ const validateTypeOfEnquiry = useCallback(() => {
+ setFieldErrors(prev => ({
+ ...prev,
+ typeOfEnquiry: typeOfEnquiry
+ ? undefined
+ : 'Please select an option before submitting.',
+ }))
+ }, [typeOfEnquiry])
+
+ const validateDescription = useCallback(() => {
+ const descriptionTrimmed = description.trim()
+ setFieldErrors(prev => ({
+ ...prev,
+ description: descriptionTrimmed
+ ? undefined
+ : 'Please enter a description before submitting.',
+ }))
+ }, [description])
+
+ const resetForm = useCallback(() => {
+ setEmail('')
+ setTypeOfEnquiry('')
+ setDescription('')
+ clearAllAttachments()
+ setDragActive(false)
+ setFieldErrors({})
+ // clearAllAttachments already clears submitError
+ setRecaptchaCompleted(false)
+ }, [clearAllAttachments])
+
+ const onSubmit = useCallback(
+ async (e: React.FormEvent) => {
+ e.preventDefault()
+ setSubmitError(null)
+ setSubmitSuccess(null)
+ setFieldErrors({})
+
+ const emailTrimmed = email.trim()
+ const descriptionTrimmed = description.trim()
+ const errors: {
+ email?: string
+ typeOfEnquiry?: string
+ description?: string
+ attachments?: string
+ } = {}
+ if (!emailTrimmed)
+ errors.email = 'Please enter your email address before submitting.'
+ else if (!isValidEmail(emailTrimmed))
+ errors.email = 'Please enter a valid email address.'
+ if (!typeOfEnquiry)
+ errors.typeOfEnquiry = 'Please select an option before submitting.'
+ if (!descriptionTrimmed)
+ errors.description = 'Please enter a description before submitting.'
+ if (Object.keys(errors).length > 0) {
+ setFieldErrors(errors)
+ return
+ }
+
+ if (hasError) {
+ const msg = 'Please remove failed files or try again.'
+ setFieldErrors(prev => ({ ...prev, attachments: msg }))
+ return
+ }
+
+ const recaptchaToken = await Promise.resolve(getRecaptchaToken())
+ if (!recaptchaToken) {
+ setFieldErrors(prev => ({
+ ...prev,
+ recaptcha: 'Please complete the reCAPTCHA verification.',
+ }))
+ return
+ }
+ // clear any stale recaptcha error once we have a token
+ setFieldErrors(prev =>
+ prev.recaptcha ? { ...prev, recaptcha: undefined } : prev
+ )
+
+ const baseUrl = import.meta.env?.VITE_SUPPORT_API_BASE_URL as
+ | string
+ | undefined
+ if (!baseUrl) {
+ setSubmitError('Missing VITE_SUPPORT_API_BASE_URL configuration.')
+ return
+ }
+
+ const domain = globalThis.window?.location?.hostname || ''
+
+ try {
+ setIsSubmitting(true)
+
+ let attachmentKeys: { key: string; filename: string }[] = []
+
+ if (attachments.length > 0) {
+ const pendingItems = attachments.filter(a => a.status === 'pending')
+ const alreadyUploaded = attachments.filter(
+ a => a.status === 'uploaded' && a.key && a.filename
+ )
+ attachmentKeys = alreadyUploaded.map(a => ({
+ key: a.key!,
+ filename: a.filename,
+ }))
+
+ if (pendingItems.length > 0) {
+ const files = pendingItems.map(a => ({
+ filename: a.file.name,
+ contentType: a.file.type || 'application/octet-stream',
+ size: a.file.size,
+ }))
+ const presigned = await getPresignedUrls(files)
+ const presignedByFilename = presigned.reduce<
+ Record
+ >((acc, current) => {
+ if (!acc[current.filename]) acc[current.filename] = []
+ acc[current.filename].push(current)
+ return acc
+ }, {})
+
+ const uploadResults = await Promise.allSettled(
+ pendingItems.map(item => {
+ const candidateList = presignedByFilename[item.file.name] || []
+ const p = candidateList.shift()
+ if (!p) {
+ setAttachmentStatus(item.id, {
+ status: 'error',
+ error: 'No upload URL was returned for this file.',
+ })
+ return Promise.reject(
+ new Error('No upload URL was returned for this file.')
+ )
+ }
+ setAttachmentStatus(item.id, {
+ status: 'uploading',
+ progress: 0,
+ })
+ const uploadController = new AbortController()
+ uploadControllersRef.current.set(item.id, uploadController)
+ return uploadToPresignedUrl(
+ p.uploadUrl,
+ item.file,
+ percent => {
+ setAttachmentStatus(item.id, { progress: percent })
+ },
+ { signal: uploadController.signal, timeoutMs: 30000 }
+ )
+ .then(() => {
+ uploadControllersRef.current.delete(item.id)
+ setAttachmentStatus(item.id, {
+ status: 'uploaded',
+ progress: 100,
+ key: p.key,
+ filename: p.filename,
+ error: undefined,
+ })
+ })
+ .catch(err => {
+ uploadControllersRef.current.delete(item.id)
+ setAttachmentStatus(item.id, {
+ status: 'error',
+ error:
+ err instanceof Error ? err.message : 'Upload failed',
+ })
+ return Promise.reject(err)
+ })
+ })
+ )
+ const failedUploads = uploadResults.filter(
+ result => result.status === 'rejected'
+ )
+ if (failedUploads.length > 0) {
+ setSubmitError(
+ 'Some files failed to upload. Please remove failed files and try again.'
+ )
+ return
+ }
+ attachmentKeys = [
+ ...attachmentKeys,
+ ...presigned.map(p => ({ key: p.key, filename: p.filename })),
+ ]
+ }
+ }
+
+ await createServiceRequestWithKeys({
+ email: emailTrimmed,
+ description: descriptionTrimmed,
+ typeOfEnquiry,
+ domain,
+ attachmentKeys,
+ recaptchaToken,
+ })
+ setSubmitSuccess(
+ "Request submitted successfully. We'll get back to you soon."
+ )
+ globalThis.window?.scrollTo({ top: 0, behavior: 'smooth' })
+ resetForm()
+ resetRecaptcha?.()
+ } catch (err) {
+ const fallback =
+ 'Our support system is temporarily unavailable. Please try again in a few minutes.'
+ const rawMessage = (err as { message?: string } | null | undefined)
+ ?.message
+ const msg =
+ rawMessage && rawMessage !== 'Failed to fetch' ? rawMessage : fallback
+ setSubmitError(msg)
+ } finally {
+ setIsSubmitting(false)
+ }
+ },
+ [
+ email,
+ typeOfEnquiry,
+ description,
+ attachments,
+ hasError,
+ resetForm,
+ getRecaptchaToken,
+ resetRecaptcha,
+ setAttachmentStatus,
+ ]
+ )
+
+ return {
+ email,
+ setEmail: handleEmailChange,
+ typeOfEnquiry,
+ setTypeOfEnquiry: handleTypeOfEnquiryChange,
+ description,
+ setDescription: handleDescriptionChange,
+ attachments,
+ addFiles,
+ removeAttachment,
+ clearAllAttachments,
+ dragActive,
+ isSubmitting,
+ submitError,
+ submitSuccess,
+ fieldErrors,
+ fileInfoText,
+ allUploaded,
+ isUploading,
+ handleDrag,
+ handleDrop,
+ handleFileInput,
+ validateEmail,
+ validateTypeOfEnquiry,
+ validateDescription,
+ clearRecaptchaError,
+ onSubmit,
+ isFormValid,
+ }
+}
diff --git a/src/hooks/useRecaptcha.ts b/src/hooks/useRecaptcha.ts
new file mode 100644
index 0000000..b31c8c2
--- /dev/null
+++ b/src/hooks/useRecaptcha.ts
@@ -0,0 +1,73 @@
+import { useEffect, useRef } from 'react'
+
+const SCRIPT_ID = 'recaptcha-v2-script'
+
+type UseRecaptchaOptions = {
+ siteKey: string | null | undefined
+ onChange?: (token: string) => void
+}
+
+export const useRecaptcha = ({ siteKey, onChange }: UseRecaptchaOptions) => {
+ const containerRef = useRef(null)
+ const widgetIdRef = useRef(null)
+ const siteKeyRef = useRef(siteKey)
+ const onChangeRef = useRef(onChange)
+
+ siteKeyRef.current = siteKey
+ onChangeRef.current = onChange
+
+ useEffect(() => {
+ if (!siteKeyRef.current) return
+
+ const ensureRendered = () => {
+ if (!containerRef.current || widgetIdRef.current !== null) return
+ const grecaptcha = globalThis.window?.grecaptcha
+ if (!grecaptcha || typeof grecaptcha.render !== 'function') return
+
+ widgetIdRef.current = grecaptcha.render(containerRef.current, {
+ sitekey: siteKeyRef.current,
+ callback: (token: string) => {
+ onChangeRef.current?.(token)
+ },
+ } as unknown as { sitekey: string })
+ }
+
+ const loadScript = () => {
+ const existingScript = globalThis.document?.getElementById(SCRIPT_ID)
+ if (existingScript) {
+ globalThis.window?.grecaptcha?.ready(ensureRendered)
+ return
+ }
+ const script = globalThis.document?.createElement('script')
+ if (!script) return
+ script.id = SCRIPT_ID
+ script.src = 'https://www.google.com/recaptcha/api.js?render=explicit'
+ script.async = true
+ script.defer = true
+ script.onload = () => {
+ globalThis.window?.grecaptcha?.ready(ensureRendered)
+ }
+ globalThis.document?.head.appendChild(script)
+ }
+
+ loadScript()
+
+ return () => {
+ widgetIdRef.current = null
+ }
+ }, [])
+
+ const getToken = async (): Promise => {
+ const grecaptcha = globalThis.window?.grecaptcha
+ if (!grecaptcha || widgetIdRef.current === null) return ''
+ return grecaptcha.getResponse(widgetIdRef.current) || ''
+ }
+
+ const reset = () => {
+ const grecaptcha = globalThis.window?.grecaptcha
+ if (!grecaptcha || widgetIdRef.current === null) return
+ grecaptcha.reset(widgetIdRef.current)
+ }
+
+ return { containerRef, getToken, reset }
+}
diff --git a/src/index.css b/src/index.css
index f394a47..234cd80 100644
--- a/src/index.css
+++ b/src/index.css
@@ -2,11 +2,2717 @@
@tailwind components;
@tailwind utilities;
+@layer base {
+ /* Gilroy Light */
+ @font-face {
+ font-family: 'Gilroy';
+ src:
+ url('/fonts/GilroyLight/font.woff2') format('woff2'),
+ url('/fonts/GilroyLight/font.woff') format('woff');
+ font-weight: 300;
+ font-style: normal;
+ font-display: swap;
+ }
+
+ /* Gilroy Medium */
+ @font-face {
+ font-family: 'Gilroy';
+ src:
+ url('/fonts/GilroyMedium/font.woff2') format('woff2'),
+ url('/fonts/GilroyMedium/font.woff') format('woff');
+ font-weight: 500;
+ font-style: normal;
+ font-display: swap;
+ }
+
+ /* Gilroy Bold */
+ @font-face {
+ font-family: 'Gilroy';
+ src:
+ url('/fonts/GilroyBold/font.woff2') format('woff2'),
+ url('/fonts/GilroyBold/font.woff') format('woff');
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+ }
+
+ /* Gilroy ExtraBold */
+ @font-face {
+ font-family: 'Gilroy';
+ src:
+ url('/fonts/GilroyExtraBold/font.woff2') format('woff2'),
+ url('/fonts/GilroyExtraBold/font.woff') format('woff');
+ font-weight: 800;
+ font-style: normal;
+ font-display: swap;
+ }
+
+ /* Theme Variables */
+ :root {
+ --Neutral-33-90: #a9b2bb54;
+ --Primary3360: #686ad2;
+ --Primary-100-50: #7d80d7;
+ --Primary-100-60: #686ad2;
+ --Neutral-100-30: #3d444d;
+ --Neutral-100-10: #1e2026;
+ --text-secondary: #5b6571;
+ --primary-button-color: #5b5bb3;
+ --Alert-100-50: #b83152;
+ --Alert-100-110: #fddae2;
+
+ /* Base Colors - Light Mode */
+ --Base-66-Base-L1: rgba(255, 255, 255, 0.66);
+ --Base-66-Base-L2: rgba(30, 32, 38, 0.66);
+ --Base-33-Base-L1: rgba(255, 255, 255, 0.33);
+ --Base-33-Base-L2: rgba(30, 32, 38, 0.33);
+ --Base-100-Base-L1: #ffffff;
+ --base-100-base-l-2-c: rgba(222, 228, 233, 0.33);
+ }
+
+ .dark-mode {
+ /* Base Colors - Dark Mode */
+ --Base-66-Base-L1: rgba(255, 255, 255, 0.66);
+ --Base-66-Base-L2: rgba(30, 32, 38, 0.66);
+ --Base-33-Base-L2: rgba(30, 32, 38, 0.33);
+ --Base-100-Base-L1: #000000;
+ --base-100-base-l-2-c: rgba(30, 32, 38, 0.66);
+ --Primary-100-50: #7d80d7;
+ --Primary-100-60: #686ad2;
+ --Neutral-100-30: #a9b2bb;
+ --Neutral-100-10: #1e2026;
+ --primary-button-color: #7d80d7;
+ --Alert-100-110: #480418;
+ }
+}
+
body {
margin: 0;
font-family:
- -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
- 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
+ 'Gilroy',
+ -apple-system,
+ BlinkMacSystemFont,
+ 'Segoe UI',
+ 'Roboto',
+ 'Oxygen',
+ 'Ubuntu',
+ 'Cantarell',
+ 'Fira Sans',
+ 'Droid Sans',
+ 'Helvetica Neue',
+ sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
+ background-image: url('/backgrounds/bg-light.png');
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-attachment: scroll;
+}
+
+body.dark-mode {
+ background-image: url('/backgrounds/bg-dark.png');
+ background-color: #1a1d2e;
+}
+
+.hero-carousel-pagination .swiper-pagination-bullet-light {
+ background: #dfe1ff;
+ opacity: 1;
+ width: 10px;
+ height: 10px;
+ margin: 0 !important;
+}
+
+.hero-carousel-pagination .swiper-pagination-bullet-active {
+ background: #5b5bb3; /* your purple color */
+}
+
+.hero-carousel-pagination .swiper-pagination-bullet-dark {
+ background: #1f1b45;
+ opacity: 1;
+ width: 10px;
+ height: 10px;
+ margin: 0 !important;
+}
+
+/* Navbar Styles */
+.navbar {
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 88px;
+ padding: 0.5rem 1rem;
+ z-index: 1000;
+}
+
+.navbar-light {
+ background:
+ linear-gradient(
+ 0deg,
+ rgba(222, 228, 233, 0) 0%,
+ rgba(222, 228, 233, 0) 100%
+ ),
+ rgba(255, 255, 255, 1);
+}
+
+.navbar-dark {
+ background:
+ linear-gradient(0deg, rgba(30, 32, 38, 1) 0%, rgba(30, 32, 38, 1) 100%),
+ rgba(30, 32, 38, 1);
+}
+
+.navbar-content {
+ width: 100%;
+ height: 72px;
+ padding: 0.5rem;
+ border-radius: 1rem;
+ backdrop-filter: blur(12px);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.navbar-content-light {
+ background:
+ linear-gradient(
+ 0deg,
+ rgba(222, 228, 233, 1) 0%,
+ rgba(222, 228, 233, 1) 100%
+ ),
+ rgba(255, 255, 255, 1);
+ border: 1px solid rgba(0, 0, 0, 0.1);
+}
+
+.navbar-content-dark {
+ background:
+ linear-gradient(0deg, rgba(30, 32, 38, 1) 0%, rgba(30, 32, 38, 1) 100%),
+ rgba(30, 32, 38, 1);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.logo-wrapper {
+ display: flex;
+ align-items: flex-end;
+ gap: 8px;
+ height: 32px;
+ overflow: visible;
+}
+
+.logo-icon {
+ flex-shrink: 0;
+ display: block;
+}
+
+.logo-text {
+ flex-shrink: 0;
+ display: block;
+ margin-bottom: 2px;
+}
+
+.contact-button {
+ background: linear-gradient(135deg, #686ad2 10%, #167eb0 90%);
+}
+
+/* Logo text colors */
+.logo-light path {
+ fill: #403d7d;
+}
+
+.logo-dark path {
+ fill: #aaaee6;
+}
+/* Hero Section */
+.hero-section {
+ width: 100%;
+ max-width: 1280px;
+ padding: 64px 16px 32px 16px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ margin: 0 auto;
+}
+
+.hero-content {
+ width: 100%;
+ max-width: 720px;
+ min-width: min(360px, 100%);
+ padding: 8px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ gap: 24px;
+}
+
+/* Hero Title Container */
+.hero-title-container {
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ display: inline-flex;
+}
+
+/* Hero Title Line */
+.hero-title-line {
+ align-self: stretch;
+ text-align: center;
+ justify-content: center;
+ display: inline;
+ flex-wrap: wrap;
+ color: #1e2026;
+ font-size: 64px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 700;
+ line-height: 71.68px;
+}
+
+.dark-mode .hero-title-line {
+ color: #aaaee6;
+}
+
+/* Hero Gradient Text */
+.hero-gradient-text {
+ background: linear-gradient(135deg, #686ad2 10%, #167eb0 90%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+/* Hero Description */
+.hero-description {
+ align-self: stretch;
+ text-align: center;
+ justify-content: center;
+ display: flex;
+ flex-direction: column;
+ color: #3d444d;
+ font-size: 18px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 500;
+ line-height: 24.48px;
+ margin: 0;
+ word-wrap: break-word;
+}
+
+.dark-mode .hero-description {
+ color: #a9b2bb;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .hero-section {
+ padding: 48px 16px 24px 16px;
+ }
+
+ .hero-title-line span {
+ display: block;
+ }
+
+ .hero-gradient-text {
+ white-space: normal; /* mobile: allow wrapping */
+ }
+}
+
+/* Verify Section - Wrapper */
+.verify-section {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+/* Boundary Frame - Main Container */
+.boundary-frame {
+ width: 100%;
+ max-width: 1280px;
+ padding: 16px;
+ display: inline-flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+}
+
+/* Overlay Border Shadow */
+.overlay-border-shadow {
+ align-self: stretch;
+ min-width: min(340px, 100%);
+ padding: 16px;
+ background:
+ linear-gradient(
+ 0deg,
+ rgba(222, 228, 233, 0) 0%,
+ rgba(222, 228, 233, 0) 100%
+ ),
+ rgba(255, 255, 255, 0.66);
+ box-shadow: 0px 8px 32px rgba(104, 106, 210, 0.33);
+ border-radius: 16px;
+ outline: 1px rgba(169, 178, 187, 0.33) solid;
+ outline-offset: -1px;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-start;
+ gap: 10px;
+}
+
+/* Dark mode styles */
+.dark-mode .overlay-border-shadow {
+ outline: 1px rgba(61, 68, 77, 0.33) solid;
+}
+
+/* Contact form fields container (inner box) */
+.contact-form-fields {
+ display: flex;
+ flex-direction: column;
+ gap: 1.25rem; /* gap-5 equivalent */
+ padding: 1.5rem;
+ border-radius: 12px;
+}
+
+/* Typography Classes - Using Tailwind utilities */
+@layer components {
+ .submit-request-title {
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 700;
+ vertical-align: middle;
+ font-size: 24px;
+ line-height: 133%;
+ }
+
+ .encountering-issues-text {
+ font-family: 'Avenir', sans-serif;
+ font-weight: 500;
+ vertical-align: middle;
+ font-size: 14px;
+ line-height: 155%;
+ }
+
+ .form-label {
+ font-family: 'Avenir', sans-serif;
+ font-weight: 500;
+ vertical-align: middle;
+ font-size: 18px;
+ line-height: 136%;
+ }
+
+ /* Field error text (email, enquiry, description, attachments) */
+ .field-error-text {
+ color: var(--Alert-100-50, #b83152);
+ font-size: 12px;
+ font-weight: 500;
+ }
+ .field-error-text.field-error-with-icon {
+ display: flex;
+ align-items: flex-start;
+ gap: 6px;
+ }
+ .field-error-text .field-error-icon {
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+ margin-top: 0.125em;
+ }
+
+ /* Top-level form alert (API/config error and success) */
+ .form-alert-error,
+ .form-alert {
+ margin-top: 1rem;
+ }
+ .form-alert-error {
+ background: var(--Alert-100-110, #fddae2);
+ color: var(--Neutral-100-10, #1e2026);
+ border: 1px solid rgba(239, 68, 68, 0.4);
+ }
+ .dark-mode .form-alert-error {
+ border-color: rgba(239, 68, 68, 0.3);
+ background: var(--Alert-100-110, #480418);
+ color: #dee4e9;
+ }
+
+ /* Contact Page Heading */
+ .contact-heading {
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 800;
+ font-size: 40px;
+ line-height: 1.2;
+ }
+
+ @media (min-width: 640px) {
+ .contact-heading {
+ font-size: 48px;
+ }
+ }
+
+ .contact-heading-contact {
+ color: var(--Primary-100-60, #686ad2);
+ }
+
+ .contact-heading-us {
+ color: var(--Neutral-100-10, #1e2026);
+ }
+
+ .dark-mode .contact-heading-us {
+ color: #ffffff;
+ }
+
+ /* Contact Page Description */
+ .contact-description {
+ font-family: 'Avenir', sans-serif;
+ font-weight: 500;
+ font-size: 18px;
+ line-height: 136%;
+ letter-spacing: normal;
+ text-align: center;
+ vertical-align: middle;
+ color: var(--Neutral-100-30, #3d444d);
+ }
+
+ .dark-mode .contact-description {
+ color: #ffffff;
+ }
+
+ /* Form Backgrounds - Using CSS variables for gradients */
+ .overlay-border-shadow {
+ background:
+ linear-gradient(
+ 0deg,
+ var(--Base-66-Base-L1, rgba(255, 255, 255, 0.66)),
+ var(--Base-66-Base-L1, rgba(255, 255, 255, 0.66))
+ ),
+ linear-gradient(0deg, rgba(222, 228, 233, 0), rgba(222, 228, 233, 0));
+ box-shadow: 0px 8px 32px rgba(104, 106, 210, 0.33);
+ }
+
+ .dark-mode .overlay-border-shadow {
+ background:
+ linear-gradient(0deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)),
+ linear-gradient(
+ 0deg,
+ var(--Base-66-Base-L2, rgba(30, 32, 38, 0.66)),
+ var(--Base-66-Base-L2, rgba(30, 32, 38, 0.66))
+ );
+ box-shadow: 0px 8px 32px 0px var(--Primary3360, #686ad2);
+ }
+
+ .contact-form-fields {
+ background:
+ linear-gradient(
+ 0deg,
+ var(--Base-100-Base-L1, #ffffff),
+ var(--Base-100-Base-L1, #ffffff)
+ ),
+ linear-gradient(
+ 0deg,
+ var(--base-100-base-l-2-c, rgba(222, 228, 233, 0.33)),
+ var(--base-100-base-l-2-c, rgba(222, 228, 233, 0.33))
+ );
+ }
+
+ .dark-mode .contact-form-fields {
+ background:
+ linear-gradient(
+ 0deg,
+ var(--Base-100-Base-L1, #000000),
+ var(--Base-100-Base-L1, #000000)
+ ),
+ linear-gradient(
+ 0deg,
+ var(--base-100-base-l-2-c, rgba(30, 32, 38, 0.66)),
+ var(--base-100-base-l-2-c, rgba(30, 32, 38, 0.66))
+ );
+ }
+
+ /* Override browser autofill white background and outline in dark mode */
+ .dark-mode input:-webkit-autofill,
+ .dark-mode input:-webkit-autofill:hover,
+ .dark-mode input:-webkit-autofill:focus,
+ .dark-mode textarea:-webkit-autofill,
+ .dark-mode textarea:-webkit-autofill:hover,
+ .dark-mode textarea:-webkit-autofill:focus {
+ -webkit-box-shadow: 0 0 0 1000px #000000 inset;
+ -webkit-text-fill-color: #dee4e9;
+ caret-color: #dee4e9;
+ outline: none !important;
+ border-color: rgb(45 44 44) !important;
+ }
+
+ .contact-form-divider {
+ background:
+ linear-gradient(
+ 0deg,
+ var(--Base-66-Base-L1, rgba(255, 255, 255, 0.66)),
+ var(--Base-66-Base-L1, rgba(255, 255, 255, 0.66))
+ ),
+ linear-gradient(0deg, rgba(222, 228, 233, 0), rgba(222, 228, 233, 0));
+ }
+
+ .dark-mode .contact-form-divider {
+ background:
+ linear-gradient(0deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)),
+ linear-gradient(
+ 0deg,
+ var(--Base-66-Base-L2, rgba(30, 32, 38, 0.66)),
+ var(--Base-66-Base-L2, rgba(30, 32, 38, 0.66))
+ );
+ }
+
+ /* Submit Button */
+ .submit-button {
+ background: var(--Primary-100-50, #7d80d7);
+ width: 383px;
+ max-width: 100%;
+ height: 40px;
+ min-width: 40px;
+ min-height: 40px;
+ border-radius: 8px;
+ padding: 5px;
+ color: white;
+ font-size: 14px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 700;
+ border: none;
+ cursor: pointer;
+ transition: opacity 0.2s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .dark-mode .submit-button {
+ color: #000000;
+ }
+
+ .submit-button:disabled {
+ cursor: not-allowed;
+ opacity: 0.6;
+ }
+
+ .submit-button:hover:not(:disabled) {
+ opacity: 0.9;
+ }
+}
+
+/* Contact form divider */
+.contact-form-divider {
+ width: 100%;
+ border: 1px solid var(--Neutral-33-90, #a9b2bb54);
+ background:
+ linear-gradient(
+ 0deg,
+ var(--Base-66-Base-L1, rgba(255, 255, 255, 0.66)),
+ var(--Base-66-Base-L1, rgba(255, 255, 255, 0.66))
+ ),
+ linear-gradient(0deg, rgba(222, 228, 233, 0), rgba(222, 228, 233, 0));
+}
+
+.dark-mode .contact-form-divider {
+ border-color: var(--Neutral-33-90, #a9b2bb54);
+ background:
+ linear-gradient(0deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)),
+ linear-gradient(
+ 0deg,
+ var(--Base-66-Base-L2, rgba(30, 32, 38, 0.66)),
+ var(--Base-66-Base-L2, rgba(30, 32, 38, 0.66))
+ );
+}
+
+/* Frame Container */
+.frame-container {
+ align-self: stretch;
+ border-radius: 12px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 16px;
+}
+
+/* Frame Dropbox */
+.frame-dropbox {
+ align-self: stretch;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-start;
+}
+
+/* Dropbox Area */
+.dropbox-area {
+ align-self: stretch;
+ width: 960px;
+ max-width: 100%;
+ height: 164px;
+ padding: 16px;
+ border-radius: 12px;
+ border: 1px dashed #aaaee6;
+ background:
+ linear-gradient(
+ 0deg,
+ var(--Base-33-Base-L1, rgba(255, 255, 255, 0.33)),
+ var(--Base-33-Base-L1, rgba(255, 255, 255, 0.33))
+ ),
+ linear-gradient(0deg, rgba(222, 228, 233, 0), rgba(222, 228, 233, 0));
+ opacity: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+}
+
+@media (min-width: 640px) {
+ .dropbox-area {
+ padding: 24px;
+ }
+}
+
+.dark-mode .dropbox-area {
+ background:
+ linear-gradient(0deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)),
+ linear-gradient(
+ 0deg,
+ var(--Base-33-Base-L2, rgba(30, 32, 38, 0.33)),
+ var(--Base-33-Base-L2, rgba(30, 32, 38, 0.33))
+ );
+ border-color: #403d7d;
+}
+
+.dropbox-area.drag-active {
+ background: rgba(104, 106, 210, 0.1);
+ outline: 2px #686ad2 dashed;
+}
+
+/* Home verify dropbox should span full container */
+.dropbox-area--home {
+ width: 100%;
+ max-width: none;
+}
+
+/* Frame Dropbox Text */
+.frame-dropbox-text {
+ padding: 8px;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ display: inline-flex;
+}
+
+.dropbox-text {
+ text-align: center;
+ color: #3d444d;
+ font-family: 'Avenir', sans-serif;
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 155%;
+ letter-spacing: 0%;
+ word-wrap: break-word;
+}
+
+.dark-mode .dropbox-text {
+ color: #a9b2bb;
+}
+
+/* Frame Divider */
+.frame-divider {
+ padding: 8px;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ display: flex;
+}
+
+.divider-text {
+ text-align: center;
+ color: #3d444d;
+ font-size: 14px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 500;
+ line-height: 21.7px;
+ word-wrap: break-word;
+}
+
+.dark-mode .divider-text {
+ color: #a9b2bb;
+}
+
+/* Standard Button Primary */
+.standard-button-primary {
+ min-width: 32px;
+ min-height: 32px;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ display: flex;
+}
+
+.button-boundary {
+ align-self: stretch;
+ min-width: 32px;
+ min-height: 32px;
+ padding: 2px;
+ position: relative;
+ background: #5b5bb3;
+ overflow: hidden;
+ border-radius: 6px;
+ justify-content: center;
+ align-items: center;
+ display: inline-flex;
+ transition: background 0.2s ease;
+ cursor: pointer;
+ border: none;
+}
+
+.dark-mode .button-boundary {
+ background: #7d80d7;
+}
+
+.button-boundary:hover {
+ background: #4a4a92;
+}
+
+.dark-mode .button-boundary:hover {
+ background: #6a6dc0;
+}
+
+.button-padding {
+ width: 4px;
+ height: 28px;
+}
+
+.contextual-icon-frame {
+ height: 24px;
+ padding: 4px;
+ overflow: hidden;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ gap: 10px;
+ display: inline-flex;
+}
+
+.contextual-icon-frame svg {
+ width: 16px;
+ height: 16px;
+}
+
+.dark-mode .upload-icon path {
+ fill: #000000;
+}
+
+.text-frame {
+ padding: 4px;
+ justify-content: flex-start;
+ align-items: center;
+ gap: 10px;
+ display: flex;
+}
+
+.button-label {
+ text-align: center;
+ justify-content: center;
+ display: flex;
+ flex-direction: column;
+ color: white;
+ font-size: 14px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 700;
+ line-height: 19.8px;
+ word-wrap: break-word;
+}
+
+.dark-mode .button-label {
+ color: black;
+}
+
+/* Frame File Info */
+.frame-file-info {
+ align-self: stretch;
+ padding: 8px;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ display: inline-flex;
+}
+
+.file-info-text {
+ flex: 1 1 0;
+ color: #5b6571;
+ font-family: 'Avenir', sans-serif;
+ font-weight: 500;
+ font-size: 12px;
+ line-height: 165%;
+ letter-spacing: 0%;
+ word-wrap: break-word;
+}
+
+.dark-mode .file-info-text {
+ color: #808894;
+}
+
+/* Demo Button Section */
+.demo-button {
+ align-self: stretch;
+ padding: 24px;
+ position: relative;
+ background: linear-gradient(135deg, #686ad2 10%, #167eb0 90%);
+ overflow: hidden;
+ border-radius: 12px;
+ justify-content: flex-start;
+ align-items: center;
+ display: inline-flex;
+ flex-wrap: wrap;
+ align-content: center;
+ gap: 16px;
+}
+
+.demo-button button,
+.demo-button .cta-button {
+ background: none;
+ border: none;
+ padding: 0;
+ font: inherit;
+ cursor: pointer;
+}
+
+.demo-button button:focus-visible,
+.cta-button:focus-visible {
+ outline: 2px solid #686ad2;
+ outline-offset: 4px;
+}
+
+.demo-button::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: url('/images/demo-background.png');
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ opacity: 1;
+ z-index: 0;
+}
+
+.demo-button > * {
+ position: relative;
+ z-index: 1;
+}
+
+.dark-mode .demo-button::before {
+ opacity: 0.4;
+}
+
+/* Demo Content */
+.demo-content {
+ flex: 1 1 0;
+ min-width: min(260px, 100%);
+ padding: 4px;
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
+ display: inline-flex;
+ outline: none;
+ border: none;
+}
+
+.demo-text-wrapper {
+ align-self: stretch;
+ padding: 4px;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ display: inline-flex;
+}
+
+.demo-heading {
+ flex: 1 1 0;
+ justify-content: center;
+ display: flex;
+ flex-direction: column;
+ color: white;
+ font-size: 24px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 700;
+ line-height: 31.92px;
+ word-wrap: break-word;
+}
+
+.demo-description-wrapper {
+ align-self: stretch;
+ padding: 4px;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ display: inline-flex;
+}
+
+.demo-description {
+ flex: 1 1 0;
+ justify-content: center;
+ display: flex;
+ flex-direction: column;
+ color: white;
+ font-size: 14px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 500;
+ line-height: 21.7px;
+ word-wrap: break-word;
+}
+
+/* CTA Button Wrapper */
+.cta-button-wrapper {
+ width: 248px;
+ padding: 0;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-start;
+ gap: 10px;
+ display: inline-flex;
+ background: transparent;
+}
+
+.cta-button {
+ align-self: stretch;
+ min-height: 48px;
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
+ gap: 0;
+ display: flex;
+ cursor: pointer;
+ transition:
+ transform 0.2s ease,
+ box-shadow 0.2s ease;
+ background: transparent;
+}
+
+.cta-boundary {
+ align-self: stretch;
+ min-width: 48px;
+ min-height: 48px;
+ padding: 8px;
+ position: relative;
+ background: linear-gradient(135deg, #686ad2 10%, #167eb0 90%);
+ overflow: hidden;
+ border-radius: 999px;
+ justify-content: flex-start;
+ align-items: center;
+ display: inline-flex;
+}
+
+.cta-padding {
+ width: 8px;
+ height: 30px;
+}
+
+.cta-text-frame {
+ padding: 4px;
+ justify-content: flex-start;
+ align-items: center;
+ gap: 10px;
+ display: flex;
+}
+
+.cta-label {
+ text-align: center;
+ justify-content: center;
+ display: flex;
+ flex-direction: column;
+ color: white;
+ font-size: 18px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 700;
+ line-height: 24.48px;
+ word-wrap: break-word;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+/* VerifySection - dropbox state variants */
+.dropbox-area--centered {
+ justify-content: center;
+ min-height: 140px;
+ gap: 12px;
+}
+
+.dropbox-area--result {
+ align-items: stretch;
+ gap: 16px;
+}
+
+/* Spinner */
+.spinner {
+ width: 32px;
+ height: 32px;
+ border: 3px solid #686ad2;
+ border-top-color: transparent;
+ border-radius: 50%;
+ margin: 0 auto 12px;
+ animation: spin 0.8s linear infinite;
+}
+
+.spinner-small {
+ width: 16px;
+ height: 16px;
+}
+
+.spinner-medium {
+ width: 32px;
+ height: 32px;
+}
+
+.spinner-large {
+ width: 48px;
+ height: 48px;
+}
+
+/* Centered wrapper for loading states */
+.spinner-centered-wrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+}
+
+.spinner-label {
+ text-align: center;
+ font-size: 14px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 500;
+ color: #3d444d;
+}
+
+.dark-mode .spinner-label {
+ color: #a9b2bb;
+}
+
+/* Ghost button - used in error states */
+.verify-ghost-button {
+ align-self: center;
+ background: none;
+ border: 1px solid #5b5bb3;
+ border-radius: 6px;
+ padding: 6px 16px;
+ color: #5b5bb3;
+ font-size: 13px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 700;
+ cursor: pointer;
+}
+
+/* Error message */
+.verify-error-message {
+ text-align: center;
+ color: #ef4444;
+ font-size: 14px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 500;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .cta-button-wrapper {
+ width: 100%;
+ }
+
+ .cta-button {
+ align-self: flex-start;
+ }
+}
+
+/* βββ NetworkModal ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+.nm-card {
+ width: 100%;
+ max-width: 640px;
+ background: #ffffff;
+ box-shadow: 0px 8px 32px rgba(104, 106, 210, 0.33);
+ border-radius: 16px;
+ outline: 1px solid rgba(169, 178, 187, 0.33);
+ outline-offset: -1px;
+ display: flex;
+ flex-direction: column;
+}
+.dark-mode .nm-card {
+ background: #1c2128;
+ outline-color: rgba(48, 54, 61, 0.8);
+}
+
+.nm-header {
+ align-self: stretch;
+ padding: 24px 24px 16px;
+ display: flex;
+ align-items: flex-start;
+ gap: 16px;
+}
+
+.nm-title-group {
+ flex: 1 1 0;
+}
+
+.nm-title {
+ color: #1e2026;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 700;
+ line-height: 1.33;
+ word-wrap: break-word;
+}
+.dark-mode .nm-title {
+ color: #e6edf3;
+}
+
+.nm-filename {
+ margin-top: 4px;
+ color: #5b6571;
+ font-size: 13px;
+ font-family: 'Avenir', sans-serif;
+ font-weight: 500;
+ word-break: break-all;
+}
+.dark-mode .nm-filename {
+ color: #8b949e;
+}
+
+.nm-content {
+ align-self: stretch;
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+}
+
+.nm-desc-row {
+ align-self: stretch;
+ padding: 16px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+}
+
+.nm-desc {
+ flex: 1 1 0;
+ text-align: center;
+ color: #3d444d;
+ font-family: 'Avenir', sans-serif;
+ font-weight: 500;
+ line-height: 1.36;
+ word-wrap: break-word;
+}
+.dark-mode .nm-desc {
+ color: #8b949e;
+}
+
+.nm-network-row {
+ align-self: stretch;
+ padding: 12px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
+}
+
+.nm-label-row {
+ padding: 4px;
+ display: flex;
+ gap: 8px;
+}
+
+.nm-label {
+ color: #5b6571;
+ font-size: 14px;
+ font-family: 'Avenir', sans-serif;
+ font-weight: 500;
+ line-height: 21.7px;
+}
+.dark-mode .nm-label {
+ color: #8b949e;
+}
+
+.nm-dropdown-info-row {
+ align-self: stretch;
+ display: flex;
+ align-items: center;
+}
+
+.nm-dropdown-wrap {
+ flex: 1 1 0;
+ padding: 4px;
+}
+
+.nm-dropdown-btn {
+ width: 100%;
+ min-height: 40px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 4px 8px;
+ border-radius: 8px;
+ border: 1px solid #a9b2bb;
+ background: transparent;
+ cursor: pointer;
+}
+.dark-mode .nm-dropdown-btn {
+ border-color: #30363d;
+ background: #0d1117;
+}
+
+.nm-net-logo {
+ width: 20px;
+ height: 20px;
+ object-fit: cover;
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+
+.nm-net-label {
+ flex: 1 1 0;
+ text-align: left;
+ color: #1e2026;
+ font-size: 14px;
+ font-family: 'Avenir', sans-serif;
+ font-weight: 500;
+ line-height: 21.7px;
+}
+.dark-mode .nm-net-label {
+ color: #e6edf3;
+}
+
+/* chevron icon colour β set via CSS color so SVG can use currentColor */
+.nm-chevron {
+ color: #5b6571;
+}
+.dark-mode .nm-chevron {
+ color: #8b949e;
+}
+
+.nm-info-wrap {
+ padding: 4px;
+}
+
+.nm-info-btn {
+ min-width: 40px;
+ min-height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ border-radius: 8px;
+ padding: 5px;
+ color: #5b5bb3;
+}
+.dark-mode .nm-info-btn {
+ color: #7d80d7;
+}
+
+.nm-dropdown-list {
+ position: absolute;
+ top: calc(100% + 4px);
+ left: 0;
+ right: 0;
+ background: #ffffff;
+ border-radius: 8px;
+ border: 1px solid #a9b2bb;
+ box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.12);
+ z-index: 10;
+ overflow: hidden;
+}
+.dark-mode .nm-dropdown-list {
+ background: #161b22;
+ border-color: #30363d;
+}
+
+.nm-group-header {
+ padding: 6px 14px;
+ font-size: 11px;
+ font-family: 'Avenir', sans-serif;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: #5b6571;
+ background: #f6f8fa;
+}
+.dark-mode .nm-group-header {
+ color: #8b949e;
+ background: #0d1117;
+}
+
+.nm-item {
+ width: 100%;
+ padding: 10px 14px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ cursor: pointer;
+ font-size: 14px;
+ font-family: 'Avenir', sans-serif;
+ font-weight: 400;
+ color: #1e2026;
+ background: transparent;
+}
+.dark-mode .nm-item {
+ color: #e6edf3;
+}
+.nm-item:hover:not(.nm-item--active) {
+ background: #f6f8fa;
+}
+.dark-mode .nm-item:hover:not(.nm-item--active) {
+ background: #21262d;
+}
+.nm-item.nm-item--active {
+ font-weight: 600;
+ background: #f0f3f6;
+}
+.dark-mode .nm-item.nm-item--active {
+ background: #21262d;
+}
+
+.nm-tooltip-inner {
+ background: #ffffff;
+ border-radius: 8px;
+ padding: 16px;
+ box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.15);
+ border: 1px solid rgba(169, 178, 187, 0.33);
+}
+.dark-mode .nm-tooltip-inner {
+ background: #1c2128;
+ border-color: #30363d;
+}
+
+.nm-tooltip-title {
+ margin: 0;
+ font-size: 13px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 700;
+ color: #1e2026;
+}
+.dark-mode .nm-tooltip-title {
+ color: #e6edf3;
+}
+
+.nm-tooltip-body {
+ margin: 8px 0 0;
+ font-size: 13px;
+ font-family: 'Avenir', sans-serif;
+ line-height: 1.5;
+ color: #3d444d;
+}
+.dark-mode .nm-tooltip-body {
+ color: #8b949e;
+}
+
+.nm-footer {
+ align-self: stretch;
+ padding: 16px 24px 24px;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 16px;
+}
+
+.nm-footer-btns {
+ flex: 1 1 0;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.nm-btn {
+ flex: 1 1 0;
+ min-width: 40px;
+ min-height: 40px;
+ padding: 5px;
+ border-radius: 8px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.nm-btn-label {
+ font-size: 14px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 700;
+ line-height: 21.7px;
+}
+
+.nm-cancel-btn {
+ border: 1px solid rgba(169, 178, 187, 0.33);
+ background: transparent;
+ color: #5b5bb3;
+}
+.dark-mode .nm-cancel-btn {
+ border-color: #30363d;
+ color: #7d80d7;
+}
+
+.nm-proceed-btn {
+ border: none;
+ background: #5b5bb3;
+ color: #ffffff;
+}
+.nm-proceed-btn:disabled {
+ background: #cccccc;
+ cursor: not-allowed;
+}
+
+/* βββ VerifyResult ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+.vr-container {
+ align-self: stretch;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+/* ββ Network info card ββ */
+
+.vr-network-card {
+ align-self: stretch;
+ padding: 8px 12px;
+ background: #ffffff;
+ border-radius: 12px;
+ outline: 1px solid rgba(169, 178, 187, 0.33);
+ outline-offset: -1px;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+}
+.dark-mode .vr-network-card {
+ background: #1c2128;
+ outline-color: rgba(48, 54, 61, 0.8);
+}
+
+.vr-network-label {
+ color: #3d444d;
+ font-size: 16px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 500;
+ line-height: 20px;
+ white-space: nowrap;
+ flex-shrink: 0;
+ padding: 4px;
+}
+.dark-mode .vr-network-label {
+ color: #8b949e;
+}
+
+.vr-network-field-group {
+ flex: 1 1 auto;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.vr-network-field {
+ flex: 1 1 auto;
+ max-width: 400px;
+ min-height: 32px;
+ padding: 0 8px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ border-radius: 8px;
+ border: 1px solid #a9b2bb;
+ opacity: 0.5;
+ overflow: hidden;
+}
+.dark-mode .vr-network-field {
+ border-color: #30363d;
+}
+
+.vr-network-value {
+ flex: 1 1 0;
+ color: #1e2026;
+ font-size: 14px;
+ font-family: 'Avenir', sans-serif;
+ font-weight: 500;
+ line-height: 21.7px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.dark-mode .vr-network-value {
+ color: #e6edf3;
+}
+
+.vr-network-sep {
+ display: flex;
+ align-items: center;
+}
+
+.vr-info-btn {
+ min-width: 40px;
+ min-height: 40px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ border-radius: 8px;
+ color: #5b5bb3;
+ flex-shrink: 0;
+}
+.dark-mode .vr-info-btn {
+ color: #7d80d7;
+}
+
+/* ββ Main result card ββ */
+
+.vr-main-card {
+ align-self: stretch;
+ padding: 8px;
+ background: #ffffff;
+ border-radius: 12px;
+ outline: 1px solid rgba(169, 178, 187, 0.33);
+ outline-offset: -1px;
+ display: flex;
+ flex-direction: column;
+}
+.dark-mode .vr-main-card {
+ background: #1c2128;
+ outline-color: rgba(48, 54, 61, 0.8);
+}
+
+/* Header */
+
+.vr-card-header {
+ align-self: stretch;
+ height: 64px;
+ padding: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 8px;
+}
+
+.vr-upload-btn {
+ min-height: 40px;
+ padding: 5px 12px;
+ background: #ffffff;
+ border-radius: 8px;
+ border: 1px solid rgba(169, 178, 187, 0.33);
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+.dark-mode .vr-upload-btn {
+ background: #1c2128;
+ border-color: rgba(48, 54, 61, 0.8);
+}
+
+.vr-upload-btn-label {
+ color: #5b5bb3;
+ font-size: 14px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 700;
+ line-height: 21.7px;
+}
+.dark-mode .vr-upload-btn-label {
+ color: #7d80d7;
+}
+
+/* Body: 3-column grid */
+
+.vr-card-body {
+ align-self: stretch;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ gap: 8px;
+}
+
+/* Left column: Issued by + tags */
+
+.vr-col-issue {
+ flex: 1 1 0;
+ min-width: 232px;
+ display: flex;
+ flex-direction: column;
+}
+
+.vr-issue-info {
+ padding: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.vr-issued-by-label {
+ color: #5b6571;
+ font-size: 16px;
+ font-family: 'Avenir', sans-serif;
+ font-weight: 500;
+ line-height: 24.48px;
+}
+.dark-mode .vr-issued-by-label {
+ color: #8b949e;
+}
+
+.vr-issued-by-value {
+ color: #1e2026;
+ font-size: 16px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 700;
+ line-height: 24.48px;
+ word-break: break-all;
+}
+.dark-mode .vr-issued-by-value {
+ color: #e6edf3;
+}
+
+.vr-issue-tags {
+ padding: 8px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+}
+
+.vr-tag {
+ padding: 8px 12px;
+ border-radius: 999px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.vr-tag--primary {
+ background: #dfe1ff;
+}
+.dark-mode .vr-tag--primary {
+ background: rgba(93, 93, 179, 0.25);
+}
+
+.vr-tag--secondary {
+ background: #b3ecff;
+}
+.dark-mode .vr-tag--secondary {
+ background: rgba(0, 108, 153, 0.25);
+}
+
+.vr-tag-text {
+ font-size: 14px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 700;
+ line-height: 21.7px;
+}
+
+.vr-tag--primary .vr-tag-text {
+ color: #312d62;
+}
+.dark-mode .vr-tag--primary .vr-tag-text {
+ color: #c0c2ff;
+}
+
+.vr-tag--secondary .vr-tag-text {
+ color: #0b384f;
+}
+.dark-mode .vr-tag--secondary .vr-tag-text {
+ color: #7bd5f5;
+}
+
+/* Middle column: verification checks */
+
+.vr-col-checks {
+ flex: 1 1 0;
+ min-width: 232px;
+ display: flex;
+ flex-direction: column;
+}
+
+.vr-checks-list {
+ align-self: stretch;
+ display: flex;
+ flex-direction: column;
+}
+
+.vr-check-row {
+ padding: 8px;
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+}
+
+.vr-check-label {
+ flex: 1 1 0;
+ color: #3d444d;
+ font-size: 16px;
+ font-family: 'Avenir', sans-serif;
+ font-weight: 500;
+ line-height: 24.48px;
+}
+.dark-mode .vr-check-label {
+ color: #8b949e;
+}
+
+/* Right column: NFT links */
+
+.vr-col-nft {
+ flex: 1 1 0;
+ min-width: 232px;
+ padding: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.vr-nft-links {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.vr-nft-link {
+ color: #5b5bb3;
+ font-size: 16px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 700;
+ line-height: 24.48px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ text-align: left;
+ padding: 0;
+ text-decoration: none;
+ display: block;
+}
+.dark-mode .vr-nft-link {
+ color: #7d80d7;
+}
+
+/* Divider */
+
+.vr-divider {
+ align-self: stretch;
+ margin: 0 8px;
+ height: 1px;
+ background: rgba(169, 178, 187, 0.33);
+}
+.dark-mode .vr-divider {
+ background: rgba(48, 54, 61, 0.8);
+}
+
+/* Title Info: Owner + Holder */
+
+.vr-title-info {
+ align-self: stretch;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.vr-title-col {
+ flex: 1 1 0;
+ min-width: 260px;
+ padding: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.vr-title-col-label {
+ color: #5b6571;
+ font-size: 16px;
+ font-family: 'Avenir', sans-serif;
+ font-weight: 500;
+ line-height: 24.48px;
+}
+.dark-mode .vr-title-col-label {
+ color: #8b949e;
+}
+
+.vr-title-col-name {
+ color: #1e2026;
+ font-size: 16px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 700;
+ line-height: 24.48px;
+}
+.dark-mode .vr-title-col-name {
+ color: #e6edf3;
+}
+
+.vr-title-col-addr {
+ color: #006c99;
+ font-size: 14px;
+ font-family: 'Avenir', sans-serif;
+ font-weight: 500;
+ line-height: 21.7px;
+ word-break: break-all;
+}
+.dark-mode .vr-title-col-addr {
+ color: #58a6d4;
+}
+
+/* Connect Wallet footer */
+
+.vr-footer {
+ align-self: stretch;
+ padding: 8px;
+ display: flex;
+ justify-content: flex-end;
+}
+
+.vr-connect-btn {
+ min-width: 188px;
+ min-height: 40px;
+ padding: 5px 16px;
+ border-radius: 8px;
+ border: none;
+ background: #5b5bb3;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.vr-connect-btn-label {
+ padding: 4px;
+ color: #ffffff;
+ font-size: 14px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 700;
+ line-height: 21.7px;
+}
+
+/* Template Renderer Styles */
+.vr-renderer-wrapper {
+ display: flex;
+ flex-direction: column;
+}
+
+.vr-template-tabs-wrapper {
+ overflow-x: auto;
+ margin-bottom: -1px;
+ position: relative;
+ z-index: 1;
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+ mask-image: linear-gradient(
+ to right,
+ transparent 0px,
+ black 32px,
+ black calc(100% - 32px),
+ transparent 100%
+ );
+ -webkit-mask-image: linear-gradient(
+ to right,
+ transparent 0px,
+ black 32px,
+ black calc(100% - 32px),
+ transparent 100%
+ );
+}
+
+.vr-template-tabs-wrapper::-webkit-scrollbar {
+ display: none;
+}
+
+.vr-template-tabs {
+ display: inline-flex;
+ padding: 0 32px;
+ gap: 0;
+ min-width: 100%;
+}
+
+.vr-template-tab {
+ appearance: none;
+ outline: none;
+ padding: 12px 16px;
+ cursor: pointer;
+ font-size: 16px;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 700;
+ color: #5b6571;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 200px;
+ flex-shrink: 0;
+ background: #dee4e9;
+ border: 1px solid rgba(169, 178, 187, 0.33);
+ border-bottom: none;
+ border-radius: 12px 12px 0 0;
+ transition:
+ color 0.15s,
+ background 0.15s;
+ user-select: none;
+}
+
+.vr-template-tab:hover {
+ color: #1e2026;
+}
+
+.vr-template-tab:focus-visible {
+ outline: 2px solid #5b5bb3;
+ outline-offset: -2px;
+}
+
+.vr-template-tab--active {
+ color: #1e2026;
+ background: #ffffff;
+ border-color: rgba(169, 178, 187, 0.33);
+ border-bottom: 1px solid #ffffff;
+ margin-bottom: -1px;
+ position: relative;
+ z-index: 1;
+}
+
+.vr-renderer-section {
+ border-radius: 12px;
+ overflow: hidden;
+ border: 1px solid rgba(169, 178, 187, 0.33);
+ background: #ffffff;
+}
+
+/* Document Utility Toolbar */
+.vr-doc-utility {
+ padding: 32px;
+ background: #ffffff;
+}
+
+.vr-doc-utility-wrap {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ justify-content: flex-end;
+ gap: 16px;
+}
+
+.vr-doc-utility-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.vr-doc-utility-label {
+ display: block;
+ font-size: 20px;
+ font-weight: 700;
+ font-family: 'Gilroy', sans-serif;
+ color: #1e2026;
+ margin-bottom: 4px;
+}
+
+.vr-doc-utility-detail {
+ font-size: 14px;
+ font-weight: 500;
+ font-family: 'Avenir', 'Gilroy', sans-serif;
+ color: #5b6571;
+ word-break: break-all;
+}
+
+.vr-doc-utility-link {
+ color: #5b5bb3;
+ text-decoration: underline;
+}
+
+.vr-doc-utility-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
+}
+
+.vr-doc-utility-btn {
+ appearance: none;
+ outline: none;
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ border: 1px solid rgba(169, 178, 187, 0.33);
+ border-radius: 8px;
+ background: #ffffff;
+ cursor: pointer;
+ transition: background 0.15s;
+ text-decoration: none;
+}
+
+.vr-doc-utility-btn:hover {
+ background: #f3f4f6;
+}
+
+.vr-doc-utility-btn:focus-visible {
+ outline: 2px solid #5b5bb3;
+ outline-offset: -2px;
+}
+
+.vr-doc-utility-btn-boundary {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.vr-doc-utility-qr-wrapper {
+ position: relative;
+}
+
+.vr-qr-popover {
+ position: absolute;
+ top: calc(100% + 8px);
+ right: 0;
+ padding: 8px;
+ background: #ffffff;
+ border: 1px solid rgba(169, 178, 187, 0.33);
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ z-index: 10;
+}
+
+.vr-renderer-loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 250px;
+ padding: 32px;
+}
+
+.vr-renderer-frame {
+ padding: 0 32px 32px;
+}
+
+.dark-mode .vr-renderer-frame {
+ background: #1c2128;
+}
+
+.dark-mode .vr-renderer-frame iframe {
+ background: #ffffff;
+}
+
+/* Attachment tab badge */
+.vr-attachment-count {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 22px;
+ height: 22px;
+ margin-left: 8px;
+ padding: 0 6px;
+ border-radius: 50%;
+ background: #a9b2bb54;
+ font-size: 12px;
+ font-weight: 700;
+ color: #5b6571;
+}
+
+/* Attachments pane */
+.vr-attachments-pane {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+ padding: 32px;
+}
+
+.vr-attachment-tile {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 16px;
+ border: 1px solid rgba(169, 178, 187, 0.33);
+ border-radius: 8px;
+ background: #ffffff;
+ min-width: 220px;
+ flex: 1 1 calc(25% - 16px);
+ max-width: calc(33% - 11px);
+}
+
+.vr-attachment-icon {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.vr-attachment-info {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.vr-attachment-name {
+ font-family: 'Gilroy', sans-serif;
+ font-size: 14px;
+ font-weight: 700;
+ color: #1e2026;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.vr-attachment-type {
+ font-family: 'Avenir', 'Gilroy', sans-serif;
+ font-size: 12px;
+ color: #5b6571;
+}
+
+.vr-attachment-download {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ border: 1px solid rgba(169, 178, 187, 0.33);
+ border-radius: 8px;
+ background: #ffffff;
+ cursor: pointer;
+ text-decoration: none;
+}
+
+.vr-attachment-download:hover {
+ background: #f3f4f6;
+}
+
+.vr-attachment-download:focus-visible {
+ outline: 2px solid #5b5bb3;
+ outline-offset: -2px;
+}
+
+/* Dark mode β Template Renderer */
+.dark-mode .vr-template-tab {
+ background: #21262d;
+ border-color: rgba(48, 54, 61, 0.8);
+ color: #8b949e;
+}
+
+.dark-mode .vr-template-tab:hover {
+ color: #e6edf3;
+}
+
+.dark-mode .vr-template-tab--active {
+ background: #1c2128;
+ color: #e6edf3;
+ border-color: rgba(48, 54, 61, 0.8);
+ border-bottom-color: #1c2128;
+}
+
+.dark-mode .vr-renderer-section {
+ background: #1c2128;
+ border-color: rgba(48, 54, 61, 0.8);
+}
+
+.dark-mode .vr-doc-utility {
+ background: #1c2128;
+}
+
+.dark-mode .vr-doc-utility-label {
+ color: #e6edf3;
+}
+
+.dark-mode .vr-doc-utility-detail {
+ color: #8b949e;
+}
+
+.dark-mode .vr-doc-utility-link {
+ color: #7d80d7;
+}
+
+.dark-mode .vr-doc-utility-btn {
+ background: #1c2128;
+ border-color: rgba(48, 54, 61, 0.8);
+}
+
+.dark-mode .vr-doc-utility-btn:hover {
+ background: #21262d;
+}
+
+.dark-mode .vr-doc-utility-btn svg path {
+ fill: #7d80d7;
+}
+
+.dark-mode .vr-qr-popover {
+ background: #1c2128;
+ border-color: rgba(48, 54, 61, 0.8);
+}
+
+.dark-mode .vr-attachment-count {
+ background: rgba(48, 54, 61, 0.8);
+ color: #8b949e;
+}
+
+.dark-mode .vr-attachment-tile {
+ background: #1c2128;
+ border-color: rgba(48, 54, 61, 0.8);
+}
+
+.dark-mode .vr-attachment-name {
+ color: #e6edf3;
+}
+
+.dark-mode .vr-attachment-type {
+ color: #8b949e;
+}
+
+.dark-mode .vr-attachment-download {
+ background: #1c2128;
+ border-color: rgba(48, 54, 61, 0.8);
+}
+
+.dark-mode .vr-attachment-download:hover {
+ background: #21262d;
+}
+
+.dark-mode .vr-renderer-loading {
+ background: #1c2128;
+}
+
+.dark-mode .vr-attachments-pane {
+ background: #1c2128;
+}
+
+/* Endorsement Chain Styles */
+
+.endorsement-chain {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ max-width: 1280px;
+ min-width: 308px;
+ max-height: 100%;
+ margin: 0 auto;
+ border-radius: 16px;
+ opacity: 1;
+ border: 1px solid var(--Neutral-33-90, #a9b2bb54);
+ box-shadow: 0px 8px 32px 0px var(--Primary3360);
+ background:
+ linear-gradient(
+ 0deg,
+ var(--Base-66-Base-L1, rgba(255, 255, 255, 0.66)),
+ var(--Base-66-Base-L1, rgba(255, 255, 255, 0.66))
+ ),
+ linear-gradient(0deg, rgba(222, 228, 233, 0), rgba(222, 228, 233, 0));
+
+ transition: max-height 0.3s ease-in-out;
+}
+
+.endorsement-chain.is-loading {
+ max-height: 333px;
+}
+
+.dark-mode .endorsement-chain {
+ background:
+ linear-gradient(0deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)),
+ linear-gradient(
+ 0deg,
+ var(--Base-66-Base-L2, rgba(30, 32, 38, 0.66)),
+ var(--Base-66-Base-L2, rgba(30, 32, 38, 0.66))
+ );
+ border: 1px solid var(--Neutral-33-90, #3d444d54);
+ box-shadow: 0px 8px 32px 0px var(--Primary3360);
+}
+
+/* Header Section */
+.endorsement-chain .header-section {
+ width: 100%;
+ max-width: 1280px;
+ max-height: 72px;
+ padding: 24px 24px 16px 24px;
+ gap: 16px;
+ opacity: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-start;
+}
+
+.endorsement-chain .text-header1 {
+ font-weight: 700;
+ font-size: 18px;
+ line-height: 136%;
+ letter-spacing: 0%;
+ vertical-align: middle;
+ color: #1e2026;
+}
+.dark-mode .endorsement-chain .text-header1 {
+ color: #dee4e9;
+}
+.endorsement-chain .subheader {
+ font-family: 'Avenir', sans-serif;
+ font-weight: 500;
+ font-size: 12px;
+ line-height: 165%;
+ letter-spacing: 0%;
+ vertical-align: middle;
+ color: #60636b;
+ min-height: 20px;
+}
+.ec-error-message {
+ font-family: Gilroy, sans-serif;
+ font-weight: 700;
+ font-size: 18px;
+ line-height: 136%;
+ letter-spacing: 0%;
+ text-align: center;
+ color: #b83152;
+}
+
+.dark-mode .endorsement-chain .subheader {
+ color: #60636b;
+}
+.endorsement-chain .wallet-address {
+ font-family: 'Avenir', sans-serif;
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 155%;
+ letter-spacing: 0%;
+ vertical-align: middle;
+ color: #5b5bb3;
+ overflow-wrap: break-word;
+ min-height: 22px;
+ flex-shrink: 0;
+}
+
+.endorsement-chain .remarks {
+ font-family: 'Avenir', sans-serif;
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 155%;
+ letter-spacing: 0%;
+ vertical-align: middle;
+ color: #60636b;
+ overflow-wrap: break-word;
+ min-height: 22px;
+}
+.dark-mode .endorsement-chain .remarks {
+ color: #60636b;
+}
+.endorsement-chain .organization {
+ font-family: 'Avenir', sans-serif;
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 155%;
+ letter-spacing: 0%;
+ vertical-align: middle;
+ color: #1e2026;
+ min-height: 22px;
+ flex-shrink: 0;
+}
+.dark-mode .endorsement-chain .organization {
+ color: #dee4e9;
+}
+.ec-spinner {
+ height: 144.48px;
+}
+.endorsement-chain .date-frame {
+ font-family: 'Avenir', sans-serif;
+ height: 36px;
+ gap: 10px;
+ padding: 8px;
+ opacity: 1;
+ margin-top: 2px;
+ font-weight: 500;
+ font-size: 12px;
+ line-height: 165%;
+ letter-spacing: 0%;
+ vertical-align: middle;
+ color: #60636b;
+}
+
+.ec-header-content {
+ width: 100%;
+ max-width: 1188px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.ec-header-title {
+ flex: 1;
+ margin: 0;
+ font-family: 'Gilroy', sans-serif;
+ font-weight: 700;
+ font-size: 24px;
+ line-height: 133%;
+ letter-spacing: 0%;
+ color: #1e2026;
+}
+
+.dark-mode .ec-header-title {
+ color: #e6edf3;
+}
+
+/* Content Section */
+.endorsement-chain .content-section {
+ width: 100%;
+ height: 100%;
+ max-width: 1280px;
+ max-height: 562px;
+ padding: 16px;
+ opacity: 1;
+ border-top: 1px solid rgba(169, 178, 187, 0.33);
+ border-bottom: 1px solid rgba(169, 178, 187, 0.33);
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-start;
+ overflow-y: auto;
+}
+
+.dark-mode .endorsement-chain .content-section {
+ border-top: 1px solid rgba(169, 178, 187, 0.33);
+ border-bottom: 1px solid rgba(169, 178, 187, 0.33);
+}
+
+/* Section Frame */
+.section-frame {
+ width: 100%;
+ height: 100%;
+ max-width: 1248px;
+ opacity: 1;
+}
+
+/* Footer Section */
+.endorsement-chain .footer-section {
+ width: 100%;
+ max-width: 1280px;
+ height: 80px;
+ padding: 16px 24px 24px 24px;
+ gap: 16px;
+ opacity: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-start;
+}
+.endorsement-chain .footer-subsection {
+ width: 100%;
+ max-width: 1232px;
+ height: 40px;
+ gap: 8px;
+ opacity: 1;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+}
+.endorsement-chain .dismiss-btn {
+ width: 100%;
+ height: 40px;
+ min-width: 160px;
+ max-width: 260px;
+ gap: 10px;
+}
+/* Entity */
+.entity {
+ width: 100%;
+ max-width: 1248px;
+ min-width: 280px;
+ padding-right: 16px;
+ padding-left: 16px;
+ display: flex;
+ opacity: 1;
+}
+
+.dark-mode .entity {
+ border-top-color: rgba(169, 178, 187, 0.33);
+ border-bottom-color: rgba(169, 178, 187, 0.33);
+}
+
+/* Entity Content Frame */
+.entity-content-frame {
+ width: 100%;
+ height: 100%;
+ max-width: 1192px;
+ opacity: 1;
+}
+
+/* Entity Content */
+.entity-content-frame .content {
+ width: 100%;
+ height: 100%;
+ max-width: 1192px;
+ min-width: 240px;
+ border-radius: 12px;
+ padding-top: 8px;
+ padding-bottom: 8px;
+ opacity: 1;
+}
+
+/* Entry Header */
+.entry-header {
+ min-height: 40px;
+ opacity: 1;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
+
+@media (max-width: 544px) {
+ .entry-header {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+}
+/* Action Header Frame */
+.action-header-frame {
+ min-width: 240px;
+ min-height: 40px;
+ padding: 8px;
+ opacity: 1;
+}
+
+/* Entity Content Body */
+.entity-content-body {
+ width: 100%;
+ height: 100%;
+ max-width: 1192px;
+ opacity: 1;
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ grid-template-rows: auto auto auto;
+ column-gap: 8px;
+ row-gap: 4px;
+ align-items: start;
+}
+
+/* Column */
+.entity-content-body .column {
+ min-width: 240px;
+ border-radius: 8px;
+ padding: 8px;
+ opacity: 1;
+ display: grid;
+ grid-template-rows: subgrid;
+ grid-row: span 3;
+ row-gap: 4px;
+}
+.entity-content-body .column.column-2-items {
+ display: flex;
+ flex-direction: column;
+}
+
+@media (max-width: 864px) {
+ .entity-content-body {
+ grid-template-columns: 1fr;
+ grid-template-rows: auto;
+ }
+ .entity-content-body .column {
+ grid-row: auto;
+ display: flex;
+ flex-direction: column;
+ }
+ .action-header-frame {
+ min-width: auto;
+ flex: 1;
+ }
+}
+
+/* Chronology Chain */
+.chronology-chain {
+ width: 24px;
+ height: 160px;
+ padding-right: 8px;
+ padding-left: 8px;
+ gap: 0;
+}
+
+/* Line Design Component */
+
+.line-design-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 24px;
+ padding-right: 8px;
+ padding-left: 8px;
+ gap: 0;
+ opacity: 1;
+}
+
+.line-design-path {
+ height: 100%;
+ width: 0px;
+ border-left-width: 2px;
+ border-left-style: dotted;
+ border-left-color: #7d80d7;
+}
+
+.line-design-path.short {
+ margin-top: 2px;
+ height: 16px;
+}
+.line-design-path.first {
+ border-color: transparent;
+}
+.line-design-path.last {
+ border-color: transparent;
+}
+
+.line-design-dot {
+ display: flex;
+ flex-direction: column;
+ width: 8px;
+ height: 24px;
+ padding-top: 8px;
+ padding-bottom: 8px;
+ margin-top: 2px;
+ gap: 10px;
+ opacity: 1;
+}
+
+.line-design-dot .dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background-color: #7d80d7;
+}
+
+.divider {
+ border-bottom: 1px solid rgba(169, 178, 187, 0.33);
+}
+
+/* Overlay Component Styles */
+.overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 48px;
+ gap: 10px;
+ background: var(--neutral-66120-white, #ffffffa8);
+ border: 1px solid #a9b2bb54;
+ box-shadow: 0px 8px 32px 0px rgba(125, 128, 215, 0.6);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ z-index: 1000;
+ opacity: 1;
+}
+
+.dark-mode .overlay {
+ background: var(--neutral-66120-white, #000000a8);
+ border: 1px solid #3d444d54;
+ box-shadow: 0px 8px 32px 0px rgba(125, 128, 215, 0.6);
+}
+
+.overlay-content {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ align-content: center;
}
diff --git a/src/main.tsx b/src/main.tsx
index 6fe7008..f1132e1 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,7 +1,8 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
-import App from './App'
import './index.css'
+import App from './App'
+import { BrowserRouter } from 'react-router-dom'
const rootElement = document.getElementById('root')
@@ -11,6 +12,8 @@ if (!rootElement) {
ReactDOM.createRoot(rootElement).render(
-
+
+
+
)
diff --git a/src/pages/Contact/index.tsx b/src/pages/Contact/index.tsx
new file mode 100644
index 0000000..d318f00
--- /dev/null
+++ b/src/pages/Contact/index.tsx
@@ -0,0 +1,202 @@
+import { useRef } from 'react'
+import { useContactForm } from '@/hooks/useContactForm'
+
+import AttachmentDropzone from '@/components/common/AttachmentDropzone'
+import { AttachmentFileList } from '@/components/common/AttachmentFileList'
+import { FieldError } from '@/components/common/FieldError'
+import FormAlert from '@/components/common/FormAlert'
+import { Recaptcha, type RecaptchaHandle } from '@/components/common/Recaptcha'
+import SelectField from '@/components/common/SelectField'
+import SubmitButton from '@/components/common/SubmitButton'
+import TextAreaField from '@/components/common/TextAreaField'
+import TextField from '@/components/common/TextField'
+
+const RECAPTCHA_SITE_KEY = import.meta.env?.VITE_RECAPTCHA_SITE_KEY as
+ | string
+ | undefined
+
+interface ContactProps {
+ isDarkMode: boolean
+}
+
+const Contact = ({ isDarkMode }: ContactProps) => {
+ const recaptchaRef = useRef(null)
+ const {
+ email,
+ setEmail,
+ typeOfEnquiry,
+ setTypeOfEnquiry,
+ description,
+ setDescription,
+ attachments,
+ removeAttachment,
+ clearAllAttachments,
+ dragActive,
+ isSubmitting,
+ submitError,
+ submitSuccess,
+ fieldErrors,
+ fileInfoText,
+ validateEmail,
+ validateTypeOfEnquiry,
+ validateDescription,
+ handleDrag,
+ handleDrop,
+ handleFileInput,
+ clearRecaptchaError,
+ onSubmit,
+ } = useContactForm({
+ getRecaptchaToken: () =>
+ RECAPTCHA_SITE_KEY
+ ? (recaptchaRef.current?.getToken() ?? Promise.resolve(''))
+ : Promise.resolve(''),
+ resetRecaptcha: () => recaptchaRef.current?.reset(),
+ recaptchaRequired: true,
+ })
+
+ return (
+
+
+
+
+ Contact {' '}
+ Us
+
+
+ Get help with TrustVC product and services. We'll get back to
+ you soon
+
+
+
+
+
+
+
+
+ Submit a Request
+
+
+ Encountering some issues? Let us know so that we can help.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Contact
diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx
new file mode 100644
index 0000000..123f6ab
--- /dev/null
+++ b/src/pages/Home/index.tsx
@@ -0,0 +1,23 @@
+import HeroSection from '../../components/home/HeroSection'
+import VerifySection from '../../components/home/VerifySection'
+import Carousel from '../../components/home/Carousel'
+import BuiltForDev from '../../components/home/BuiltForDev'
+
+interface HomeProps {
+ isDarkMode: boolean
+}
+
+const Home = ({ isDarkMode }: HomeProps) => {
+ return (
+
+ )
+}
+
+export default Home
diff --git a/src/pages/NotFound/NotFound.test.tsx b/src/pages/NotFound/NotFound.test.tsx
new file mode 100644
index 0000000..07dff61
--- /dev/null
+++ b/src/pages/NotFound/NotFound.test.tsx
@@ -0,0 +1,15 @@
+import { render, screen } from '@testing-library/react'
+import { describe, it, expect } from 'vitest'
+import NotFound from './index'
+
+describe('NotFound page', () => {
+ it('renders 404 message and CTA', () => {
+ render( )
+
+ expect(screen.getByText(/Page not found/i)).toBeInTheDocument()
+ expect(screen.getByRole('link', { name: /Back to Home/i })).toHaveAttribute(
+ 'href',
+ '/'
+ )
+ })
+})
diff --git a/src/pages/NotFound/index.tsx b/src/pages/NotFound/index.tsx
new file mode 100644
index 0000000..80b4d12
--- /dev/null
+++ b/src/pages/NotFound/index.tsx
@@ -0,0 +1,34 @@
+interface NotFoundProps {
+ isDarkMode: boolean
+}
+
+const NotFound = ({ isDarkMode }: NotFoundProps) => {
+ return (
+
+
+
+ Page not found
+
+
+ The page you are looking for doesnβt exist or has been moved.
+
+
+ Back to Home
+
+
+
+ )
+}
+
+export default NotFound
diff --git a/src/routes.tsx b/src/routes.tsx
new file mode 100644
index 0000000..2c59708
--- /dev/null
+++ b/src/routes.tsx
@@ -0,0 +1,20 @@
+import { Routes, Route } from 'react-router-dom'
+import Home from './pages/Home'
+import Contact from './pages/Contact'
+import NotFound from './pages/NotFound'
+
+interface AppRouterProps {
+ isDarkMode: boolean
+}
+
+const AppRouter = ({ isDarkMode }: AppRouterProps) => {
+ return (
+
+ } />
+ } />
+ } />
+
+ )
+}
+
+export default AppRouter
diff --git a/src/setupTests.ts b/src/setupTests.ts
new file mode 100644
index 0000000..f01f23f
--- /dev/null
+++ b/src/setupTests.ts
@@ -0,0 +1,49 @@
+import { expect, afterEach, vi } from 'vitest'
+import { cleanup } from '@testing-library/react'
+import * as matchers from '@testing-library/jest-dom/matchers'
+
+// Extend Vitest's expect with jest-dom matchers
+expect.extend(matchers)
+
+// Mock Swiper CSS imports to avoid jsdom parse errors during tests
+vi.mock('swiper/css', () => ({}))
+vi.mock('swiper/css/pagination', () => ({}))
+vi.mock('swiper/css/navigation', () => ({}))
+
+// Cleanup after each test
+afterEach(() => {
+ cleanup()
+ // Reset localStorage mock to prevent state leakage between tests
+ Object.keys(localStorageMock).forEach(key => {
+ delete localStorageMock[key]
+ })
+})
+
+// Mock localStorage
+const localStorageMock: Record = {}
+
+const storage: Storage = {
+ getItem: (key: string): string | null => {
+ return key in localStorageMock ? localStorageMock[key] : null
+ },
+ setItem: (key: string, value: string): void => {
+ localStorageMock[key] = value
+ },
+ removeItem: (key: string): void => {
+ delete localStorageMock[key]
+ },
+ clear: (): void => {
+ Object.keys(localStorageMock).forEach(key => {
+ delete localStorageMock[key]
+ })
+ },
+ key: (index: number): string | null => {
+ const keys = Object.keys(localStorageMock)
+ return keys[index] || null
+ },
+ get length(): number {
+ return Object.keys(localStorageMock).length
+ },
+}
+
+globalThis.localStorage = storage
diff --git a/src/shims/dotenv-config.js b/src/shims/dotenv-config.js
new file mode 100644
index 0000000..647a960
--- /dev/null
+++ b/src/shims/dotenv-config.js
@@ -0,0 +1 @@
+// Browser shim β dotenv is Node.js-only, no-op in browser builds
diff --git a/src/shims/node-fetch.js b/src/shims/node-fetch.js
new file mode 100644
index 0000000..fd674fb
--- /dev/null
+++ b/src/shims/node-fetch.js
@@ -0,0 +1,29 @@
+// Browser shim for node-fetch.
+// node-fetch's XHR-based polyfill chain fails in browser context with a body
+// parsing error ("[object Object]"). We replace it with the browser's native fetch
+// which handles responses correctly. For the opencerts registry specifically,
+// we fall back to an empty registry if the native fetch fails (e.g. CORS),
+// so the verifier returns SKIPPED instead of throwing.
+
+const nativeFetch = globalThis.fetch.bind(globalThis)
+
+const nodeFetchShim = async (url, options) => {
+ if (String(url).includes('opencerts.io/static/registry.json')) {
+ try {
+ const res = await nativeFetch(url, options)
+ if (res.ok) return res
+ } catch {
+ // CORS or network error β fall through to empty registry
+ }
+ return new globalThis.Response('{"issuers":{}}', {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ })
+ }
+ return nativeFetch(url, options)
+}
+
+export default nodeFetchShim
+export const Headers = globalThis.Headers
+export const Request = globalThis.Request
+export const Response = globalThis.Response
diff --git a/src/test/setup.js b/src/test/setup.js
deleted file mode 100644
index deb5f8f..0000000
--- a/src/test/setup.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { afterEach } from 'vitest'
-import { cleanup } from '@testing-library/react'
-import '@testing-library/jest-dom/vitest'
-
-// Cleanup after each test case
-afterEach(() => {
- cleanup()
-})
diff --git a/src/types/attachment.ts b/src/types/attachment.ts
new file mode 100644
index 0000000..a58119b
--- /dev/null
+++ b/src/types/attachment.ts
@@ -0,0 +1,26 @@
+export type AttachmentFileStatus =
+ | 'pending'
+ | 'uploading'
+ | 'uploaded'
+ | 'error'
+
+export interface AttachmentItem {
+ id: string
+ file: File
+ key?: string
+ filename: string
+ status: AttachmentFileStatus
+ progress: number
+ error?: string
+ previewUrl?: string
+}
+
+export function truncateFilename(name: string, maxLength = 32): string {
+ if (name.length <= maxLength) return name
+ const dotIndex = name.lastIndexOf('.')
+ const hasExtension = dotIndex > 0 && dotIndex < name.length - 1
+ const ext = hasExtension ? name.slice(dotIndex) : ''
+ const base = hasExtension ? name.slice(0, dotIndex) : name
+ const keep = maxLength - ext.length - 3
+ return base.slice(0, Math.max(0, keep)) + '...' + ext
+}
diff --git a/src/utils/attachmentConfig.ts b/src/utils/attachmentConfig.ts
new file mode 100644
index 0000000..028d49e
--- /dev/null
+++ b/src/utils/attachmentConfig.ts
@@ -0,0 +1,27 @@
+export const ATTACHMENT_CONFIG = {
+ maxTotalBytes: 10 * 1024 * 1024,
+ maxFiles: 10,
+ allowedTypes: ['image/jpeg', 'image/jpg', 'image/png'] as const,
+ allowedExtensions: ['.jpg', '.jpeg', '.png'] as const,
+}
+
+export const MAX_TOTAL_UPLOAD_BYTES = ATTACHMENT_CONFIG.maxTotalBytes
+export const MAX_FILES = ATTACHMENT_CONFIG.maxFiles
+
+export function isValidFileType(file: File): boolean {
+ const lowerName = file.name.toLowerCase()
+ const dotIndex = lowerName.lastIndexOf('.')
+ const ext = dotIndex >= 0 ? lowerName.slice(dotIndex) : ''
+ const normalizedType = file.type.toLowerCase()
+ const typeAllowed = ATTACHMENT_CONFIG.allowedTypes.some(
+ allowed => allowed === normalizedType
+ )
+ const extensionAllowed = ATTACHMENT_CONFIG.allowedExtensions.some(
+ allowed => allowed === ext
+ )
+ return typeAllowed && extensionAllowed
+}
+
+export function getFileConstraintText(): string {
+ return 'Maximum 10 MB total size. Supported files include .JPG or .PNG only.'
+}
diff --git a/src/utils/fetchClient.ts b/src/utils/fetchClient.ts
new file mode 100644
index 0000000..9b5479e
--- /dev/null
+++ b/src/utils/fetchClient.ts
@@ -0,0 +1,84 @@
+type FetchClientOptions = {
+ baseUrl: string
+ timeoutMs?: number
+}
+
+interface ApiErrorBody {
+ success?: boolean
+ error?: { message?: string }
+ message?: string
+}
+
+export type FetchClientErrorDetails = {
+ status: number
+ message: string
+ data?: unknown
+}
+
+export class FetchClientError extends Error {
+ public readonly status: number
+ public readonly data?: unknown
+
+ constructor(details: FetchClientErrorDetails) {
+ super(details.message)
+ this.name = 'FetchClientError'
+ this.status = details.status
+ this.data = details.data
+ }
+}
+
+export const createFetchClient = ({
+ baseUrl,
+ timeoutMs,
+}: FetchClientOptions) => {
+ const normalizedBaseUrl = baseUrl.replace(/\/$/, '')
+ const defaultTimeoutMs = timeoutMs ?? 15000
+
+ const request = async (path: string, init?: RequestInit): Promise => {
+ const controller = new AbortController()
+ const timeoutMs = defaultTimeoutMs
+ const timeoutId = globalThis.setTimeout(() => controller.abort(), timeoutMs)
+ const signal = init?.signal
+
+ if (signal) {
+ if (signal.aborted) controller.abort()
+ signal.addEventListener('abort', () => controller.abort(), { once: true })
+ }
+
+ const response = await fetch(`${normalizedBaseUrl}${path}`, {
+ ...init,
+ signal: controller.signal,
+ }).finally(() => {
+ globalThis.clearTimeout(timeoutId)
+ })
+
+ const contentType = response.headers.get('content-type') || ''
+ let data: unknown = null
+ if (contentType.includes('application/json')) {
+ try {
+ data = await response.json()
+ } catch {
+ data = null
+ }
+ }
+
+ const errorBody =
+ typeof data === 'object' && data !== null ? (data as ApiErrorBody) : null
+ if (!response.ok || (errorBody && errorBody.success === false)) {
+ const message =
+ errorBody?.error?.message ||
+ errorBody?.message ||
+ `Request failed with status ${response.status}`
+ throw new FetchClientError({ status: response.status, message, data })
+ }
+
+ return data as T
+ }
+
+ return { request }
+}
+
+export const fetchClientSupport = createFetchClient({
+ baseUrl:
+ (import.meta.env?.VITE_SUPPORT_API_BASE_URL as string | undefined) || '',
+})
diff --git a/src/utils/helper.test.ts b/src/utils/helper.test.ts
new file mode 100644
index 0000000..cbe2e21
--- /dev/null
+++ b/src/utils/helper.test.ts
@@ -0,0 +1,343 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import {
+ getRpcUrl,
+ toErrorMessage,
+ formatAddress,
+ getTemplateSourceUrl,
+ getOpenAttestationData,
+ getQRCodeLink,
+ getAttachments,
+ formatFileSize,
+ getFileExtension,
+} from './helper'
+
+// Mock @trustvc/trustvc
+vi.mock('@trustvc/trustvc', () => ({
+ SUPPORTED_CHAINS: {
+ '1': { rpcUrl: 'https://mainnet.infura.io' },
+ '137': { rpcUrl: 'https://polygon-rpc.com' },
+ '999': { rpcUrl: 'https://undefined-key.io/undefined' },
+ },
+ vc: {
+ isSignedDocument: vi.fn(() => false),
+ isRawDocument: vi.fn(() => false),
+ },
+ getDocumentData: vi.fn((doc: any) => doc?.data ?? doc),
+ getDataV2: vi.fn((doc: any) => doc?.data ?? doc),
+ isWrappedV2Document: vi.fn(() => false),
+ isWrappedV3Document: vi.fn(() => false),
+ isRawV2Document: vi.fn(() => false),
+ isRawV3Document: vi.fn(() => false),
+}))
+
+const trustvc = await import('@trustvc/trustvc')
+
+describe('getRpcUrl', () => {
+ it('returns env variable if set', () => {
+ const originalEnv = import.meta.env.VITE_RPC_URL_1
+ import.meta.env.VITE_RPC_URL_1 = 'https://custom-rpc.io'
+ expect(getRpcUrl('1')).toBe('https://custom-rpc.io')
+ if (originalEnv) {
+ import.meta.env.VITE_RPC_URL_1 = originalEnv
+ } else {
+ delete import.meta.env.VITE_RPC_URL_1
+ }
+ })
+
+ it('returns chain default URL from SUPPORTED_CHAINS', () => {
+ const result = getRpcUrl('137')
+ expect(typeof result).toBe('string')
+ expect(result).toBeTruthy()
+ })
+
+ it('returns null for unrecognised chain', () => {
+ expect(getRpcUrl('99999')).toBeNull()
+ })
+})
+
+describe('toErrorMessage', () => {
+ it('returns specific message for SyntaxError', () => {
+ expect(toErrorMessage(new SyntaxError('bad json'))).toBe(
+ 'Invalid file format. Please upload a valid TrustVC document.'
+ )
+ })
+
+ it('returns error message for generic Error', () => {
+ expect(toErrorMessage(new Error('something broke'))).toBe('something broke')
+ })
+
+ it('returns fallback for non-Error values', () => {
+ expect(toErrorMessage('string error')).toBe(
+ 'Verification failed. Please try again.'
+ )
+ })
+
+ it('returns custom fallback', () => {
+ expect(toErrorMessage(42, 'Custom fallback')).toBe('Custom fallback')
+ })
+})
+
+describe('formatAddress', () => {
+ it('truncates a long address', () => {
+ expect(formatAddress('0x28F7aB32C521D13F2E6980d072Ca7CA493020145')).toBe(
+ '0x28F7...0145'
+ )
+ })
+
+ it('returns short address as-is', () => {
+ expect(formatAddress('0x1234')).toBe('0x1234')
+ })
+
+ it('returns empty string as-is', () => {
+ expect(formatAddress('')).toBe('')
+ })
+
+ it('supports custom prefix/suffix lengths', () => {
+ expect(
+ formatAddress('0x28F7aB32C521D13F2E6980d072Ca7CA493020145', 10, 6)
+ ).toBe('0x28F7aB32...020145')
+ })
+})
+
+describe('formatFileSize', () => {
+ it('returns empty string for empty data', () => {
+ expect(formatFileSize('')).toBe('')
+ })
+
+ it('returns bytes for small data', () => {
+ // 4 base64 chars = 3 bytes
+ expect(formatFileSize('AAAA')).toBe('3 B')
+ })
+
+ it('returns KB for medium data', () => {
+ // ~1400 base64 chars β 1050 bytes β 1.0 KB
+ const data = 'A'.repeat(1400)
+ expect(formatFileSize(data)).toBe('1.0 KB')
+ })
+
+ it('returns MB for large data', () => {
+ // ~1,400,000 base64 chars β 1,050,000 bytes β 1.0 MB
+ const data = 'A'.repeat(1400000)
+ expect(formatFileSize(data)).toBe('1.0 MB')
+ })
+
+ it('accounts for base64 padding characters', () => {
+ // 'AA==' is 4 chars with 2 padding = 1 byte
+ expect(formatFileSize('AA==')).toBe('1 B')
+ // 'AAA=' is 4 chars with 1 padding = 2 bytes
+ expect(formatFileSize('AAA=')).toBe('2 B')
+ })
+})
+
+describe('getFileExtension', () => {
+ it('extracts extension from filename', () => {
+ expect(getFileExtension('document.pdf', '')).toBe('PDF')
+ })
+
+ it('extracts extension from filename with multiple dots', () => {
+ expect(getFileExtension('my.file.json', '')).toBe('JSON')
+ })
+
+ it('falls back to mimeType for pdf', () => {
+ expect(getFileExtension('', 'application/pdf')).toBe('PDF')
+ })
+
+ it('falls back to mimeType for png', () => {
+ expect(getFileExtension('', 'image/png')).toBe('PNG')
+ })
+
+ it('falls back to mimeType for jpeg', () => {
+ expect(getFileExtension('', 'image/jpeg')).toBe('JPG')
+ })
+
+ it('falls back to mimeType for csv', () => {
+ expect(getFileExtension('', 'text/csv')).toBe('CSV')
+ })
+
+ it('falls back to mimeType for xml', () => {
+ expect(getFileExtension('', 'application/xml')).toBe('XML')
+ })
+
+ it('falls back to mimeType for plain text', () => {
+ expect(getFileExtension('', 'text/plain')).toBe('TXT')
+ })
+
+ it('returns FILE for unknown type', () => {
+ expect(getFileExtension('', 'application/octet-stream')).toBe('FILE')
+ })
+
+ it('prefers filename extension over mimeType', () => {
+ expect(getFileExtension('file.csv', 'application/pdf')).toBe('CSV')
+ })
+})
+
+describe('getTemplateSourceUrl', () => {
+ beforeEach(() => {
+ vi.mocked(trustvc.vc.isSignedDocument).mockReturnValue(false)
+ vi.mocked(trustvc.vc.isRawDocument).mockReturnValue(false)
+ vi.mocked(trustvc.isWrappedV2Document).mockReturnValue(false)
+ vi.mocked(trustvc.isWrappedV3Document).mockReturnValue(false)
+ })
+
+ it('returns undefined for null document', () => {
+ expect(getTemplateSourceUrl(null)).toBeUndefined()
+ })
+
+ it('extracts URL from W3C VC renderMethod', () => {
+ vi.mocked(trustvc.vc.isSignedDocument).mockReturnValue(true)
+ const doc = { renderMethod: [{ id: 'https://renderer.example.com' }] }
+ expect(getTemplateSourceUrl(doc)).toBe('https://renderer.example.com')
+ })
+
+ it('extracts URL from OA V2 $template', () => {
+ vi.mocked(trustvc.isWrappedV2Document).mockReturnValue(true)
+ vi.mocked(trustvc.getDocumentData).mockReturnValue({
+ $template: { url: 'https://v2-renderer.example.com' },
+ } as any)
+ const doc = {
+ data: { $template: { url: 'https://v2-renderer.example.com' } },
+ }
+ expect(getTemplateSourceUrl(doc)).toBe('https://v2-renderer.example.com')
+ })
+
+ it('extracts URL from OA V3 metadata', () => {
+ vi.mocked(trustvc.isWrappedV3Document).mockReturnValue(true)
+ const doc = {
+ openAttestationMetadata: {
+ template: { url: 'https://v3-renderer.example.com' },
+ },
+ }
+ expect(getTemplateSourceUrl(doc)).toBe('https://v3-renderer.example.com')
+ })
+
+ it('returns undefined for unrecognised document', () => {
+ expect(getTemplateSourceUrl({ foo: 'bar' })).toBeUndefined()
+ })
+})
+
+describe('getOpenAttestationData', () => {
+ beforeEach(() => {
+ vi.mocked(trustvc.vc.isSignedDocument).mockReturnValue(false)
+ vi.mocked(trustvc.vc.isRawDocument).mockReturnValue(false)
+ })
+
+ it('returns raw document for W3C VC', () => {
+ vi.mocked(trustvc.vc.isSignedDocument).mockReturnValue(true)
+ const doc = { issuer: 'did:example:123' }
+ expect(getOpenAttestationData(doc)).toBe(doc)
+ })
+
+ it('calls getDocumentData for OA documents', () => {
+ const doc = { data: { name: 'test' } }
+ vi.mocked(trustvc.getDocumentData).mockReturnValue({ name: 'test' } as any)
+ expect(getOpenAttestationData(doc)).toEqual({ name: 'test' })
+ })
+})
+
+describe('getQRCodeLink', () => {
+ beforeEach(() => {
+ vi.mocked(trustvc.vc.isSignedDocument).mockReturnValue(false)
+ vi.mocked(trustvc.vc.isRawDocument).mockReturnValue(false)
+ vi.mocked(trustvc.isWrappedV2Document).mockReturnValue(false)
+ vi.mocked(trustvc.isRawV2Document).mockReturnValue(false)
+ vi.mocked(trustvc.isWrappedV3Document).mockReturnValue(false)
+ vi.mocked(trustvc.isRawV3Document).mockReturnValue(false)
+ })
+
+ it('returns undefined for null document', () => {
+ expect(getQRCodeLink(null)).toBeUndefined()
+ })
+
+ it('extracts link from OA V2 document', () => {
+ vi.mocked(trustvc.isWrappedV2Document).mockReturnValue(true)
+ vi.mocked(trustvc.getDataV2).mockReturnValue({
+ links: { self: { href: 'https://action.example.com' } },
+ } as any)
+ const doc = {}
+ expect(getQRCodeLink(doc)).toBe('https://action.example.com')
+ })
+
+ it('extracts link from OA V3 document', () => {
+ vi.mocked(trustvc.isRawV3Document).mockReturnValue(true)
+ const doc = {
+ credentialSubject: {
+ links: { self: { href: 'https://v3-action.example.com' } },
+ },
+ }
+ expect(getQRCodeLink(doc)).toBe('https://v3-action.example.com')
+ })
+
+ it('extracts URI from W3C VC qrCode', () => {
+ vi.mocked(trustvc.vc.isSignedDocument).mockReturnValue(true)
+ const doc = { qrCode: { uri: 'https://qr.example.com' } }
+ expect(getQRCodeLink(doc)).toBe('https://qr.example.com')
+ })
+
+ it('returns undefined for unrecognised document', () => {
+ expect(getQRCodeLink({ foo: 'bar' })).toBeUndefined()
+ })
+})
+
+describe('getAttachments', () => {
+ beforeEach(() => {
+ vi.mocked(trustvc.vc.isSignedDocument).mockReturnValue(false)
+ vi.mocked(trustvc.vc.isRawDocument).mockReturnValue(false)
+ vi.mocked(trustvc.isWrappedV2Document).mockReturnValue(false)
+ vi.mocked(trustvc.isWrappedV3Document).mockReturnValue(false)
+ })
+
+ it('returns empty array for null document', () => {
+ expect(getAttachments(null)).toEqual([])
+ })
+
+ it('returns empty array for unrecognised document', () => {
+ expect(getAttachments({ foo: 'bar' })).toEqual([])
+ })
+
+ it('extracts attachments from OA V2 document', () => {
+ vi.mocked(trustvc.isWrappedV2Document).mockReturnValue(true)
+ vi.mocked(trustvc.getDataV2).mockReturnValue({
+ attachments: [
+ { filename: 'test.pdf', data: 'base64data', type: 'application/pdf' },
+ ],
+ } as any)
+ const result = getAttachments({})
+ expect(result).toEqual([
+ { filename: 'test.pdf', data: 'base64data', type: 'application/pdf' },
+ ])
+ })
+
+ it('extracts and maps attachments from OA V3 document', () => {
+ vi.mocked(trustvc.isWrappedV3Document).mockReturnValue(true)
+ const doc = {
+ attachments: [
+ { fileName: 'doc.pdf', data: 'abc123', mimeType: 'application/pdf' },
+ ],
+ }
+ const result = getAttachments(doc)
+ expect(result).toEqual([
+ { filename: 'doc.pdf', data: 'abc123', type: 'application/pdf' },
+ ])
+ })
+
+ it('extracts attachments from W3C VC credentialSubject', () => {
+ vi.mocked(trustvc.vc.isSignedDocument).mockReturnValue(true)
+ const doc = {
+ credentialSubject: {
+ attachments: [
+ { filename: 'file.png', data: 'imgdata', mimeType: 'image/png' },
+ ],
+ },
+ }
+ const result = getAttachments(doc)
+ expect(result).toEqual([
+ { filename: 'file.png', data: 'imgdata', type: 'image/png' },
+ ])
+ })
+
+ it('returns empty array for V2 document without attachments', () => {
+ vi.mocked(trustvc.isWrappedV2Document).mockReturnValue(true)
+ vi.mocked(trustvc.getDataV2).mockReturnValue({} as any)
+ expect(getAttachments({})).toEqual([])
+ })
+})
diff --git a/src/utils/helper.ts b/src/utils/helper.ts
new file mode 100644
index 0000000..11ea788
--- /dev/null
+++ b/src/utils/helper.ts
@@ -0,0 +1,172 @@
+import {
+ SUPPORTED_CHAINS,
+ getDocumentData,
+ getDataV2,
+ isWrappedV2Document,
+ isWrappedV3Document,
+ isRawV2Document,
+ isRawV3Document,
+ vc,
+ SignedVerifiableCredential,
+} from '@trustvc/trustvc'
+
+export const getRpcUrl = (chainId: string): string | null => {
+ const chainEnvUrl = import.meta.env[`VITE_RPC_URL_${chainId}`]
+ if (chainEnvUrl) return chainEnvUrl
+
+ const chainDefaultUrl =
+ SUPPORTED_CHAINS[chainId as keyof typeof SUPPORTED_CHAINS]?.rpcUrl
+ const safeChainUrl = chainDefaultUrl?.includes('undefined')
+ ? null
+ : chainDefaultUrl
+ if (safeChainUrl) return safeChainUrl
+
+ // Chain not recognised β return null to surface the issue
+ return null
+}
+
+/**
+ * Converts an unknown error to a user-friendly error message string
+ * @param err - The error object (unknown type)
+ * @param fallback - Default message if error cannot be parsed
+ * @returns A user-friendly error message string
+ */
+export const toErrorMessage = (
+ err: unknown,
+ fallback = 'Verification failed. Please try again.'
+): string => {
+ if (err instanceof SyntaxError) {
+ return 'Invalid file format. Please upload a valid TrustVC document.'
+ }
+ if (err instanceof Error) {
+ return err.message
+ }
+ return fallback
+}
+
+/**
+ * Formats an Ethereum address for display (shows first and last characters)
+ * @param address - The full Ethereum address
+ * @param prefixLength - Number of characters to show at start (default: 6)
+ * @param suffixLength - Number of characters to show at end (default: 4)
+ * @returns Formatted address string
+ */
+export const formatAddress = (
+ address: string,
+ prefixLength = 6,
+ suffixLength = 4
+): string => {
+ if (!address || address.length < prefixLength + suffixLength) {
+ return address
+ }
+ return `${address.substring(0, prefixLength)}...${address.substring(address.length - suffixLength)}`
+}
+
+// ββ Document helper types ββ
+
+export interface DocumentAttachment {
+ filename: string
+ data: string
+ type: string
+}
+
+// ββ Document helper functions ββ
+
+export const getTemplateSourceUrl = (rawDocument: any): string | undefined => {
+ if (vc.isSignedDocument(rawDocument) || vc.isRawDocument(rawDocument)) {
+ return [rawDocument.renderMethod]?.flat()?.[0]?.id
+ } else if (isWrappedV2Document(rawDocument)) {
+ const documentData = getDocumentData(rawDocument)
+ return typeof (documentData as any)?.$template === 'object'
+ ? (documentData as any).$template.url
+ : undefined
+ } else if (isWrappedV3Document(rawDocument)) {
+ return (rawDocument as any).openAttestationMetadata?.template?.url
+ }
+ return undefined
+}
+
+export const getOpenAttestationData = (rawDocument: any): any => {
+ if (vc.isSignedDocument(rawDocument) || vc.isRawDocument(rawDocument)) {
+ return rawDocument
+ }
+ return getDocumentData(rawDocument)
+}
+
+export const getQRCodeLink = (rawDocument: any): string | undefined => {
+ if (isRawV2Document(rawDocument) || isWrappedV2Document(rawDocument)) {
+ const data = isWrappedV2Document(rawDocument)
+ ? getDataV2(rawDocument)
+ : rawDocument
+ return (data as any)?.links?.self?.href
+ } else if (isRawV3Document(rawDocument) || isWrappedV3Document(rawDocument)) {
+ const data = rawDocument?.credentialSubject ?? rawDocument
+ return (data as any)?.links?.self?.href
+ } else if (
+ vc.isSignedDocument(rawDocument) ||
+ vc.isRawDocument(rawDocument)
+ ) {
+ return (rawDocument as SignedVerifiableCredential)?.qrCode?.uri
+ }
+ return undefined
+}
+
+export const getAttachments = (rawDocument: any): DocumentAttachment[] => {
+ if (!rawDocument) return []
+
+ if (isWrappedV2Document(rawDocument)) {
+ const documentData = getDataV2(rawDocument)
+ return (documentData as any)?.attachments ?? []
+ } else if (isWrappedV3Document(rawDocument)) {
+ return (
+ (rawDocument as any)?.attachments?.map((a: any) => ({
+ data: a.data,
+ filename: a.fileName,
+ type: a.mimeType,
+ })) ?? []
+ )
+ } else if (
+ vc.isSignedDocument(rawDocument) ||
+ vc.isRawDocument(rawDocument)
+ ) {
+ return [(rawDocument as any)?.credentialSubject]
+ .flat()
+ .map((s: any) => s?.attachments)
+ .filter(Boolean)
+ .flat()
+ .map((a: any) => ({
+ data: a.data,
+ filename: a.filename,
+ type: a.mimeType,
+ }))
+ }
+ return []
+}
+
+export const formatFileSize = (base64Data: string): string => {
+ if (!base64Data) return ''
+ const padding = (base64Data.match(/=+$/) || [''])[0].length
+ const bytes = Math.ceil((base64Data.length * 3) / 4) - padding
+ if (bytes < 1024) return `${bytes} B`
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
+}
+
+export const getFileExtension = (
+ filename: string,
+ mimeType: string
+): string => {
+ if (filename) {
+ const ext = filename.split('.').pop()?.toUpperCase()
+ if (ext && ext !== filename.toUpperCase()) return ext
+ }
+ if (mimeType.includes('pdf')) return 'PDF'
+ if (mimeType.includes('png')) return 'PNG'
+ if (mimeType.includes('jpeg') || mimeType.includes('jpg')) return 'JPG'
+ if (mimeType.includes('json') || mimeType.includes('openattestation'))
+ return 'JSON'
+ if (mimeType.includes('csv')) return 'CSV'
+ if (mimeType.includes('xml')) return 'XML'
+ if (mimeType.includes('text')) return 'TXT'
+ return 'FILE'
+}
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644
index 0000000..baed44a
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,15 @@
+export {
+ fetchClientSupport,
+ createFetchClient,
+ FetchClientError,
+} from './fetchClient'
+export { createServiceRequest } from './serviceRequest'
+export {
+ getPresignedUrls,
+ uploadToPresignedUrl,
+ createServiceRequestWithKeys,
+} from './upload'
+export type {
+ PresignUploadItem,
+ CreateServiceRequestWithKeysPayload,
+} from './upload'
diff --git a/src/utils/serviceRequest.ts b/src/utils/serviceRequest.ts
new file mode 100644
index 0000000..cc9318c
--- /dev/null
+++ b/src/utils/serviceRequest.ts
@@ -0,0 +1,33 @@
+import { fetchClientSupport } from './fetchClient'
+
+export type CreateServiceRequestResponse = {
+ success: boolean
+ data?: {
+ message: string
+ serviceRequest?: {
+ id: string
+ issueKey: string
+ issueId: string
+ portalUrl: string
+ webUrl: string
+ }
+ attachmentsUploaded?: number
+ }
+ error?: {
+ message: string
+ code?: string
+ details?: unknown
+ }
+}
+
+export const createServiceRequest = async (
+ formData: FormData
+): Promise => {
+ return fetchClientSupport.request(
+ '/service-request',
+ {
+ method: 'POST',
+ body: formData,
+ }
+ )
+}
diff --git a/src/utils/upload.ts b/src/utils/upload.ts
new file mode 100644
index 0000000..510a535
--- /dev/null
+++ b/src/utils/upload.ts
@@ -0,0 +1,124 @@
+import { fetchClientSupport } from './fetchClient'
+
+export type PresignUploadItem = {
+ key: string
+ uploadUrl: string
+ filename: string
+ expiresIn: number
+}
+
+export type CreateServiceRequestWithKeysPayload = {
+ email: string
+ description: string
+ typeOfEnquiry: string
+ domain: string
+ attachmentKeys: { key: string; filename: string }[]
+ recaptchaToken: string
+}
+
+export type CreateServiceRequestWithKeysResponse = {
+ success: boolean
+ data?: {
+ message: string
+ serviceRequest?: {
+ id: string
+ issueKey: string
+ issueId: string
+ portalUrl: string
+ webUrl: string
+ }
+ attachmentsUploaded?: number
+ attachmentsQueued?: number
+ }
+ error?: { message: string; code?: string; details?: unknown }
+}
+
+export async function getPresignedUrls(
+ files: { filename: string; contentType: string; size?: number }[]
+): Promise {
+ const res = await fetchClientSupport.request<{
+ success?: boolean
+ data?: { uploads: PresignUploadItem[] }
+ error?: { message: string }
+ }>('/upload/presign', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ files }),
+ })
+ const uploads = res.data?.uploads
+ if (!uploads) {
+ throw new Error(res.error?.message || 'Failed to get upload URLs')
+ }
+ return uploads
+}
+
+export async function uploadToPresignedUrl(
+ uploadUrl: string,
+ file: File,
+ onProgress?: (_percent: number) => void,
+ options?: { signal?: AbortSignal; timeoutMs?: number }
+): Promise {
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest()
+ const timeoutMs = options?.timeoutMs ?? 30000
+ let settled = false
+
+ const fail = (error: Error) => {
+ if (settled) return
+ settled = true
+ reject(error)
+ }
+
+ const succeed = () => {
+ if (settled) return
+ settled = true
+ resolve()
+ }
+
+ const onAbortSignal = () => xhr.abort()
+ if (options?.signal) {
+ if (options.signal.aborted) {
+ fail(new Error('Upload aborted'))
+ return
+ }
+ options.signal.addEventListener('abort', onAbortSignal, { once: true })
+ }
+
+ xhr.upload.addEventListener('progress', e => {
+ if (e.lengthComputable && onProgress) {
+ onProgress(Math.round((e.loaded / e.total) * 100))
+ }
+ })
+ xhr.addEventListener('load', () => {
+ if (xhr.status >= 200 && xhr.status < 300) {
+ if (onProgress) onProgress(100)
+ succeed()
+ } else {
+ fail(new Error(`Upload failed: ${xhr.status}`))
+ }
+ })
+ xhr.addEventListener('error', () => fail(new Error('Upload failed')))
+ xhr.addEventListener('abort', () => fail(new Error('Upload aborted')))
+ xhr.addEventListener('timeout', () => fail(new Error('Upload timed out')))
+ xhr.open('PUT', uploadUrl)
+ xhr.timeout = timeoutMs
+ xhr.setRequestHeader(
+ 'Content-Type',
+ file.type || 'application/octet-stream'
+ )
+ xhr.send(file)
+ })
+}
+
+export async function createServiceRequestWithKeys(
+ payload: CreateServiceRequestWithKeysPayload
+): Promise {
+ return fetchClientSupport.request(
+ '/service-request',
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ }
+ )
+}
diff --git a/tailwind.config.js b/tailwind.config.js
index dca8ba0..3439401 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -5,7 +5,64 @@ export default {
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
- extend: {},
+ extend: {
+ colors: {
+ primary: {
+ 30: '#403D7D',
+ 50: '#5B5BB3',
+ 60: '#686AD2',
+ DEFAULT: '#686AD2',
+ },
+ secondary: {
+ 60: '#167EB0',
+ 100: '#8AD2EE',
+ },
+ neutral: {
+ 10: '#1E2026',
+ 20: '#3D444D',
+ 30: '#5B6571',
+ 33: '#A9B2BB54',
+ 50: '#A9B2BB',
+ 60: '#DEE4E9',
+ },
+ base: {
+ '66-l1': 'rgba(255, 255, 255, 0.66)',
+ '66-l2': 'rgba(30, 32, 38, 0.66)',
+ '100-l1': {
+ light: '#FFFFFF',
+ dark: '#000000',
+ },
+ '100-l2-c': {
+ light: 'rgba(222, 228, 233, 0.33)',
+ dark: 'rgba(30, 32, 38, 0.66)',
+ },
+ },
+ },
+ fontFamily: {
+ gilroy: ['Gilroy', 'sans-serif'],
+ avenir: ['Avenir', 'ui-sans-serif', 'system-ui'],
+ },
+ fontSize: {
+ 'submit-title': ['24px', { lineHeight: '133%', letterSpacing: '0%' }],
+ 'encountering-text': ['14px', { lineHeight: '155%', letterSpacing: '0%' }],
+ 'form-label': ['18px', { lineHeight: '136%', letterSpacing: '0%' }],
+ },
+ fontWeight: {
+ medium: '500',
+ },
+ boxShadow: {
+ 'form-card': '0px 8px 32px rgba(104, 106, 210, 0.33)',
+ 'form-card-dark': '0px 8px 32px 0px #686AD2',
+ },
+ backgroundImage: {
+ 'overlay-light': 'linear-gradient(0deg, rgba(255, 255, 255, 0.66), rgba(255, 255, 255, 0.66)), linear-gradient(0deg, rgba(222, 228, 233, 0), rgba(222, 228, 233, 0))',
+ 'overlay-dark': 'linear-gradient(0deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)), linear-gradient(0deg, rgba(30, 32, 38, 0.66), rgba(30, 32, 38, 0.66))',
+ 'form-fields-light': 'linear-gradient(0deg, #FFFFFF, #FFFFFF), linear-gradient(0deg, rgba(222, 228, 233, 0.33), rgba(222, 228, 233, 0.33))',
+ 'form-fields-dark': 'linear-gradient(0deg, #000000, #000000), linear-gradient(0deg, rgba(30, 32, 38, 0.66), rgba(30, 32, 38, 0.66))',
+ 'divider-light': 'linear-gradient(0deg, rgba(255, 255, 255, 0.66), rgba(255, 255, 255, 0.66)), linear-gradient(0deg, rgba(222, 228, 233, 0), rgba(222, 228, 233, 0))',
+ 'divider-dark': 'linear-gradient(0deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)), linear-gradient(0deg, rgba(30, 32, 38, 0.66), rgba(30, 32, 38, 0.66))',
+ },
+ },
},
plugins: [],
}
diff --git a/tsconfig.json b/tsconfig.json
index 081a1f8..2cada60 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,9 +1,19 @@
{
"compilerOptions": {
"target": "ES2020",
+ "baseUrl": ".",
+ "paths": {
+ "@/*": [
+ "src/*"
+ ]
+ },
"useDefineForClassFields": true,
"module": "ESNext",
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "lib": [
+ "ES2020",
+ "DOM",
+ "DOM.Iterable"
+ ],
"skipLibCheck": true,
"allowJs": false,
"esModuleInterop": false,
@@ -15,7 +25,13 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
- "types": ["vite/client", "vitest/globals", "@testing-library/jest-dom"]
+ "types": [
+ "vite/client",
+ "vitest/globals",
+ "@testing-library/jest-dom"
+ ]
},
- "include": ["src"]
-}
+ "include": [
+ "src"
+ ]
+}
\ No newline at end of file
diff --git a/vite.config.js b/vite.config.js
index ad7ba0f..9995d5d 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,16 +1,38 @@
+import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+import path from 'path'
+import { nodePolyfills } from 'vite-plugin-node-polyfills'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+
+export default defineConfig(({ mode }) => {
+ const isTest = mode === 'test'
-export default defineConfig(() => {
return {
- plugins: [react()],
define: {
- 'process.env': {}
+ 'process.env': {},
+ 'process.browser': true,
+ ...(isTest ? {} : { 'process.version': JSON.stringify('v16.0.0') })
+ },
+ plugins: [
+ react(),
+ nodePolyfills({
+ globals: { Buffer: true, global: true, process: true },
+ protocolImports: true,
+ }),
+ ],
+ resolve: {
+ alias: {
+ 'dotenv/config': path.resolve(__dirname, 'src/shims/dotenv-config.js'),
+ 'node-fetch': path.resolve(__dirname, 'src/shims/node-fetch.js'),
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
+ },
},
test: {
globals: true,
environment: 'jsdom',
- setupFiles: './src/test/setup.js',
+ setupFiles: './src/setupTests.ts',
css: true,
}
}