Files
ponzischeme89 6d44e05de4 v1
2026-04-18 07:23:55 +12:00

315 lines
13 KiB
Markdown

# Goodwalk Mobile Architecture
> Developer reference for the dedicated mobile experience layer introduced in April 2026.
---
## Overview
Goodwalk's mobile experience is a **first-class application layer** built within the existing SvelteKit project. It is not a responsive version of the desktop — mobile users are automatically routed into dedicated mobile routes that use purpose-built layouts, components, and navigation patterns.
```
Desktop users → /admin/* /members/*
Mobile users → /admin/m/* /members/m/*
```
Both the Admin and Member mobile experiences share the same component library and design token system while keeping their routes, permissions, and business logic completely separate.
---
## Folder Structure
```
frontend/src/
├── lib/
│ └── mobile/
│ ├── utils/
│ │ └── device.js UA detection: isMobileUA(), isMobileDevice()
│ ├── stores/
│ │ └── mobile.js isMobile Svelte store + initMobileStore()
│ ├── guards/
│ │ └── routeGuard.js guardMobileRoute() — client-side desktop redirect
│ └── components/
│ ├── MobileAppShell.svelte
│ ├── MobilePageShell.svelte
│ ├── MobileHeader.svelte
│ ├── MobileBottomNav.svelte
│ ├── MobileSection.svelte
│ ├── MobileCard.svelte
│ ├── MobileListItem.svelte
│ ├── MobileStatCard.svelte
│ ├── MobileActionBar.svelte
│ ├── MobileSearchBar.svelte
│ ├── MobileFormField.svelte
│ ├── MobileEmptyState.svelte
│ ├── MobileFilterSheet.svelte
│ └── MobileTabs.svelte
└── routes/
├── +layout.server.js Exposes isMobile to client via page data
├── admin/
│ └── m/ Admin mobile routes
│ ├── +layout.js ssr: false (matches desktop admin)
│ ├── +layout.svelte Auth + theme + nav shell
│ ├── dashboard/+page.svelte
│ ├── bookings/+page.svelte
│ ├── customers/+page.svelte
│ ├── reporting/+page.svelte
│ └── settings/+page.svelte
└── members/
└── m/ Members mobile routes
├── +layout.server.js Cookie auth guard (mirrors desktop)
├── +layout.svelte Auth sync + polling + nav shell
├── home/+page.svelte
├── bookings/+page.svelte
├── walks/+page.svelte
├── messages/+page.svelte
└── profile/+page.svelte
```
---
## How Mobile Routing Works
### 1. Server-side detection (hooks.server.js)
On every GET request, `hooks.server.js` tests the `User-Agent` header against a mobile phone pattern:
```js
const MOBILE_UA_RE = /iPhone|Android.*Mobile|IEMobile|BlackBerry|Opera Mini/i;
```
**Tablets** do NOT match this pattern (Android tablets omit "Mobile" in the UA; iPad is excluded). Tablets receive the desktop experience.
If a phone-class UA is detected and the user is on a desktop route (`/admin/*` or `/members/*`), a `307` redirect is issued to the corresponding mobile entry point.
### 2. Redirect targets
The redirect target is calculated relative to the browser's visible URL, accounting for subdomain rewriting:
| Environment | User visits | Redirected to |
|---|---|---|
| Local dev | `/admin` | `/admin/m/dashboard` |
| Local dev | `/members/dashboard` | `/members/m/home` |
| Admin subdomain | `admin.goodwalk.co.nz/` | `admin.goodwalk.co.nz/m/dashboard` (→ internally `/admin/m/dashboard`) |
| Members subdomain | `members.goodwalk.co.nz/bookings` | `members.goodwalk.co.nz/m/home` |
### 3. Redirect exclusions
These paths are **never redirected** to mobile routes, even on a phone:
- `*/login`, `*/verify` — auth flows work on all platforms
- `*/claim`, `*/onboarding` — onboarding is currently desktop-only
- Paths already under `/m/` — prevents redirect loops
- Requests with `?d=1` — explicit desktop override (useful for QA, power users)
### 4. Client-side desktop guard (guardMobileRoute)
Mobile layout files call `guardMobileRoute()` in `onMount`. If a desktop browser somehow lands on a `/m/` route (e.g. shared link, direct URL entry), it is sent back to the desktop equivalent.
```js
// In any /admin/m/ or /members/m/ +layout.svelte:
import { guardMobileRoute } from '$lib/mobile/guards/routeGuard.js';
onMount(() => {
guardMobileRoute($page.url.pathname, $page.url.search);
});
```
The guard respects `?d=1` — if that param is present, the user stays on the mobile route regardless of device (for QA testing mobile on desktop).
### 5. Preventing redirect loops
Loop prevention is layered:
1. `hooks.server.js` never redirects paths that already contain `/m/`.
2. Mobile layouts never redirect mobile users.
3. `?d=1` stops both server and client redirects.
---
## Auth in Mobile Routes
### Admin mobile (`/admin/m/`)
- **SSR disabled** — `+layout.js` sets `ssr: false`, identical to the desktop admin.
- Auth is handled entirely **client-side** in `+layout.svelte` via `getAdminAccessToken()`.
- If no token is found, a login form is rendered in place of the page content.
- After login, `saveAdminTokens()` is called and the layout re-renders the shell.
- The admin theme preference is loaded from `localStorage` on mount.
### Members mobile (`/members/m/`)
- **SSR enabled** — `+layout.server.js` runs on the server and checks the `gw_member_session` cookie.
- If the cookie is missing, a server-side `307 redirect` to `/members/login?returnTo=<path>` is issued.
- After login (on the desktop login page), the session cookie is set and the member is returned to the mobile route via the `returnTo` param.
- Client-side auth state is further validated in `+layout.svelte` via `isLoggedIn()`.
---
## How to Use Shared Mobile Components
All components live in `src/lib/mobile/components/` and use the `--m-*` CSS custom property token layer provided by `MobileAppShell`.
### Basic page structure
```svelte
<!-- A mobile page file (+page.svelte inside /admin/m/ or /members/m/) -->
<script>
import MobilePageShell from '$lib/mobile/components/MobilePageShell.svelte';
import MobileSection from '$lib/mobile/components/MobileSection.svelte';
import MobileCard from '$lib/mobile/components/MobileCard.svelte';
</script>
<MobilePageShell title="My Page" showBack>
<svelte:fragment slot="header-action">
<!-- Optional icon button in the top-right -->
</svelte:fragment>
<MobileSection title="Section Heading">
<MobileCard>
Content here
</MobileCard>
</MobileSection>
</MobilePageShell>
```
### Key component contracts
**MobileAppShell** — set once per layout (`+layout.svelte`), never in individual pages.
- Props: `variant` (`'admin'|'members'`), `navItems`, `currentPath`
**MobilePageShell** — set once per page, wraps all page content.
- Props: `title`, `showBack`, `backHref`, `transparent`
- Slot: `header-action` for right-side icon buttons
**MobileBottomNav** — rendered automatically by `MobileAppShell`. Never add it manually to a page.
**MobileCard** — use `href` prop to make it a tappable link. Avoid `on:click` on cards when a simple link will do.
**MobileListItem** — always provide `divider={false}` on the last item in a list.
**MobileEmptyState** — always include all three variant cases in data-fetching pages:
```svelte
{#if loading}
<MobileEmptyState variant="loading" />
{:else if error}
<MobileEmptyState variant="error" icon="⚠️" title="Error" body={error} />
{:else if items.length === 0}
<MobileEmptyState icon="🐾" title="Nothing here yet" body="..." />
{:else}
<!-- list content -->
{/if}
```
**MobileFilterSheet** — use `bind:open` to control visibility. The sheet handles its own `Escape` key and overlay click dismissal.
---
## How Admin and Member Mobile Experiences Differ
| Concern | Admin mobile | Members mobile |
|---|---|---|
| Route prefix | `/admin/m/` | `/members/m/` |
| SSR | Disabled (`ssr: false`) | Enabled |
| Auth | Client-side localStorage JWT | Server cookie + localStorage |
| Theme | Admin theme switcher (5 variants) | Fixed green brand palette |
| Nav items | Dashboard, Clients, Add Client, Reporting, Settings | Home, Book, Walks, Messages, Account |
| Feature flags | `bookings_enabled`, `walks_enabled`, etc. | Same set, independently fetched |
| Business logic | Manages all clients/bookings/walks | Views own data only |
| Login screen | Rendered inline in the layout | Separate `/members/login` page |
---
## Extending the Mobile Shell
### Adding a new admin mobile page
1. Create `src/routes/admin/m/<section>/+page.svelte`.
2. Use `MobilePageShell` as the root component in the template.
3. If the page needs a back button, set `showBack` and optionally `backHref`.
4. Add an entry to `BASE_NAV` in `src/routes/admin/m/+layout.svelte` if it should appear in the bottom nav. Otherwise link to it from an existing page.
### Adding a new members mobile page
1. Create `src/routes/members/m/<section>/+page.svelte`.
2. Auth is handled by `+layout.server.js` — no per-page auth check needed.
3. Use `MobilePageShell` and shared components.
4. Add a nav entry in `ACTIVE_NAV` in `src/routes/members/m/+layout.svelte` if it needs a tab.
### Adding a new shared component
1. Create `src/lib/mobile/components/Mobile<Name>.svelte`.
2. Use only `--m-*` CSS custom properties — never hardcode colours.
3. Ensure all interactive elements have `min-height: var(--m-touch-min, 44px)`.
4. Annotate props with JSDoc comments at the top of the `<script>` block.
5. Export from the component directly — no barrel index needed.
---
## Avoiding Desktop/Mobile Concerns Mixing
**Never do this:**
```svelte
<!-- ❌ Don't add mobile breakpoints to desktop components -->
<style>
@media (max-width: 768px) {
.my-desktop-component { display: none; }
}
</style>
```
```svelte
<!-- ❌ Don't import desktop-specific values into mobile components -->
<script>
import { adminThemePresets } from '$lib/adminApi'; // belongs in admin layout, not mobile component
</script>
```
**Do this instead:**
- Keep mobile-specific logic in `src/lib/mobile/` and `src/routes/*/m/`.
- Pass data down via props from the mobile layout to mobile components.
- If a concern is shared (e.g. feature flags, API fetching), import from the existing shared lib (`$lib/featureSettings`, `$lib/memberApi`, etc.) — these are fine to use in mobile pages.
- The mobile components themselves (`MobileCard`, `MobileSection`, etc.) should contain zero business logic.
---
## Migration Notes
The following changes were made to existing files:
### `src/hooks.server.js`
- Added `MOBILE_UA_RE` constant and `isMobileUA()` function.
- Added `maybeMobileRedirect()` which returns a `307 Response` for mobile users on desktop routes.
- The redirect fires **after** subdomain rewriting so the canonical pathname is used for matching.
- Added `event.locals.isMobile` for downstream use.
- The subdomain rewriter now skips paths starting with `/m/` (prevents double-prefixing mobile routes on subdomains).
### `src/routes/+layout.server.js`
- Added `isMobile: locals.isMobile ?? false` to the returned data object.
### `src/routes/+layout.svelte`
- Added a comment clarifying that `/admin/m/*` and `/members/m/*` paths satisfy the existing `isAdmin`/`isMembers` checks and correctly bypass the marketing site chrome.
---
## Follow-up Recommendations
1. **Members login → mobile redirect.** After a member logs in via `/members/login`, `memberApi.js` currently does `goto('/members/dashboard')`. On mobile, this client-side navigation skips `hooks.server.js` and lands the user on the desktop dashboard. Fix: detect mobile in the post-login goto (use `isMobileDevice()` from `$lib/mobile/utils/device.js`) and redirect to `/members/m/home` instead.
2. **Onboarding mobile experience.** Onboarding (`/members/onboarding`) is currently excluded from mobile redirects. Consider building a dedicated `/members/m/onboarding` flow using `MobileFormField` and `MobilePageShell` with back navigation.
3. **Admin mobile — individual client detail page.** The customers list links to `/admin/members/:id` (desktop). Build `/admin/m/customers/:id/+page.svelte` for a mobile-optimised client profile.
4. **Offline / PWA support.** The mobile shell is PWA-ready (safe area support, overscroll contain, touch optimisations). Add a `manifest.json` and service worker to make it installable.
5. **Swipe-back gesture.** iOS-style swipe-back for stacked navigation can be layered on top using Svelte's `fly` transition with a touch gesture handler on the `MobilePageShell` container.
6. **Dark mode.** The `--m-*` token layer makes adding a dark theme straightforward. Add `prefers-color-scheme: dark` overrides in `MobileAppShell`'s `<style>` block.
7. **Pull-to-refresh.** The `m-shell__main` scroll region is a natural candidate for a pull-to-refresh implementation.