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)/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-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-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/(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/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/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 && (
-
+
+
+
)}
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/src/components/orders/service-counter.css b/src/components/orders/service-counter.css
index 69213e6b3..63ecb92b8 100644
--- a/src/components/orders/service-counter.css
+++ b/src/components/orders/service-counter.css
@@ -1163,7 +1163,8 @@
}
@media (max-width: 680px) {
.sc-hero {
- min-height: 660px;
+ /* Match the other workspace heroes (Dashboard ~430px at 390px wide). */
+ min-height: clamp(400px, 108vw, 470px);
background-image: url("/brand/orders/service-counter/hero-mobile-light.webp");
background-position: center bottom;
}
@@ -1183,10 +1184,10 @@
padding: 8px 17px;
}
.sc-hero-copy {
- padding: 29px 21px 32px;
+ padding: 24px 21px 26px;
}
.service-counter .sc-hero h1 {
- font-size: clamp(35px, 9vw, 49px);
+ font-size: clamp(32px, 8.4vw, 42px);
max-width: 390px;
}
.service-counter .sc-hero-tagline {
@@ -1229,7 +1230,7 @@
grid-template-columns: 1fr;
}
.sc-hero {
- min-height: 620px;
+ min-height: 400px;
}
.sc-hero-copy {
padding-top: 25px;
diff --git a/src/components/pwa/operational-pwa.module.css b/src/components/pwa/operational-pwa.module.css
index cf09d295d..0f689dc93 100644
--- a/src/components/pwa/operational-pwa.module.css
+++ b/src/components/pwa/operational-pwa.module.css
@@ -75,3 +75,10 @@
margin: 0 0 0.4rem;
text-transform: uppercase;
}
+
+/* On phones the floating control covers the bottom navigation; keep it for tablets/desktop. */
+@media (max-width: 900px) {
+ .wakeControl {
+ display: none;
+ }
+}
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/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/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/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");
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 {
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();
+ });
+});