+
Inbound ({data.inbound.length})
Wallets who vouched for {data.cubidId}.
@@ -114,22 +124,37 @@ export default function CirclePage() {
{data.inbound.length === 0 ? No attestations yet. : null}
-
- Outbound ({data.outbound.length})
-
- Cubid IDs you've vouched for as {walletAddress ?? "your wallet"}.
-
-
) : null}
diff --git a/frontend/src/app/(routes)/indexer/page.tsx b/frontend/src/app/(routes)/indexer/page.tsx
new file mode 100644
index 0000000..746acd1
--- /dev/null
+++ b/frontend/src/app/(routes)/indexer/page.tsx
@@ -0,0 +1,27 @@
+const INDEXER_URL = "https://moonbeam.moonscan.io/";
+
+export default function IndexerPage() {
+ return (
+
+
+ Moonbeam tooling
+ Moonbeam indexer
+
+ Browse transaction details, contract events, and on-chain history without leaving Trust Me Bro. The
+ Moonbeam explorer loads below inside the app shell so you can jump straight back into handshakes when you are
+ done.
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/app/(routes)/new-user/page.tsx b/frontend/src/app/(routes)/new-user/page.tsx
index a1a3b9f..4631c05 100644
--- a/frontend/src/app/(routes)/new-user/page.tsx
+++ b/frontend/src/app/(routes)/new-user/page.tsx
@@ -1,46 +1,54 @@
"use client";
import { useRouter } from "next/navigation";
-import { type FormEvent, useEffect, useState } from "react";
+import { type FormEvent, useEffect, useMemo, useState } from "react";
import { isValidCubidId, requestCubidId } from "../../../lib/cubid";
+import { useRestrictToIncompleteOnboarding } from "../../../lib/onboarding";
import { upsertMyProfile } from "../../../lib/profile";
import { useUserStore } from "../../../lib/store";
import { ensureWallet } from "../../../lib/wallet";
+function createRandomCubidId(): string {
+ const globalCrypto = globalThis.crypto;
+ if (globalCrypto && typeof globalCrypto.randomUUID === "function") {
+ return `cubid_${globalCrypto.randomUUID().replace(/-/g, "")}`;
+ }
+ return `cubid_${Math.random().toString(36).slice(2, 34)}`;
+}
+
export default function NewUserPage() {
const router = useRouter();
- const session = useUserStore((state) => state.session);
- const profile = useUserStore((state) => state.user);
+ const { session, profile, ready } = useRestrictToIncompleteOnboarding();
const setUser = useUserStore((state) => state.setUser);
const setWalletAddress = useUserStore((state) => state.setWalletAddress);
+ const initialCubidId = useMemo(() => profile?.cubid_id ?? createRandomCubidId(), [profile?.cubid_id]);
const [form, setForm] = useState({
displayName: profile?.display_name ?? "",
photoUrl: profile?.photo_url ?? "",
- cubidId: profile?.cubid_id ?? "",
+ cubidId: initialCubidId,
});
const [status, setStatus] = useState
(null);
const [error, setError] = useState(null);
const [saving, setSaving] = useState(false);
useEffect(() => {
- if (!session) {
- router.replace("/(routes)/signin");
- return;
- }
- if (profile?.cubid_id) {
- router.replace("/(routes)/profile");
- }
- }, [profile?.cubid_id, router, session]);
-
- useEffect(() => {
- setForm({
+ setForm((prev) => ({
displayName: profile?.display_name ?? "",
photoUrl: profile?.photo_url ?? "",
- cubidId: profile?.cubid_id ?? "",
- });
- }, [profile]);
+ cubidId: profile?.cubid_id ?? prev.cubidId ?? initialCubidId,
+ }));
+ }, [initialCubidId, profile?.cubid_id, profile?.display_name, profile?.photo_url]);
+
+ if (!ready && !profile?.cubid_id) {
+ return (
+
+ Loading your onboarding flow…
+ Hold tight while we confirm your session.
+
+ );
+ }
async function handleGenerateCubid() {
if (!session?.user?.email) {
@@ -77,6 +85,9 @@ export default function NewUserPage() {
async function handleSubmit(event: FormEvent) {
event.preventDefault();
+ if (!ready) {
+ return;
+ }
setError(null);
if (!isValidCubidId(form.cubidId)) {
setError("Cubid ID must match cubid_[a-z0-9]{4,32}");
diff --git a/frontend/src/app/(routes)/profile/page.tsx b/frontend/src/app/(routes)/profile/page.tsx
index 74c4b4e..6f1922a 100644
--- a/frontend/src/app/(routes)/profile/page.tsx
+++ b/frontend/src/app/(routes)/profile/page.tsx
@@ -1,17 +1,17 @@
"use client";
-import { useRouter } from "next/navigation";
import { type FormEvent, useEffect, useMemo, useState } from "react";
+import { useRequireCompletedOnboarding } from "../../../lib/onboarding";
import { upsertMyProfile } from "../../../lib/profile";
import { useUserStore } from "../../../lib/store";
+import { ensureWallet } from "../../../lib/wallet";
export default function ProfilePage() {
- const router = useRouter();
- const session = useUserStore((state) => state.session);
- const profile = useUserStore((state) => state.user);
+ const { session, profile, ready } = useRequireCompletedOnboarding();
const setUser = useUserStore((state) => state.setUser);
+ const setWalletAddress = useUserStore((state) => state.setWalletAddress);
const [displayName, setDisplayName] = useState(profile?.display_name ?? "");
const [photoUrl, setPhotoUrl] = useState(profile?.photo_url ?? "");
@@ -31,11 +31,14 @@ export default function ProfilePage() {
setImageError(false);
}, [photoPreviewUrl]);
- useEffect(() => {
- if (!session) {
- router.replace("/(routes)/signin");
- }
- }, [router, session]);
+ if (!ready || !session || !profile) {
+ return (
+
+ Loading your profile…
+ One moment while we confirm your onboarding status.
+
+ );
+ }
async function handleSubmit(event: FormEvent) {
event.preventDefault();
@@ -61,6 +64,22 @@ export default function ProfilePage() {
}
}
+ async function handleConnectWallet() {
+ setError(null);
+ setStatus("Requesting wallet access…");
+ try {
+ const address = await ensureWallet();
+ const updated = await upsertMyProfile({ evm_address: address });
+ setUser(updated);
+ setWalletAddress(address);
+ setStatus("Wallet linked");
+ } catch (err) {
+ const message = err instanceof Error ? err.message : "Wallet connection failed";
+ setError(message);
+ setStatus(null);
+ }
+ }
+
return (
diff --git a/frontend/src/app/(routes)/results/page.tsx b/frontend/src/app/(routes)/results/page.tsx
index c0cc800..c7bc09c 100644
--- a/frontend/src/app/(routes)/results/page.tsx
+++ b/frontend/src/app/(routes)/results/page.tsx
@@ -1,6 +1,7 @@
"use client";
import Badge from "@/components/Badge";
+import { useRequireCompletedOnboarding } from "@/lib/onboarding";
import { useScanStore } from "@/lib/scanStore";
function formatFreshness(seconds: number): string {
@@ -11,8 +12,18 @@ function formatFreshness(seconds: number): string {
}
export default function ResultsPage() {
+ const { ready } = useRequireCompletedOnboarding();
const result = useScanStore((state) => state.lastResult);
+ if (!ready) {
+ return (
+