From 6476508d6d4756d5fc487786cc596885d8916e50 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 13:12:15 +0000 Subject: [PATCH 1/4] fix(auth): Google login creates accounts, onboarding prefill, land on overview Login's Google button now requests sign-up with the disclosed current terms and routes through /login (outlets -> overview, none -> onboarding). Wizard and /app land on the dashboard overview. Google name/email prefill the operations contact. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_018caQbzEhmf9nMEnNbccatn --- src/app/app/page.tsx | 4 +- src/app/login/login-form.tsx | 23 ++++++- src/app/onboarding/page.tsx | 5 +- src/app/onboarding/setup-form.tsx | 10 +++- src/app/register/register-form.tsx | 2 +- .../onboarding/onboarding-wizard.tsx | 24 ++++++-- tests/e2e/mobile-onboarding.spec.ts | 7 ++- tests/e2e/phase4-onboarding.spec.ts | 3 +- tests/login-google-signup.test.tsx | 60 +++++++++++++++++++ 9 files changed, 121 insertions(+), 17 deletions(-) create mode 100644 tests/login-google-signup.test.tsx diff --git a/src/app/app/page.tsx b/src/app/app/page.tsx index 377cb315d..94f85874b 100644 --- a/src/app/app/page.tsx +++ b/src/app/app/page.tsx @@ -7,8 +7,8 @@ import { getRestaurantsByUserId } from "@/modules/restaurants/service"; // Always read the authenticated session on the server, including after sign-out/revocation. // This route intentionally stays network-only in the service worker. export const dynamic = "force-dynamic"; -/** Menu Studio. Single source for where the installed app and Play TWA land after sign-in. */ -const APP_HOME_PATH = "/dashboard/menu"; +/** Overview. Single source for where the installed app and Play TWA land after sign-in. */ +const APP_HOME_PATH = "/dashboard"; export const metadata = { title: "Open MenuMinute", robots: { index: false, follow: false }, diff --git a/src/app/login/login-form.tsx b/src/app/login/login-form.tsx index dffef97b8..c183143b5 100644 --- a/src/app/login/login-form.tsx +++ b/src/app/login/login-form.tsx @@ -18,6 +18,7 @@ import { z } from "zod"; import { authClient } from "@/lib/auth-client"; import { AuthShell } from "@/components/auth/auth-shell"; import { authNextKey, safeReturnPath } from "@/lib/auth-return"; +import { TERMS_VERSION } from "@/content/legal"; const schema = z.object({ email: z.email("Enter a valid email address."), password: z.string().min(1, "Password is required."), @@ -66,8 +67,13 @@ export function LoginForm({ googleEnabled = false }: { googleEnabled?: boolean } try { const result = await authClient.signIn.social({ provider: "google", - callbackURL: next || "/dashboard", + // One seamless flow: unknown Google accounts are registered, with the + // disclosed current terms recorded server-side, then routed via /login + // (outlets -> dashboard overview, none -> onboarding). + requestSignUp: true, + callbackURL: "/login" + (next ? "?next=" + encodeURIComponent(next) : ""), errorCallbackURL: "/login" + (next ? "?next=" + encodeURIComponent(next) : ""), + additionalData: { acceptTerms: true, termsVersion: TERMS_VERSION }, }); if (result.error) setError("Google sign-in did not finish. Please retry or sign in with email."); @@ -84,6 +90,19 @@ export function LoginForm({ googleEnabled = false }: { googleEnabled?: boolean } {googleBusy ? "Connecting to Google…" : "Continue with Google"} )} + {googleEnabled && ( +

+ New here? Continuing with Google creates your account and means you agree to the{" "} + + Terms of Service + {" "} + and acknowledge the{" "} + + Privacy Policy + + . +

+ )} )} diff --git a/src/app/onboarding/page.tsx b/src/app/onboarding/page.tsx index c89a80133..af126c94b 100644 --- a/src/app/onboarding/page.tsx +++ b/src/app/onboarding/page.tsx @@ -49,7 +49,10 @@ export default async function OnboardingPage({ className="wizard-welcome-mobile" /> - + ); } diff --git a/src/app/onboarding/setup-form.tsx b/src/app/onboarding/setup-form.tsx index 4860c6878..cd7bb7c07 100644 --- a/src/app/onboarding/setup-form.tsx +++ b/src/app/onboarding/setup-form.tsx @@ -6,6 +6,12 @@ import { OnboardingWizard } from "@/components/onboarding/onboarding-wizard"; * Super Onboarding Wizard setup form with progressive disclosure, GPS geocoordinates, * mediawork placeholders, and server-owned draft reconciliation. */ -export function OnboardingForm({ initialQuick = false }: { initialQuick?: boolean }) { - return ; +export function OnboardingForm({ + initialQuick = false, + prefill, +}: { + initialQuick?: boolean; + prefill?: { name?: string; email?: string }; +}) { + return ; } diff --git a/src/app/register/register-form.tsx b/src/app/register/register-form.tsx index a86ccf152..549a8c8a9 100644 --- a/src/app/register/register-form.tsx +++ b/src/app/register/register-form.tsx @@ -134,7 +134,7 @@ export function RegistrationForm({ googleEnabled = false }: { googleEnabled?: bo const result = await authClient.signIn.social({ provider: "google", requestSignUp: true, - callbackURL: next || "/onboarding", + callbackURL: "/login" + (next ? "?next=" + encodeURIComponent(next) : ""), errorCallbackURL: "/register" + (next ? "?next=" + encodeURIComponent(next) : ""), additionalData: { acceptTerms: true, termsVersion: TERMS_VERSION }, diff --git a/src/components/onboarding/onboarding-wizard.tsx b/src/components/onboarding/onboarding-wizard.tsx index 7e11cc09b..bf20664e1 100644 --- a/src/components/onboarding/onboarding-wizard.tsx +++ b/src/components/onboarding/onboarding-wizard.tsx @@ -132,10 +132,18 @@ function validateStep(step: number, draft: WizardDraft): string | null { interface OnboardingWizardProps { onSuccess?: (outletId: string) => void; initialQuick?: boolean; + /** Account profile (e.g. from Google) used only to suggest empty contact fields. */ + prefill?: { name?: string; email?: string }; } -export function OnboardingWizard({ onSuccess, initialQuick = false }: OnboardingWizardProps) { +export function OnboardingWizard({ + onSuccess, + initialQuick = false, + prefill, +}: OnboardingWizardProps) { const router = useRouter(); + const prefillName = prefill?.name; + const prefillEmail = prefill?.email; const [currentStep, setCurrentStep] = useState(0); const [quickStart, setQuickStart] = useState(initialQuick); const quickLinkRequested = useRef(initialQuick); @@ -183,7 +191,7 @@ export function OnboardingWizard({ onSuccess, initialQuick = false }: Onboarding } catch { /* Storage may be unavailable. */ } - router.replace("/dashboard/get-started"); + router.replace("/dashboard"); router.refresh(); } return; @@ -220,7 +228,13 @@ export function OnboardingWizard({ onSuccess, initialQuick = false }: Onboarding window.navigator.language, Intl.DateTimeFormat().resolvedOptions().timeZone ); - const fresh = { ...initialWizardDraft, ...(browser ?? {}) }; + const fresh = { + ...initialWizardDraft, + ...(browser ?? {}), + // Suggestions from the signed-in account; saved drafts are never overwritten. + operationsContactName: prefillName?.trim() ?? "", + operationsContactEmail: prefillEmail?.trim() ?? "", + }; setDraft(shouldQuick ? quickStartDraft(fresh) : fresh); } let storedStep = Number.NaN; @@ -251,7 +265,7 @@ export function OnboardingWizard({ onSuccess, initialQuick = false }: Onboarding } finally { reconciliation.current = false; } - }, [router, onSuccess]); + }, [router, onSuccess, prefillName, prefillEmail]); useEffect(() => { void Promise.resolve().then(reconcile); @@ -409,7 +423,7 @@ export function OnboardingWizard({ onSuccess, initialQuick = false }: Onboarding } catch { /* Storage may be unavailable. */ } - router.replace("/dashboard/get-started"); + router.replace("/dashboard"); router.refresh(); } } diff --git a/tests/e2e/mobile-onboarding.spec.ts b/tests/e2e/mobile-onboarding.spec.ts index d61319ad1..d25c69877 100644 --- a/tests/e2e/mobile-onboarding.spec.ts +++ b/tests/e2e/mobile-onboarding.spec.ts @@ -161,9 +161,10 @@ for (const size of SIZES) { }); expect(covered, "launch button is not covered by another bar").toBe(false); await launch.click(); - await expect(page).toHaveURL(/\/dashboard\/get-started/); - expect(await overflowOffenders(page), "get-started overflow").toEqual([]); - await snap("06-get-started"); + // Completing onboarding lands on the dashboard overview; the checklist is one tap away. + await expect(page).toHaveURL(/\/dashboard$/); + expect(await overflowOffenders(page), "overview overflow").toEqual([]); + await snap("06-overview"); // --- First dashboard --------------------------------------------------------------------- await page.goto("/dashboard"); diff --git a/tests/e2e/phase4-onboarding.spec.ts b/tests/e2e/phase4-onboarding.spec.ts index edcbcb0c1..45f3be006 100644 --- a/tests/e2e/phase4-onboarding.spec.ts +++ b/tests/e2e/phase4-onboarding.spec.ts @@ -71,7 +71,8 @@ test("first-outlet wizard resumes a saved draft, recovers from failure and opens await page.getByRole("checkbox", { name: /I confirm INR is the currency/ }).check(); await expect(recoveredLaunch).toBeEnabled(); await recoveredLaunch.click(); - await expect(page).toHaveURL(/\/dashboard\/get-started/); + await expect(page).toHaveURL(/\/dashboard$/); + await page.goto("/dashboard/get-started"); await expect(page.getByText("Kitchen for Guided Setup").first()).toBeVisible(); await expect(page.getByRole("link", { name: /Import your existing menu/ })).toBeVisible(); expect(requests).toBe(2); diff --git a/tests/login-google-signup.test.tsx b/tests/login-google-signup.test.tsx new file mode 100644 index 000000000..10cec4e5d --- /dev/null +++ b/tests/login-google-signup.test.tsx @@ -0,0 +1,60 @@ +// @vitest-environment jsdom +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { TERMS_VERSION } from "../src/content/legal"; + +const { social } = vi.hoisted(() => ({ + social: vi.fn(async (_input: unknown) => ({ error: null })), +})); +vi.mock("@/lib/auth-client", () => ({ + authClient: { signIn: { social, email: vi.fn() } }, +})); +vi.mock("next/navigation", () => ({ + useRouter: () => ({ push: vi.fn(), replace: vi.fn(), refresh: vi.fn() }), + useSearchParams: () => new URLSearchParams("next=%2Fdashboard%2Forders"), +})); + +import { LoginForm } from "../src/app/login/login-form"; + +beforeEach(() => { + window.matchMedia ??= ((query: string) => ({ + matches: false, + media: query, + addEventListener: () => {}, + removeEventListener: () => {}, + addListener: () => {}, + removeListener: () => {}, + onchange: null, + dispatchEvent: () => false, + })) as unknown as typeof window.matchMedia; +}); + +afterEach(() => { + cleanup(); + social.mockClear(); +}); + +describe("login Google button", () => { + it("registers unknown Google accounts with the disclosed current terms, then routes via /login", async () => { + render(); + expect(screen.getAllByRole("link", { name: "Terms of Service" })[0]).toHaveAttribute( + "href", + "/terms" + ); + fireEvent.click(screen.getByRole("button", { name: /Continue with Google/ })); + await waitFor(() => expect(social).toHaveBeenCalledTimes(1)); + expect(social).toHaveBeenCalledWith( + expect.objectContaining({ + provider: "google", + requestSignUp: true, + callbackURL: "/login?next=%2Fdashboard%2Forders", + additionalData: { acceptTerms: true, termsVersion: TERMS_VERSION }, + }) + ); + }); + + it("hides the Google path when the provider is not configured", () => { + render(); + expect(screen.queryByRole("button", { name: /Continue with Google/ })).toBeNull(); + }); +}); From df4eaa2e08afc4ffbc55551f5fd3b0cd44a9e3a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 13:12:15 +0000 Subject: [PATCH 2/4] feat(shell): mobile bottom bar Menus/QR Codes/Orders/Floor, sign out in More, hide logo header Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_018caQbzEhmf9nMEnNbccatn --- docs/shell-and-navigation.md | 18 ++++----- src/app/(dashboard)/workspace-bottom-bar.tsx | 41 +++++++++++++++++++- src/app/(dashboard)/workspace.css | 9 +++-- src/modules/dashboard/nav-model.ts | 14 +++---- tests/dashboard-nav-model.test.ts | 24 +++++++----- tests/dashboard-shell-ui.test.tsx | 15 ++++--- tests/e2e/shell-navigation.spec.ts | 13 ++++--- 7 files changed, 92 insertions(+), 42 deletions(-) diff --git a/docs/shell-and-navigation.md b/docs/shell-and-navigation.md index d8896b3c1..32ca79593 100644 --- a/docs/shell-and-navigation.md +++ b/docs/shell-and-navigation.md @@ -4,14 +4,14 @@ Status: **implemented in one PR.** Scope is the signed-in merchant workspace she ## What shipped -| Area | Behaviour | -| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Grouped sidebar | Overview, then **Run service** (Orders, Point of Sale, Kitchen, Dining floor), **Menu** (Menu Studio, QR codes), **Insights** (Reports), **Admin** (Organization, Settings). Destinations the plan or role does not include collapse into an **Add-ons** disclosure with a plain hint. | -| Mobile bottom bar | Orders, Till, Kitchen, Floor and **More**, filtered by what the user can use. A menu-only outlet is back-filled with Overview / Menu so the bar is never nearly empty. More holds everything else, plus Search and Notifications. | -| Command palette | `Ctrl/Cmd+K` or the topbar button. Pages, actions ("Open till", "Add a dish"), dishes ("Mark X sold out" / "Restore X"), orders by number, tables, and an "Ask Ember" hand-off. | -| Topbar | Search, **+ New** menu, **till chip**, **connection and last-sync** indicator, **notification bell**. | -| Notification centre | One drawer: status, aggregated alerts, and the single sound and mute control. | -| Dining floor page | `/dashboard/floor`: the tables and guest-request panels that were only an overview widget, so the bar, sidebar and palette have somewhere to go. | +| Area | Behaviour | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Grouped sidebar | Overview, then **Run service** (Orders, Point of Sale, Kitchen, Dining floor), **Menu** (Menu Studio, QR codes), **Insights** (Reports), **Admin** (Organization, Settings). Destinations the plan or role does not include collapse into an **Add-ons** disclosure with a plain hint. | +| Mobile bottom bar | Menus, QR Codes, Orders, Floor and **More**, filtered by what the user can use. An outlet without ordering is back-filled with Overview / POS so the bar is never nearly empty. More holds everything else (POS, Kitchen, Reports, Settings…), plus Search, Notifications and **Sign out**. The logo header is hidden on phones; the shared topbar remains. | +| Command palette | `Ctrl/Cmd+K` or the topbar button. Pages, actions ("Open till", "Add a dish"), dishes ("Mark X sold out" / "Restore X"), orders by number, tables, and an "Ask Ember" hand-off. | +| Topbar | Search, **+ New** menu, **till chip**, **connection and last-sync** indicator, **notification bell**. | +| Notification centre | One drawer: status, aggregated alerts, and the single sound and mute control. | +| Dining floor page | `/dashboard/floor`: the tables and guest-request panels that were only an overview widget, so the bar, sidebar and palette have somewhere to go. | ## Decisions @@ -37,7 +37,7 @@ Status: **implemented in one PR.** Scope is the signed-in merchant workspace she ## Accessibility - Palette is a real combobox + listbox (`aria-activedescendant`, `aria-selected`, grouped options, result-count announcement). Focus returns to whatever opened it (see below). -- The bottom bar is a `nav` landmark. Visible labels are short ("Till"); accessible names stay full ("Point of Sale", "Kitchen Setup", "Orders, 3 waiting"). +- The bottom bar is a `nav` landmark. Visible labels are short ("Menus", "QR Codes"); accessible names stay full ("Point of Sale", "Kitchen Setup", "Orders, 3 waiting"). - Status is never colour alone: connection, till, alert severity, dish availability and table state all carry an icon and a word. - 44 px minimum targets, safe-area insets, reduced motion honoured. diff --git a/src/app/(dashboard)/workspace-bottom-bar.tsx b/src/app/(dashboard)/workspace-bottom-bar.tsx index 7ab076b7b..09d7b9dbd 100644 --- a/src/app/(dashboard)/workspace-bottom-bar.tsx +++ b/src/app/(dashboard)/workspace-bottom-bar.tsx @@ -1,13 +1,15 @@ "use client"; import Link from "next/link"; -import { usePathname } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; +import { useState } from "react"; import * as Dialog from "@radix-ui/react-dialog"; import { Bell, CircleHelp, Ellipsis, ExternalLink, + LogOut, Search, ShieldAlert, UserRound, @@ -19,12 +21,13 @@ import { moreItems, type ResolvedNavItem, } from "@/modules/dashboard/nav-model"; +import { authClient } from "@/lib/auth-client"; import { useReturnFocus } from "@/lib/use-return-focus"; import { WorkspaceIcon } from "./workspace-icons"; import { useWorkspaceShell } from "./workspace-shell"; /** - * Phone navigation: Orders, POS, Kitchen and Floor (whichever the user can use), then More. + * Phone navigation: Menus, QR Codes, Orders and Floor (whichever the user can use), then More. * Visible text is short; the accessible name stays the full label (for example "Kitchen Setup"). */ export function WorkspaceBottomBar({ @@ -35,6 +38,23 @@ export function WorkspaceBottomBar({ publicHref?: string | null; }) { const pathname = usePathname(); + const router = useRouter(); + const [signingOut, setSigningOut] = useState(false); + const [signOutError, setSignOutError] = useState(""); + async function signOut() { + if (signingOut) return; + setSigningOut(true); + setSignOutError(""); + try { + const result = await authClient.signOut(); + if (result.error) throw new Error("not confirmed"); + router.replace("/login"); + router.refresh(); + } catch { + setSignOutError("Couldn't sign out. Please try again."); + setSigningOut(false); + } + } const returnFocus = useReturnFocus(); const { capabilities, @@ -199,6 +219,23 @@ export function WorkspaceBottomBar({ Help & guides (new tab) +
  • + + {signOutError && ( +

    + {signOutError} +

    + )} +
  • diff --git a/src/app/(dashboard)/workspace.css b/src/app/(dashboard)/workspace.css index 5ea7f9d30..24c098367 100644 --- a/src/app/(dashboard)/workspace.css +++ b/src/app/(dashboard)/workspace.css @@ -1083,13 +1083,14 @@ border-bottom: 1px solid var(--decorative); } + /* Phones/tablets use the bottom bar and shared topbar; the logo header only wasted space. */ .mm-workspace-brand { - min-height: 60px; - margin: 0 16px; + display: none; } - .mm-workspace-brand img { - width: 150px; + /* Nothing left to show (single outlet): remove the empty header and its border. */ + .mm-workspace-sidebar:not(:has(.mm-workspace-outlet)) { + display: none; } .mm-workspace-outlet { diff --git a/src/modules/dashboard/nav-model.ts b/src/modules/dashboard/nav-model.ts index c78e52e80..980254f36 100644 --- a/src/modules/dashboard/nav-model.ts +++ b/src/modules/dashboard/nav-model.ts @@ -116,7 +116,7 @@ export const navItems: readonly NavItem[] = [ id: "menu", href: "/dashboard/menu", label: "Menu Studio", - shortLabel: "Menu", + shortLabel: "Menus", group: "menu", icon: "menu", gate: null, @@ -126,7 +126,7 @@ export const navItems: readonly NavItem[] = [ id: "qr", href: "/dashboard/qr-codes", label: "QR codes", - shortLabel: "QR", + shortLabel: "QR Codes", group: "menu", icon: "qr", gate: null, @@ -281,9 +281,9 @@ export function isNavSelected(item: Pick, pathname: string): bo } /** - * Mobile bottom bar: Orders, POS, Kitchen and Floor when the user can use them, then More. - * An outlet without ordering would otherwise get a nearly empty bar, so it is back-filled with - * Overview, Menu and QR codes until there are at least three destinations. + * Mobile bottom bar: Menus, QR Codes, Orders and Floor when the user can use them, then More. + * An outlet without ordering would otherwise get a short bar, so it is back-filled with + * Overview, POS and Kitchen until there are at least three destinations. */ export function bottomBarItems(capabilities: DashboardNavigation | undefined): ResolvedNavItem[] { const resolved = resolveNavigation(capabilities); @@ -291,9 +291,9 @@ export function bottomBarItems(capabilities: DashboardNavigation | undefined): R ids .map((id) => resolved.find((item) => item.id === id)!) .filter((item) => item.nav.state === "available"); - const primary = pick(["orders", "pos", "kitchen", "floor"]); + const primary = pick(["menu", "qr", "orders", "floor"]); if (primary.length >= 3) return primary; - const filler = pick(["overview", "menu", "qr"]).filter( + const filler = pick(["overview", "pos", "kitchen"]).filter( (item) => !primary.some((p) => p.id === item.id) ); const needed = Math.max(0, 4 - primary.length); diff --git a/tests/dashboard-nav-model.test.ts b/tests/dashboard-nav-model.test.ts index 1df536ada..2dee35af1 100644 --- a/tests/dashboard-nav-model.test.ts +++ b/tests/dashboard-nav-model.test.ts @@ -118,33 +118,39 @@ describe("grouped sidebar", () => { }); describe("mobile bottom bar", () => { - it("shows Orders, POS, Kitchen and Floor when all are usable", () => { - expect(ids(bottomBarItems(caps()))).toEqual(["orders", "pos", "kitchen", "floor"]); + it("shows Menus, QR Codes, Orders and Floor when all are usable", () => { + expect(ids(bottomBarItems(caps()))).toEqual(["menu", "qr", "orders", "floor"]); + }); + + it("uses the requested short labels", () => { + const bar = bottomBarItems(caps()); + expect(bar.map((item) => item.shortLabel)).toEqual(["Menus", "QR Codes", "Orders", "Floor"]); }); it("filters by permission rather than showing dead tabs", () => { - expect(ids(bottomBarItems(caps({ kitchenOperational: false })))).toEqual([ - "orders", + expect(ids(bottomBarItems(caps({ ordering: false })))).toEqual([ + "overview", + "menu", + "qr", "pos", - "floor", ]); }); it("back-fills a menu-only outlet so the bar is never nearly empty", () => { const bar = ids(bottomBarItems(caps({ ordering: false, kitchenOperational: false }))); - expect(bar).toEqual(["overview", "pos", "menu", "qr"]); + expect(bar).toEqual(["overview", "menu", "qr", "pos"]); }); it("falls back to ungated destinations when capabilities are unknown", () => { - expect(ids(bottomBarItems(undefined))).toEqual(["overview", "pos", "menu", "qr"]); + expect(ids(bottomBarItems(undefined))).toEqual(["overview", "menu", "qr", "pos"]); }); it("lists everything else that is available in More, with no duplicates of the bar", () => { const more = ids(moreItems(caps())); expect(more).toEqual([ "overview", - "menu", - "qr", + "pos", + "kitchen", "guests", "reports", "organization", diff --git a/tests/dashboard-shell-ui.test.tsx b/tests/dashboard-shell-ui.test.tsx index 3336a45f7..e3d40ea2e 100644 --- a/tests/dashboard-shell-ui.test.tsx +++ b/tests/dashboard-shell-ui.test.tsx @@ -226,7 +226,7 @@ describe("mobile bottom bar", () => { within(bar) .getAllByRole("link") .map((l) => l.getAttribute("aria-label")) - ).toEqual(["Orders", "Point of Sale", "Kitchen", "Dining floor"]); + ).toEqual(["Menu Studio", "QR codes", "Orders", "Dining floor"]); expect(within(bar).getByRole("button", { name: "More" })).toBeInTheDocument(); }); @@ -254,7 +254,7 @@ describe("mobile bottom bar", () => { ); }); - it("keeps the Kitchen Setup name while it is still being set up", () => { + it("keeps the Kitchen Setup name while it is still being set up", async () => { render( { ); - expect(screen.getByRole("link", { name: "Kitchen Setup" })).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "More" })); + const sheet = await screen.findByRole("dialog", { name: "More" }); + expect(within(sheet).getByRole("link", { name: "Kitchen Setup" })).toBeInTheDocument(); }); it("badges Orders with the number waiting, in the accessible name too", async () => { @@ -286,8 +288,8 @@ describe("mobile bottom bar", () => { fireEvent.click(more); const sheet = await screen.findByRole("dialog", { name: "More" }); for (const name of [ - "Menu Studio", - "QR codes", + "Point of Sale", + "Kitchen", "Reports & Insights", "Settings", "Account & security", @@ -295,6 +297,9 @@ describe("mobile bottom bar", () => { expect(within(sheet).getByRole("link", { name })).toBeInTheDocument(); // Items already on the bar are not repeated in the sheet. expect(within(sheet).queryByRole("link", { name: "Orders" })).toBeNull(); + expect(within(sheet).queryByRole("link", { name: "Menu Studio" })).toBeNull(); + // Sign out is one tap away instead of buried in Settings. + expect(within(sheet).getByRole("button", { name: "Sign out" })).toBeInTheDocument(); fireEvent.keyDown(sheet, { key: "Escape" }); await waitFor(() => expect(screen.queryByRole("dialog", { name: "More" })).toBeNull()); }); diff --git a/tests/e2e/shell-navigation.spec.ts b/tests/e2e/shell-navigation.spec.ts index 7bd77e228..2116d3225 100644 --- a/tests/e2e/shell-navigation.spec.ts +++ b/tests/e2e/shell-navigation.spec.ts @@ -399,7 +399,7 @@ test.describe("phone shell", () => { await signIn(page, await seed({ ordering: true })); const bar = page.getByRole("navigation", { name: "Primary" }); await expect(bar).toBeVisible(); - for (const name of ["Orders", "Point of Sale", "Dining floor", "More"]) + for (const name of ["Menu Studio", "QR codes", "Orders", "Dining floor", "More"]) await expect(bar.getByRole(name === "More" ? "button" : "link", { name })).toBeVisible(); // The old chip row is gone, and the bar never makes the page scroll sideways. await expect(page.getByRole("navigation", { name: "Workspace" })).toBeHidden(); @@ -412,9 +412,10 @@ test.describe("phone shell", () => { await bar.getByRole("button", { name: "More" }).click(); const sheet = page.getByRole("dialog", { name: "More" }); - await expect(sheet.getByRole("link", { name: "Menu Studio" })).toBeVisible(); - await sheet.getByRole("link", { name: "Menu Studio" }).click(); - await expect(page).toHaveURL(/\/dashboard\/menu/); + await expect(sheet.getByRole("button", { name: "Sign out" })).toBeVisible(); + await expect(sheet.getByRole("link", { name: "Point of Sale" })).toBeVisible(); + await sheet.getByRole("link", { name: "Point of Sale" }).click(); + await expect(page).toHaveURL(/\/dashboard\/pos/); await expect(page.getByRole("dialog", { name: "More" })).toHaveCount(0); await expect(bar.getByRole("button", { name: "More" })).toBeVisible(); }); @@ -479,8 +480,8 @@ test.describe("phone shell", () => { try { await signIn(page, await seed({ ordering: false })); const bar = page.getByRole("navigation", { name: "Primary" }); - // The owner can set up the kitchen, which takes a slot; Overview and Menu fill the rest. - for (const name of ["Overview", "Point of Sale", "Kitchen Setup", "Menu Studio"]) + // Menus and QR Codes stay; Overview and POS fill the remaining slots (Kitchen is in More). + for (const name of ["Overview", "Menu Studio", "QR codes", "Point of Sale"]) await expect(bar.getByRole("link", { name })).toBeVisible(); await expect(bar.getByRole("link", { name: "Orders" })).toHaveCount(0); } finally { From 0cbd187eeba6d04a38ca6d65547cabf9795a00f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 13:12:15 +0000 Subject: [PATCH 3/4] fix(mobile): uniform Orders hero, full-width POS, hide kitchen Ember row and wake control on phones Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_018caQbzEhmf9nMEnNbccatn --- src/app/(dashboard)/dashboard/pos/page.tsx | 6 +++--- src/app/(dashboard)/dashboard/pos/pos-editorial.css | 2 +- src/app/(dashboard)/workspace-ux.css | 2 +- src/app/kitchen/kitchen.css | 11 +++++++++++ src/app/kitchen/page.tsx | 12 +++++++----- src/components/orders/service-counter.css | 9 +++++---- src/components/pwa/operational-pwa.module.css | 7 +++++++ 7 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/app/(dashboard)/dashboard/pos/page.tsx b/src/app/(dashboard)/dashboard/pos/page.tsx index c8253d76d..f5cf93831 100644 --- a/src/app/(dashboard)/dashboard/pos/page.tsx +++ b/src/app/(dashboard)/dashboard/pos/page.tsx @@ -75,9 +75,6 @@ export default async function PosPage() { : null; return ( <> - {workspace.context.enabled && pwaOperationalEnabledForOutlet(outlet.id) ? ( - - ) : null} + {workspace.context.enabled && pwaOperationalEnabledForOutlet(outlet.id) ? ( + + ) : null} ); } diff --git a/src/app/(dashboard)/dashboard/pos/pos-editorial.css b/src/app/(dashboard)/dashboard/pos/pos-editorial.css index 134a0169c..ecdf40b74 100644 --- a/src/app/(dashboard)/dashboard/pos/pos-editorial.css +++ b/src/app/(dashboard)/dashboard/pos/pos-editorial.css @@ -1,5 +1,5 @@ /* POS editorial workspace: matches the Dashboard and Tabletop Press without changing finance behavior. */ -.mm-workspace-main:has(> .mm-shared-topbar + .till-page.pos-editorial) { +.mm-workspace-main:has(> .till-page.pos-editorial) { max-width: none; margin: 0; padding: 0; diff --git a/src/app/(dashboard)/workspace-ux.css b/src/app/(dashboard)/workspace-ux.css index dce1003cd..50833fd0d 100644 --- a/src/app/(dashboard)/workspace-ux.css +++ b/src/app/(dashboard)/workspace-ux.css @@ -3,7 +3,7 @@ --mm-content-wide: 1480px; --mm-content-gutter: clamp(16px, 3vw, 44px); } -.mm-workspace-main:has(> .mm-shared-topbar + :is(.pos-editorial, .reports-workspace, .qr-studio)) { +.mm-workspace-main:has(> :is(.pos-editorial, .reports-workspace, .qr-studio)) { max-width: none; margin: 0; padding: 0; diff --git a/src/app/kitchen/kitchen.css b/src/app/kitchen/kitchen.css index ab06295b1..120de7edb 100644 --- a/src/app/kitchen/kitchen.css +++ b/src/app/kitchen/kitchen.css @@ -2814,3 +2814,14 @@ flex-basis: min(82%, 250px); } } + +/* Ember launcher wrapper: transparent on larger screens, removed from the phone layout. */ +.kk-ember-slot { + display: contents; +} + +@media (max-width: 700px) { + .kk-ember-slot { + display: none; + } +} diff --git a/src/app/kitchen/page.tsx b/src/app/kitchen/page.tsx index fe114950a..8310401c3 100644 --- a/src/app/kitchen/page.tsx +++ b/src/app/kitchen/page.tsx @@ -97,11 +97,13 @@ export default async function KitchenPage() { ) : null} {showEmber && ( - +
    + +
    )} Date: Wed, 11 Nov 2026 13:16:26 +0000 Subject: [PATCH 4/4] test(e2e): onboarding now lands on the dashboard overview Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_018caQbzEhmf9nMEnNbccatn --- tests/e2e/foundation.spec.ts | 2 +- tests/e2e/quick-menu-onboarding.spec.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/e2e/foundation.spec.ts b/tests/e2e/foundation.spec.ts index 67cf4fb2f..e2f1a1c05 100644 --- a/tests/e2e/foundation.spec.ts +++ b/tests/e2e/foundation.spec.ts @@ -47,7 +47,7 @@ test("signup, atomic outlet setup, private mutation and session recovery", async await page.getByRole("checkbox", { name: /I confirm OMR is the currency/ }).check(); await expect(launch).toBeEnabled(); await launch.click(); - await expect(page).toHaveURL(/\/dashboard\/get-started/); + await expect(page).toHaveURL(/\/dashboard$/); await page.goto("/dashboard/menu"); await expect(page.getByRole("heading", { name: "Menu Studio" })).toBeVisible(); const created = await page.request.post("/api/menu/categories", { diff --git a/tests/e2e/quick-menu-onboarding.spec.ts b/tests/e2e/quick-menu-onboarding.spec.ts index 47e78e11f..318663702 100644 --- a/tests/e2e/quick-menu-onboarding.spec.ts +++ b/tests/e2e/quick-menu-onboarding.spec.ts @@ -47,7 +47,9 @@ test("QR-only Quick Start resumes its compact flow and reaches the first menu mi await page.getByRole("checkbox", { name: /I confirm the operating timezone/ }).check(); await page.getByRole("checkbox", { name: /I confirm BWP is the currency/ }).check(); await page.getByRole("button", { name: "Confirm & Create Outlet" }).click(); - await expect(page).toHaveURL(/\/dashboard\/get-started/); + // Onboarding now lands on the overview; the checklist remains one navigation away. + await expect(page).toHaveURL(/\/dashboard$/); + await page.goto("/dashboard/get-started"); await expect(page.getByRole("link", { name: "Add your first dish" })).toBeVisible(); const status = await (await page.request.get("/api/onboarding")).json(); expect(status.status).toBe("completed");