Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions src/pages/profile/profile-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,70 @@ describe("ProfilePage display name", () => {
})
})

describe("ProfilePage display name nudge", () => {
it("shows the nudge banner when display_name is null", async () => {
window.sessionStorage.clear()
const me = authMeHandler({
id: "u-1",
email: "player@example.com",
display_name: null,
avatar_url: null,
tos_accepted_at: "2026-01-01T00:00:00Z",
})
server.use(me.handler, consentsHandler)

await renderWithFileRoutes(<></>, { initialLocation: "/profile" })

expect(await screen.findByTestId("display-name-nudge")).toBeInTheDocument()
})

it("hides the nudge banner when display_name is set", async () => {
window.sessionStorage.clear()
const me = authMeHandler({
id: "u-1",
email: "player@example.com",
display_name: "ZeroEmpires",
avatar_url: null,
tos_accepted_at: "2026-01-01T00:00:00Z",
})
server.use(me.handler, consentsHandler)

await renderWithFileRoutes(<></>, { initialLocation: "/profile" })

// Wait for the page to settle by finding a known element
await screen.findByLabelText("Display name")
expect(screen.queryByTestId("display-name-nudge")).not.toBeInTheDocument()
})

it("dismisses the nudge for the session when X is clicked", async () => {
window.sessionStorage.clear()
const me = authMeHandler({
id: "u-1",
email: "player@example.com",
display_name: null,
avatar_url: null,
tos_accepted_at: "2026-01-01T00:00:00Z",
})
server.use(me.handler, consentsHandler)

await renderWithFileRoutes(<></>, { initialLocation: "/profile" })

const nudge = await screen.findByTestId("display-name-nudge")
expect(nudge).toBeInTheDocument()

const dismissBtn = screen.getByRole("button", {
name: /dismiss display name suggestion/i,
})
const user = userEvent.setup()
await user.click(dismissBtn)

expect(screen.queryByTestId("display-name-nudge")).not.toBeInTheDocument()
expect(
window.sessionStorage.getItem("cb_display_name_nudge_dismissed")
).toBe("1")
})
})

describe("ProfilePage connected accounts", () => {
it("shows Connect buttons for unlinked providers and the linked status for linked ones", async () => {
const me = authMeHandler({
Expand Down
35 changes: 34 additions & 1 deletion src/pages/profile/profile-page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react"
import { Link, useNavigate } from "@tanstack/react-router"
import { LoaderCircle } from "lucide-react"
import { LoaderCircle, X } from "lucide-react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
Expand All @@ -20,6 +20,8 @@ import {
import type { ProviderConnection } from "@/lib/oauth-state"
import { Route as ProfileRoute } from "@/routes/profile"

const DISPLAY_NAME_NUDGE_KEY = "cb_display_name_nudge_dismissed"

type ProviderId = "google" | "steam"

const PROVIDERS: { id: ProviderId; label: string }[] = [
Expand Down Expand Up @@ -81,6 +83,18 @@ export function ProfilePage() {
message: string
remediation: string[]
} | null>(null)
const [nudgeDismissed, setNudgeDismissed] = useState(() =>
typeof window === "undefined"
? false
: window.sessionStorage.getItem(DISPLAY_NAME_NUDGE_KEY) === "1"
)

const showDisplayNameNudge = !auth.displayName && !nudgeDismissed

function dismissNudge() {
window.sessionStorage.setItem(DISPLAY_NAME_NUDGE_KEY, "1")
setNudgeDismissed(true)
}

useEffect(() => {
setDisplayName(auth.displayName ?? "")
Expand Down Expand Up @@ -270,6 +284,25 @@ export function ProfilePage() {
</CardTitle>
</CardHeader>
<CardContent className="grid gap-6">
{showDisplayNameNudge && (
<div
className="border-primary/30 bg-primary/5 relative rounded-md border p-3 text-xs leading-relaxed"
data-testid="display-name-nudge"
>
<p className="text-muted-foreground pr-6">
You're showing up as your email across the site. Pick a display
name below to personalize your account.
</p>
<button
type="button"
aria-label="Dismiss display name suggestion"
onClick={dismissNudge}
className="text-muted-foreground hover:text-foreground absolute top-2 right-2 transition-colors"
>
<X className="size-3.5" />
</button>
</div>
)}
<div className="grid gap-4">
<div className="grid gap-1 text-sm">
<span className="text-muted-foreground">Email</span>
Expand Down
31 changes: 31 additions & 0 deletions src/pages/register/register-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { getErrorMessage } from "@/lib/api-errors"
import { useAuth } from "@/lib/auth"
import { submitConsents, type ConsentInput } from "@/lib/consent"

const DISPLAY_NAME_MAX_LENGTH = 100

export function RegisterPage() {
const auth = useAuth()
const navigate = useNavigate()
Expand All @@ -27,6 +29,7 @@ export function RegisterPage() {
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [confirmPassword, setConfirmPassword] = useState("")
const [displayName, setDisplayName] = useState("")
const [tosAccepted, setTosAccepted] = useState(false)
const [analyticsConsent, setAnalyticsConsent] = useState(false)
const [sessionReplayConsent, setSessionReplayConsent] = useState(false)
Expand Down Expand Up @@ -71,6 +74,17 @@ export function RegisterPage() {
} catch {
// Swallow — user is signed up; they'll be prompted again on profile.
}
const trimmedDisplayName = displayName.trim()
if (trimmedDisplayName) {
try {
await api.patch("auth/me", {
json: { display_name: trimmedDisplayName },
})
} catch {
// Swallow — user is signed up; the /profile nudge banner will
// prompt them to set it later if this PATCH failed.
}
}
// Refresh the AuthContext (reads /auth/me with the new cookie) before
// navigating, so the destination's beforeLoad sees isAuthenticated=true.
await auth.checkAuth()
Expand Down Expand Up @@ -136,6 +150,23 @@ export function RegisterPage() {
autoComplete="new-password"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="displayName">
Display name{" "}
<span className="text-muted-foreground text-xs font-normal">
(optional)
</span>
</Label>
<Input
id="displayName"
type="text"
placeholder="How you'll appear across criticalbit.gg"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
maxLength={DISPLAY_NAME_MAX_LENGTH}
autoComplete="nickname"
/>
</div>
<label className="flex cursor-pointer items-start gap-3">
<Checkbox
checked={tosAccepted}
Expand Down
Loading