From 5269e6448d25cd5c1ec2cf3438b9f87ca4411fa7 Mon Sep 17 00:00:00 2001
From: Usman Abeeb <136495186+therealbibson@users.noreply.github.com>
Date: Sun, 27 Sep 2026 04:33:41 +0100
Subject: [PATCH] fix(components): show verification date and method tooltip on
VerifiedCreatorBadge
Closes #654
---
src/__tests__/VerifiedCreatorBadge.test.tsx | 90 +++++++++++++++++++++
src/components/VerifiedCreatorBadge.tsx | 68 ++++++++++++++--
src/lib/attestation.ts | 6 ++
3 files changed, 156 insertions(+), 8 deletions(-)
create mode 100644 src/__tests__/VerifiedCreatorBadge.test.tsx
diff --git a/src/__tests__/VerifiedCreatorBadge.test.tsx b/src/__tests__/VerifiedCreatorBadge.test.tsx
new file mode 100644
index 00000000..0e6543ee
--- /dev/null
+++ b/src/__tests__/VerifiedCreatorBadge.test.tsx
@@ -0,0 +1,90 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import VerifiedCreatorBadge, {
+ formatVerificationDate,
+} from "@/components/VerifiedCreatorBadge";
+
+const state = vi.hoisted(() => ({
+ verified: false,
+ attestation: null as null | {
+ address: string;
+ signature: string;
+ timestamp?: number;
+ method?: string;
+ },
+}));
+
+vi.mock("@/lib/attestation", () => ({
+ isVerifiedCreator: () => state.verified,
+ getAttestation: () => state.attestation,
+ getAttestations: () => ({}),
+ storeAttestation: () => {},
+ removeAttestation: () => {},
+ generateChallenge: () => "",
+}));
+
+const ADDRESS = "GDVERIFIEDCREATORADDRESS000000000000000000000000000000000";
+
+describe("VerifiedCreatorBadge", () => {
+ beforeEach(() => {
+ state.verified = false;
+ state.attestation = null;
+ });
+
+ it("renders nothing when the creator is not verified", () => {
+ const { container } = render();
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("exposes a tooltip with the verification date and method via aria-describedby", () => {
+ state.verified = true;
+ state.attestation = {
+ address: ADDRESS,
+ signature: "sig",
+ timestamp: Date.UTC(2024, 0, 15),
+ method: "on-chain proof",
+ };
+
+ render();
+
+ const badge = screen.getByText(/Verified Creator/i);
+ const tooltip = screen.getByRole("tooltip");
+
+ expect(tooltip).toHaveTextContent(
+ "Verified via on-chain proof on January 15, 2024"
+ );
+ expect(badge).toHaveAttribute("aria-describedby", tooltip.id);
+ });
+
+ it("falls back to a generic method when none is recorded", () => {
+ state.verified = true;
+ state.attestation = {
+ address: ADDRESS,
+ signature: "sig",
+ timestamp: Date.UTC(2024, 5, 1),
+ };
+
+ render();
+
+ expect(screen.getByRole("tooltip")).toHaveTextContent(
+ "Verified via signed attestation on June 1, 2024"
+ );
+ });
+
+ it("renders without a tooltip when no verification date is available", () => {
+ state.verified = true;
+ state.attestation = { address: ADDRESS, signature: "sig" };
+
+ render();
+
+ expect(screen.getByText(/Verified Creator/i)).toBeInTheDocument();
+ expect(screen.queryByRole("tooltip")).toBeNull();
+ expect(screen.getByText(/Verified Creator/i)).not.toHaveAttribute(
+ "aria-describedby"
+ );
+ });
+
+ it("formats dates in a human-readable form", () => {
+ expect(formatVerificationDate(Date.UTC(2024, 0, 15))).toBe("January 15, 2024");
+ });
+});
diff --git a/src/components/VerifiedCreatorBadge.tsx b/src/components/VerifiedCreatorBadge.tsx
index 352261e9..3f6dcf9c 100644
--- a/src/components/VerifiedCreatorBadge.tsx
+++ b/src/components/VerifiedCreatorBadge.tsx
@@ -1,31 +1,83 @@
'use client';
-import { useEffect, useState } from 'react';
-import { isVerifiedCreator } from '@/lib/attestation';
+import { useEffect, useId, useState } from 'react';
+import { getAttestation, isVerifiedCreator } from '@/lib/attestation';
interface Props {
address: string;
}
+/** Format a verification timestamp as a human-readable date, e.g. "January 15, 2024". */
+export function formatVerificationDate(timestamp: number): string {
+ return new Intl.DateTimeFormat('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ }).format(new Date(timestamp));
+}
+
export default function VerifiedCreatorBadge({ address }: Props) {
const [isVerified, setIsVerified] = useState(false);
const [mounted, setMounted] = useState(false);
+ const [verification, setVerification] = useState<{
+ timestamp: number | null;
+ method: string | null;
+ }>({ timestamp: null, method: null });
+
+ // `useId` contains colons in React 18; strip them so the id is a valid
+ // `aria-describedby` target.
+ const tooltipId = `verified-creator-tooltip-${useId().replace(/[^a-zA-Z0-9_-]/g, '')}`;
useEffect(() => {
setMounted(true);
- setIsVerified(isVerifiedCreator(address));
+ const verified = isVerifiedCreator(address);
+ setIsVerified(verified);
+
+ if (!verified) {
+ setVerification({ timestamp: null, method: null });
+ return;
+ }
+
+ const attestation = getAttestation(address);
+ setVerification({
+ timestamp: attestation?.timestamp ?? null,
+ method: attestation?.method ?? null,
+ });
}, [address]);
if (!mounted || !isVerified) {
return null;
}
+ const hasVerificationDate =
+ typeof verification.timestamp === 'number' &&
+ Number.isFinite(verification.timestamp);
+ const methodLabel = verification.method?.trim() || 'signed attestation';
+ const tooltipText = hasVerificationDate
+ ? `Verified via ${methodLabel} on ${formatVerificationDate(
+ verification.timestamp as number,
+ )}`
+ : null;
+
return (
-
- ✓ Verified Creator
+
+
+ ✓ Verified Creator
+
+
+ {tooltipText && (
+
+ {tooltipText}
+
+ )}
);
}
diff --git a/src/lib/attestation.ts b/src/lib/attestation.ts
index 45f45caa..600a0ef0 100644
--- a/src/lib/attestation.ts
+++ b/src/lib/attestation.ts
@@ -9,6 +9,12 @@ export interface CreatorAttestation {
address: string;
signature: string;
timestamp: number; // Unix timestamp in milliseconds
+ /**
+ * Optional human-readable description of how the creator was verified
+ * (e.g. "on-chain proof", "signed attestation"). Surfaced in the
+ * VerifiedCreatorBadge tooltip.
+ */
+ method?: string;
}
const STORAGE_KEY = "stellarsplit_creator_attestations";