diff --git a/frontend/__tests__/app-header.test.tsx b/frontend/__tests__/app-header.test.tsx
index e3c70b2..072e0d4 100644
--- a/frontend/__tests__/app-header.test.tsx
+++ b/frontend/__tests__/app-header.test.tsx
@@ -38,7 +38,7 @@ describe("AppHeader", () => {
render( );
- expect(screen.getByRole("link", { name: /Peer Mapper/i })).toHaveAttribute("href", "/");
+ expect(screen.getByRole("link", { name: /Trust Me Bro/i })).toHaveAttribute("href", "/");
expect(screen.getByRole("link", { name: /My QR code/i })).toHaveAttribute("href", "/scan/my-qr");
expect(screen.getByRole("link", { name: /Camera/i })).toHaveAttribute("href", "/scan/camera");
expect(screen.getByRole("link", { name: /My Circle/i })).toHaveAttribute("href", "/circle");
diff --git a/frontend/__tests__/profile-page.test.tsx b/frontend/__tests__/profile-page.test.tsx
new file mode 100644
index 0000000..5b102bf
--- /dev/null
+++ b/frontend/__tests__/profile-page.test.tsx
@@ -0,0 +1,82 @@
+import type { Session } from "@supabase/supabase-js";
+import { act, fireEvent, render, screen, within } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import ProfilePage from "../src/app/(routes)/profile/page";
+import { useUserStore } from "../src/lib/store";
+
+const { replaceMock } = vi.hoisted(() => ({
+ replaceMock: vi.fn(),
+}));
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({
+ replace: replaceMock,
+ }),
+}));
+
+describe("ProfilePage", () => {
+ beforeEach(() => {
+ replaceMock.mockReset();
+ act(() => {
+ useUserStore.getState().reset();
+ });
+
+ const session = {
+ access_token: "token",
+ user: { id: "user-1", email: "user@example.com" },
+ } as unknown as Session;
+
+ act(() => {
+ useUserStore.setState({
+ session,
+ user: {
+ user_id: session.user.id,
+ cubid_id: "cubid_me",
+ display_name: "Maple Leaf",
+ photo_url: "https://example.com/photo.png",
+ },
+ });
+ });
+ });
+
+ afterEach(() => {
+ act(() => {
+ useUserStore.getState().reset();
+ });
+ });
+
+ it("shows an on-page preview of the profile", () => {
+ render( );
+
+ expect(screen.getByText(/This name can be a nickname/i)).toBeInTheDocument();
+ expect(screen.getByText(/Maple Leaf/)).toBeInTheDocument();
+
+ const previewHeading = screen.getByRole("heading", { name: /Preview for peers/i });
+ const previewAside = previewHeading.closest("aside");
+ expect(previewAside).not.toBeNull();
+ if (previewAside) {
+ expect(within(previewAside).getByText(/Cubid ID: cubid_me/)).toBeInTheDocument();
+ }
+
+ const previewImage = screen.getByAltText(/Profile photo preview/i) as HTMLImageElement;
+ expect(previewImage.src).toContain("https://example.com/photo.png");
+ });
+
+ it("surfaces a warning when the photo URL cannot load", async () => {
+ const user = userEvent.setup();
+
+ render( );
+
+ const photoInput = screen.getByLabelText(/Photo URL/i);
+ await user.clear(photoInput);
+ await user.type(photoInput, "https://example.com/broken.png");
+
+ const previewImage = screen.getByAltText(/Profile photo preview/i);
+ fireEvent.error(previewImage);
+
+ expect(screen.getByText(/We couldn’t load this image/i)).toBeInTheDocument();
+ expect(screen.getByText("!")).toBeInTheDocument();
+ });
+});
diff --git a/frontend/__tests__/scan-camera-page.test.tsx b/frontend/__tests__/scan-camera-page.test.tsx
index 546c1d7..7c421fa 100644
--- a/frontend/__tests__/scan-camera-page.test.tsx
+++ b/frontend/__tests__/scan-camera-page.test.tsx
@@ -1,17 +1,26 @@
import type { Session } from "@supabase/supabase-js";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import CameraPage from "../src/app/(routes)/scan/camera/page";
import { useScanStore } from "../src/lib/scanStore";
import { useUserStore } from "../src/lib/store";
-const { pushMock, requestQrChallengeMock, verifyQrChallengeMock, ensureWalletMock } = vi.hoisted(() => ({
+const {
+ pushMock,
+ requestQrChallengeMock,
+ verifyQrChallengeMock,
+ ensureWalletMock,
+ getUserMediaMock,
+ notifyHandshakeCompleteMock,
+} = vi.hoisted(() => ({
pushMock: vi.fn(),
requestQrChallengeMock: vi.fn(),
verifyQrChallengeMock: vi.fn(),
ensureWalletMock: vi.fn(),
+ getUserMediaMock: vi.fn(),
+ notifyHandshakeCompleteMock: vi.fn(),
}));
vi.mock("next/navigation", () => ({
@@ -29,12 +38,28 @@ vi.mock("../src/lib/wallet", () => ({
ensureWallet: ensureWalletMock,
}));
+vi.mock("../src/lib/handshake", () => ({
+ notifyHandshakeComplete: notifyHandshakeCompleteMock,
+}));
+
+const originalPlay = HTMLMediaElement.prototype.play;
+
+beforeAll(() => {
+ HTMLMediaElement.prototype.play = vi.fn().mockResolvedValue(undefined);
+});
+
+afterAll(() => {
+ HTMLMediaElement.prototype.play = originalPlay;
+});
+
describe("CameraPage", () => {
beforeEach(() => {
pushMock.mockReset();
requestQrChallengeMock.mockReset();
verifyQrChallengeMock.mockReset();
ensureWalletMock.mockReset();
+ getUserMediaMock.mockReset();
+ notifyHandshakeCompleteMock.mockReset();
act(() => {
useUserStore.getState().reset();
useScanStore.getState().reset();
@@ -51,6 +76,15 @@ describe("CameraPage", () => {
walletAddress: null,
});
+ getUserMediaMock.mockResolvedValue({
+ getTracks: () => [{ stop: vi.fn() }],
+ } as unknown as MediaStream);
+
+ Object.defineProperty(window.navigator, "mediaDevices", {
+ configurable: true,
+ value: { getUserMedia: getUserMediaMock },
+ });
+
const ethereumRequest = vi.fn();
(window as typeof window & { ethereum?: { request: ReturnType } }).ethereum = {
request: ethereumRequest,
@@ -59,10 +93,12 @@ describe("CameraPage", () => {
afterEach(() => {
delete (window as typeof window & { ethereum?: unknown }).ethereum;
+ delete (window.navigator as Navigator & { mediaDevices?: MediaDevices }).mediaDevices;
});
it("completes the QR verification happy path", async () => {
const user = userEvent.setup();
+ const handshakeChannel = "handshake-channel";
const ethereum = (window as typeof window & {
ethereum?: { request: ReturnType };
}).ethereum;
@@ -86,12 +122,20 @@ describe("CameraPage", () => {
],
});
+ notifyHandshakeCompleteMock.mockResolvedValueOnce(undefined);
+
render( );
+ await waitFor(() => expect(getUserMediaMock).toHaveBeenCalled());
+ const devSummary = await screen.findByText(/for devs only/i);
+ await user.click(devSummary);
+
const payloadTextarea = screen.getByPlaceholderText(/Paste JSON like/);
const targetAddress = "0x000000000000000000000000000000000000dEaD";
fireEvent.change(payloadTextarea, {
- target: { value: `{"cubidId":"cubid_peer","address":"${targetAddress}"}` },
+ target: {
+ value: `{"cubidId":"cubid_peer","channel":"${handshakeChannel}","address":"${targetAddress}"}`,
+ },
});
await user.click(screen.getByRole("button", { name: /parse qr payload/i }));
@@ -138,6 +182,19 @@ describe("CameraPage", () => {
),
);
+ await waitFor(() =>
+ expect(notifyHandshakeCompleteMock).toHaveBeenCalledWith({
+ channel: handshakeChannel,
+ challengeId: "challenge-1",
+ expiresAt: 1700001200,
+ overlaps: [
+ { issuer: "0xIssuer", trustLevel: 4, circle: null, freshnessSeconds: 42 },
+ ],
+ targetCubid: "cubid_peer",
+ viewerCubid: "cubid_me",
+ }),
+ );
+
await waitFor(() => expect(pushMock).toHaveBeenCalledWith("/results"));
const result = useScanStore.getState().lastResult;
@@ -150,9 +207,13 @@ describe("CameraPage", () => {
render( );
+ await waitFor(() => expect(getUserMediaMock).toHaveBeenCalled());
+ const devSummary = await screen.findByText(/for devs only/i);
+ await user.click(devSummary);
+
const payloadTextarea = screen.getByPlaceholderText(/Paste JSON like/);
fireEvent.change(payloadTextarea, {
- target: { value: '{"address":"0xPeer"}' },
+ target: { value: '{"address":"0xPeer","channel":"handshake"}' },
});
await user.click(screen.getByRole("button", { name: /parse qr payload/i }));
diff --git a/frontend/__tests__/scan-qr-page.test.tsx b/frontend/__tests__/scan-qr-page.test.tsx
new file mode 100644
index 0000000..9174a7b
--- /dev/null
+++ b/frontend/__tests__/scan-qr-page.test.tsx
@@ -0,0 +1,108 @@
+import type { Session } from "@supabase/supabase-js";
+import { act, render, screen, waitFor } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import MyQrPage from "../src/app/(routes)/scan/my-qr/page";
+import type { HandshakeCompletion } from "../src/lib/handshake";
+import { useScanStore } from "../src/lib/scanStore";
+import { useUserStore } from "../src/lib/store";
+
+const { pushMock, subscribeToHandshakeMock, randomUuidMock } = vi.hoisted(() => ({
+ pushMock: vi.fn(),
+ subscribeToHandshakeMock: vi.fn(),
+ randomUuidMock: vi.fn(() => "handshake-channel"),
+}));
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({
+ push: pushMock,
+ }),
+}));
+
+vi.mock("../src/lib/handshake", () => ({
+ subscribeToHandshake: subscribeToHandshakeMock,
+}));
+
+describe("MyQrPage", () => {
+ const originalCrypto = globalThis.crypto;
+
+ beforeEach(() => {
+ pushMock.mockReset();
+ subscribeToHandshakeMock.mockReset();
+ Object.defineProperty(globalThis, "crypto", {
+ configurable: true,
+ value: {
+ ...originalCrypto,
+ randomUUID: randomUuidMock,
+ },
+ });
+ act(() => {
+ useUserStore.getState().reset();
+ useScanStore.getState().reset();
+ });
+
+ const session = { access_token: "token", user: { id: "user-1" } } as unknown as Session;
+ useUserStore.setState({
+ session,
+ user: {
+ user_id: "user-1",
+ cubid_id: "cubid_me",
+ display_name: "Casey Rivers",
+ },
+ });
+ });
+
+ afterEach(() => {
+ subscribeToHandshakeMock.mockReset();
+ randomUuidMock.mockClear();
+ Object.defineProperty(globalThis, "crypto", {
+ configurable: true,
+ value: originalCrypto,
+ });
+ });
+
+ it("renders the QR code and profile name", async () => {
+ subscribeToHandshakeMock.mockResolvedValue(() => undefined);
+
+ render( );
+
+ await waitFor(() => expect(subscribeToHandshakeMock).toHaveBeenCalledWith("handshake-channel", expect.any(Function)));
+
+ expect(screen.getByRole("img", { name: /scan this within ninety seconds/i })).toBeInTheDocument();
+ expect(screen.getByText("Casey Rivers")).toBeInTheDocument();
+ expect(screen.getByText(/Cubid ID: cubid_me/i)).toBeInTheDocument();
+ });
+
+ it("redirects to results when a handshake completes", async () => {
+ let handler: ((payload: HandshakeCompletion) => void) | null = null;
+ subscribeToHandshakeMock.mockImplementation(async (_channel, callback) => {
+ handler = callback;
+ return vi.fn();
+ });
+
+ render( );
+
+ await waitFor(() => expect(handler).toBeInstanceOf(Function));
+
+ const payload: HandshakeCompletion = {
+ channel: "handshake-channel",
+ challengeId: "challenge-1",
+ expiresAt: 1700001200,
+ overlaps: [],
+ targetCubid: "cubid_me",
+ viewerCubid: "cubid_peer",
+ };
+
+ await act(async () => {
+ handler?.(payload);
+ });
+
+ await waitFor(() => expect(pushMock).toHaveBeenCalledWith("/results"));
+
+ expect(screen.getByText(/Handshake completed/i)).toBeInTheDocument();
+ const result = useScanStore.getState().lastResult;
+ expect(result?.challengeId).toBe("challenge-1");
+ expect(result?.viewerCubid).toBe("cubid_peer");
+ expect(result?.targetCubid).toBe("cubid_me");
+ });
+});
diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs
index a83736a..a1f619c 100644
--- a/frontend/eslint.config.mjs
+++ b/frontend/eslint.config.mjs
@@ -9,6 +9,7 @@ const eslintConfig = defineConfig([
rules: {
"react-hooks/purity": "off",
"react-hooks/set-state-in-effect": "off",
+ "@next/next/no-img-element": "off",
},
},
// Override default ignores of eslint-config-next.
diff --git a/frontend/package.json b/frontend/package.json
index 95d56d6..a187749 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -12,6 +12,7 @@
"dependencies": {
"@supabase/ssr": "^0.7.0",
"@supabase/supabase-js": "^2.80.0",
+ "qrcode.react": "^4.2.0",
"next": "16.0.1",
"react": "19.2.0",
"react-dom": "19.2.0",
diff --git a/frontend/src/app/(routes)/new-user/page.tsx b/frontend/src/app/(routes)/new-user/page.tsx
index 0af7f2c..a1a3b9f 100644
--- a/frontend/src/app/(routes)/new-user/page.tsx
+++ b/frontend/src/app/(routes)/new-user/page.tsx
@@ -29,10 +29,10 @@ export default function NewUserPage() {
router.replace("/(routes)/signin");
return;
}
- if (profile?.cubid_id && profile?.display_name && profile?.evm_address) {
- router.replace("/(routes)/circle");
+ if (profile?.cubid_id) {
+ router.replace("/(routes)/profile");
}
- }, [profile, router, session]);
+ }, [profile?.cubid_id, router, session]);
useEffect(() => {
setForm({
@@ -105,7 +105,7 @@ export default function NewUserPage() {
return (
- Welcome to Peer Mapper
+ Welcome to Trust Me Bro
Confirm your Cubid identity, pick a display name, and connect your Nova/EVM wallet to start vouching and scanning.
@@ -119,7 +119,7 @@ export default function NewUserPage() {
setForm((prev) => ({ ...prev, displayName: event.target.value }))}
- placeholder="Casey Mapper"
+ placeholder="Casey Rivers"
value={form.displayName}
/>
diff --git a/frontend/src/app/(routes)/profile/page.tsx b/frontend/src/app/(routes)/profile/page.tsx
index bf60276..74c4b4e 100644
--- a/frontend/src/app/(routes)/profile/page.tsx
+++ b/frontend/src/app/(routes)/profile/page.tsx
@@ -1,7 +1,8 @@
"use client";
+
import { useRouter } from "next/navigation";
-import { type FormEvent, useEffect, useState } from "react";
+import { type FormEvent, useEffect, useMemo, useState } from "react";
import { upsertMyProfile } from "../../../lib/profile";
import { useUserStore } from "../../../lib/store";
@@ -12,10 +13,23 @@ export default function ProfilePage() {
const profile = useUserStore((state) => state.user);
const setUser = useUserStore((state) => state.setUser);
- const [displayName, setDisplayName] = useState("");
- const [photoUrl, setPhotoUrl] = useState("");
+ const [displayName, setDisplayName] = useState(profile?.display_name ?? "");
+ const [photoUrl, setPhotoUrl] = useState(profile?.photo_url ?? "");
const [status, setStatus] = useState(null);
const [error, setError] = useState(null);
+ const [imageError, setImageError] = useState(false);
+
+ const photoPreviewUrl = useMemo(() => photoUrl || profile?.photo_url || "", [photoUrl, profile?.photo_url]);
+ const previewName = displayName || profile?.display_name || "Your chosen name";
+
+ useEffect(() => {
+ setDisplayName(profile?.display_name ?? "");
+ setPhotoUrl(profile?.photo_url ?? "");
+ }, [profile?.display_name, profile?.photo_url]);
+
+ useEffect(() => {
+ setImageError(false);
+ }, [photoPreviewUrl]);
useEffect(() => {
if (!session) {
@@ -51,7 +65,9 @@ export default function ProfilePage() {
My profile
- Review your Cubid ID and refresh your display information.
+
+ This name can be a nickname. Your name, picture, and Cubid ID are what you share with anyone you invite to connect.
+
@@ -59,36 +75,75 @@ export default function ProfilePage() {
Wallet: {profile?.evm_address ?? "—"}
-
+
+
- {status ?
{status}
: null}
- {error ?
{error}
: null}
+
+
+
Preview for peers
+
+ Here’s how your profile appears to someone you invite. Double-check the image link below before you share it.
+
+
+
+ {photoPreviewUrl && !imageError ? (
+
+ setImageError(true)}
+ />
+
+ ) : (
+
+ {photoPreviewUrl && imageError ? "!" : "No photo"}
+
+ )}
+
+
{previewName}
+
Trust Me Bro
+
Cubid ID: {profile?.cubid_id ?? "Pending from onboarding"}
+
+
+ {photoPreviewUrl && imageError ? (
+
+ We couldn’t load this image. Confirm the URL works in a new tab, then paste it here again.
+
+ ) : null}
+ {!photoPreviewUrl && !imageError ? (
+ Add a photo URL above to see it rendered here.
+ ) : null}
+
+
);
}
diff --git a/frontend/src/app/(routes)/scan/camera/page.tsx b/frontend/src/app/(routes)/scan/camera/page.tsx
index 3b5db37..ac03103 100644
--- a/frontend/src/app/(routes)/scan/camera/page.tsx
+++ b/frontend/src/app/(routes)/scan/camera/page.tsx
@@ -5,12 +5,14 @@ import { type FormEvent, useState } from "react";
import QRScanner from "@/components/QRScanner";
import { requestQrChallenge, verifyQrChallenge } from "@/lib/api";
+import { type HandshakeCompletion, notifyHandshakeComplete } from "@/lib/handshake";
import { useScanStore } from "@/lib/scanStore";
import { useUserStore } from "@/lib/store";
import { ensureWallet } from "@/lib/wallet";
type ParsedQrPayload = {
cubidId: string;
+ channel: string;
address?: string;
};
@@ -39,6 +41,9 @@ export default function CameraPage() {
if (!payload.cubidId) {
throw new Error("QR payload missing cubidId");
}
+ if (!payload.channel) {
+ throw new Error("QR payload missing channel");
+ }
return payload;
}
@@ -156,15 +161,34 @@ export default function CameraPage() {
session.access_token,
);
- setResult({
+ const completion: HandshakeCompletion = {
+ channel: parsed.channel,
targetCubid: parsed.cubidId,
viewerCubid: profile.cubid_id,
challengeId: result.challengeId,
expiresAt: result.expiresAt,
overlaps: result.overlaps,
+ };
+
+ setResult({
+ targetCubid: completion.targetCubid,
+ viewerCubid: completion.viewerCubid,
+ challengeId: completion.challengeId,
+ expiresAt: completion.expiresAt,
+ overlaps: completion.overlaps,
verifiedAt: Date.now(),
});
- setStatus("Overlap ready");
+ setStatus("Sharing overlaps with your peer…");
+
+ try {
+ await notifyHandshakeComplete(completion);
+ } catch (err) {
+ const message = err instanceof Error ? err.message : "Failed to notify peer of handshake";
+ setError(message);
+ setStatus(null);
+ return;
+ }
+
router.push("/results");
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to verify challenge";
@@ -184,87 +208,108 @@ export default function CameraPage() {
-
-
+
+
-
- {parsed ? (
-
-
Target Cubid: {parsed.cubidId}
-
- Issue challenge
-
-
- ) : null}
-
- {challenge ? (
-
-
Challenge ID: {challenge.id}
-
{challenge.value}
-
- Sign as viewer
-
-
- ) : null}
-
- {challenge ? (
-
+
);
diff --git a/frontend/src/app/(routes)/scan/my-qr/page.tsx b/frontend/src/app/(routes)/scan/my-qr/page.tsx
index 6f6794b..a09f6b7 100644
--- a/frontend/src/app/(routes)/scan/my-qr/page.tsx
+++ b/frontend/src/app/(routes)/scan/my-qr/page.tsx
@@ -1,26 +1,86 @@
"use client";
import Link from "next/link";
+import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import QRDisplay from "@/components/QRDisplay";
+import { subscribeToHandshake } from "@/lib/handshake";
+import { useScanStore } from "@/lib/scanStore";
import { useUserStore } from "@/lib/store";
+function createHandshakeChannel(): string {
+ const globalCrypto = globalThis.crypto;
+ if (globalCrypto && typeof globalCrypto.randomUUID === "function") {
+ return globalCrypto.randomUUID();
+ }
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
+}
+
export default function MyQrPage() {
+ const router = useRouter();
const session = useUserStore((state) => state.session);
const profile = useUserStore((state) => state.user);
+ const setResult = useScanStore((state) => state.setResult);
const [timestamp, setTimestamp] = useState(() => Date.now());
+ const [handshakeChannel, setHandshakeChannel] = useState(createHandshakeChannel);
+ const [handshakeMessage, setHandshakeMessage] = useState
(null);
useEffect(() => {
setTimestamp(Date.now());
}, [profile?.cubid_id]);
+ useEffect(() => {
+ setHandshakeMessage(null);
+ setHandshakeChannel(createHandshakeChannel());
+ }, [timestamp]);
+
const payload = useMemo(
- () => ({ cubidId: profile?.cubid_id ?? "", ts: timestamp }),
- [profile?.cubid_id, timestamp],
+ () => ({ cubidId: profile?.cubid_id ?? "", ts: timestamp, channel: handshakeChannel }),
+ [profile?.cubid_id, timestamp, handshakeChannel],
);
+ useEffect(() => {
+ if (!profile?.cubid_id) {
+ return undefined;
+ }
+
+ let active = true;
+ let unsubscribe: (() => void) | undefined;
+
+ (async () => {
+ try {
+ unsubscribe = await subscribeToHandshake(handshakeChannel, (payload) => {
+ if (!active) {
+ return;
+ }
+ if (payload.targetCubid !== profile.cubid_id) {
+ return;
+ }
+ setResult({
+ targetCubid: payload.targetCubid,
+ viewerCubid: payload.viewerCubid,
+ challengeId: payload.challengeId,
+ expiresAt: payload.expiresAt,
+ overlaps: payload.overlaps,
+ verifiedAt: Date.now(),
+ });
+ setHandshakeMessage("Handshake completed—opening shared overlaps…");
+ router.push("/results");
+ });
+ } catch {
+ if (!active) return;
+ setHandshakeMessage("We couldn’t watch for handshake updates. Refresh and try again.");
+ }
+ })();
+
+ return () => {
+ active = false;
+ unsubscribe?.();
+ };
+ }, [handshakeChannel, profile?.cubid_id, router, setResult]);
+
if (!session) {
return (
@@ -40,6 +100,8 @@ export default function MyQrPage() {
);
}
+ const displayName = profile?.display_name?.trim() || "Your chosen name";
+
return (
@@ -51,11 +113,25 @@ export default function MyQrPage() {
-
-
-
+
+
+
+
+
{displayName}
+
Trust Me Bro
+
Cubid ID: {profile?.cubid_id ?? "—"}
+
+ {handshakeMessage ? (
+
+ {handshakeMessage}
+
+ ) : null}
-
+
Need onboarding? Make sure your profile carries a Cubid ID so peers can
locate you instantly.
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index d8bebf1..4d51860 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -8,7 +8,7 @@ import { AppHeader } from "../components/AppHeader";
import { AuthProvider } from "../components/AuthProvider";
export const metadata: Metadata = {
- title: "Peer Mapper",
+ title: "Trust Me Bro",
description: "QR-powered trust overlaps on Moonbeam",
};
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx
index 4c1bb9a..b5e06f5 100644
--- a/frontend/src/app/page.tsx
+++ b/frontend/src/app/page.tsx
@@ -8,7 +8,7 @@ const HIGHLIGHTS = [
{
title: "Prove people are known, not anonymous",
description:
- "Peer Mapper connects Supabase auth, Cubid IDs, and Ethereum attestations so you can verify that the human in front of you is vouched for by people you already trust.",
+ "Trust Me Bro connects Supabase auth, Cubid IDs, and Ethereum attestations so you can verify that the human in front of you is vouched for by people you already trust.",
},
{
title: "QR handshakes in under a minute",
@@ -72,7 +72,7 @@ export default function Home() {
-
Peer Mapper
+
Trust Me Bro
Trust people faster with verifiable overlaps
diff --git a/frontend/src/components/AppFooter.tsx b/frontend/src/components/AppFooter.tsx
index f67630f..0263343 100644
--- a/frontend/src/components/AppFooter.tsx
+++ b/frontend/src/components/AppFooter.tsx
@@ -31,7 +31,7 @@ export function AppFooter() {
))}
-
© {new Date().getFullYear()} Peer Mapper • Built for trusted introductions on Moonbeam.
+
© {new Date().getFullYear()} Trust Me Bro • Built for trusted introductions on Moonbeam.
);
diff --git a/frontend/src/components/AppHeader.tsx b/frontend/src/components/AppHeader.tsx
index 7746819..c7f082a 100644
--- a/frontend/src/components/AppHeader.tsx
+++ b/frontend/src/components/AppHeader.tsx
@@ -7,9 +7,9 @@ import { useMemo } from "react";
import { useUserStore } from "../lib/store";
function getInitials(value: string | null | undefined): string {
- if (!value) return "PM";
+ if (!value) return "TM";
const trimmed = value.trim();
- if (!trimmed) return "PM";
+ if (!trimmed) return "TM";
const segments = trimmed.split(/\s+/).slice(0, 2);
if (segments.length === 1) {
return segments[0]!.slice(0, 2).toUpperCase();
@@ -25,7 +25,7 @@ export function AppHeader() {
const session = useUserStore((state) => state.session);
const profile = useUserStore((state) => state.user);
- const displayName = profile?.display_name ?? session?.user?.email ?? "Peer Mapper";
+ const displayName = profile?.display_name ?? session?.user?.email ?? "Trust Me Bro";
const avatarInitials = useMemo(
() => getInitials(profile?.display_name ?? profile?.cubid_id ?? session?.user?.email),
[profile?.cubid_id, profile?.display_name, session?.user?.email],
@@ -39,7 +39,7 @@ export function AppHeader() {