toga-ai 1.0.166 → 1.0.167

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,5 +2,8 @@
2
2
 
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
+ | [TOGa Commerce (toga2-commerce / commerce2-react) Architecture](architecture.md) | `toga2-commerce` (npm package name **`commerce2-react`**, product name **TOGa Commerce**) is the customer-facing **B2B commerce storefront** of the 2.0 platform | src/main.tsx, src/App.tsx, src/routes.tsx, src/contexts/AuthContext.tsx, src/contexts/helpers/getLoginSettings.ts, src/api/axiosInstance.ts, src/stores/, src/themeConfig/ThemeContext.tsx, src/fieldsConfig/index.ts, src/hooks/useAssignClientFields.ts, vite.config.ts, package.json |
5
6
  | [Cart Notification Emails — duplicate prevention](features/cart-notification-emails.md) | On the cart "Notifications" section a user can add CC email addresses to an order. | src/pages/Cart/CartPage.tsx, src/pages/Cart/view/cartForm/CartForm.tsx, src/stores/useEmailOptionsStore.ts, src/stores/useCartSalesQuoteZu.ts, src/pages/Cart/viewModel/FIELDS/*/*/*/CARTPAGE.ts |
7
+ | [Client Fields — per-tenant / language / role content & config](features/client-fields.md) | Almost no user-facing text, field layout, or page config is hard-coded in `toga2-commerce`. | src/fieldsConfig/index.ts, src/fieldsConfig/getClientLoginFields.ts, src/fieldsConfig/clientFields/COMPASS.json, src/fieldsConfig/clientFields/COMPASSCANADA.json, src/fieldsConfig/clientFields/QUAD.json, src/hooks/useAssignClientFields.ts, src/hooks/useDynamicConditionalFieldOptions.ts, src/stores/useFieldsStore.ts, src/components/BaseDetailField/BaseDetailField.tsx |
8
+ | [Multi-Tenant Resolution & Theming](features/multi-tenant-theming.md) | `toga2-commerce` serves multiple clients from one codebase. | src/themeConfig/themes.json, src/themeConfig/ThemeContext.tsx, src/themeConfig/types.ts, src/components/ThemeSwitcher/ThemeSwitcher.tsx, src/components/AuthLayout/AuthLayout.tsx, src/api/axiosInstance.ts, src/contexts/AuthContext.tsx, tailwind.config.js |
6
9
  | [AWS Amplify Build & Deploy (non-prod environments)](workflows/amplify-build-and-deploy.md) | How `toga2-commerce` (React + Vite, "commerce2-react") builds and deploys on **AWS Amplify**. | toga2-commerce/amplify.yml, toga2-commerce/.gitattributes, toga2-commerce/package.json, toga2-commerce/.github/workflows/sync-stage-environments.yml |
@@ -0,0 +1,246 @@
1
+ ---
2
+ title: TOGa Commerce (toga2-commerce / commerce2-react) Architecture
3
+ framework: "2.0"
4
+ repo: toga2-commerce
5
+ project: TOGa Commerce
6
+ client: shared
7
+ type: architecture
8
+ status: active
9
+ updated: 2026-06-23
10
+ owners: ["apeterson"]
11
+ files:
12
+ - src/main.tsx
13
+ - src/App.tsx
14
+ - src/routes.tsx
15
+ - src/contexts/AuthContext.tsx
16
+ - src/contexts/helpers/getLoginSettings.ts
17
+ - src/api/axiosInstance.ts
18
+ - src/stores/
19
+ - src/themeConfig/ThemeContext.tsx
20
+ - src/fieldsConfig/index.ts
21
+ - src/hooks/useAssignClientFields.ts
22
+ - vite.config.ts
23
+ - package.json
24
+ related:
25
+ - 2.0/apps/toga2-commerce/features/multi-tenant-theming.md
26
+ - 2.0/apps/toga2-commerce/features/client-fields.md
27
+ - 2.0/apps/toga2-commerce/workflows/amplify-build-and-deploy.md
28
+ - 2.0/apps/api2/architecture.md
29
+ ---
30
+
31
+ ## Summary
32
+
33
+ `toga2-commerce` (npm package name **`commerce2-react`**, product name **TOGa Commerce**) is the
34
+ customer-facing **B2B commerce storefront** of the 2.0 platform. It is a **React 18 + TypeScript +
35
+ Vite 5 single-page app** styled with **Tailwind**, and it is the React front end in the 2.0
36
+ "decoupled" architecture: it talks to **`api2`** (the TOGa v2 JSON API) for all data, and renders
37
+ nothing server-side.
38
+
39
+ It is **multi-tenant**: a single codebase serves several clients (currently **COMPASS**,
40
+ **COMPASSCANADA**, **QUAD**, plus a **DEFAULT** fallback). The tenant is resolved at runtime from
41
+ the **hostname** and drives three independent systems — the API base URL, the theme, and the
42
+ per-tenant field/content config. See [multi-tenant-theming](features/multi-tenant-theming.md) and
43
+ [client-fields](features/client-fields.md) for those subsystems in depth.
44
+
45
+ > Note: this is a **2.0 *app* with no PHP**. Its registry `dependsOn` is `api2`; it consumes the
46
+ > `_underscore`/`api2` backend over HTTP but contains no framework PHP classes itself.
47
+
48
+ ## Tech stack (verified versions, package.json)
49
+
50
+ | Concern | Library | Version |
51
+ |---|---|---|
52
+ | UI | react / react-dom | ^18.3.1 |
53
+ | Routing | react-router-dom | ^7.5.1 |
54
+ | Server state / cache | @tanstack/react-query (+ persist-client, sync-storage-persister) | ^5.59.19 |
55
+ | Client state | zustand | ^4.5.4 |
56
+ | HTTP | axios | 1.7.7 |
57
+ | Forms | react-hook-form | ^7.53.1 |
58
+ | Animation | framer-motion | ^11.0.18 |
59
+ | Styling | tailwindcss | ^3.4.4 |
60
+ | Errors / monitoring | @sentry/react | ^8.50.0 |
61
+ | Build / tooling | vite ^5.4.10, typescript ^5.8.2 |
62
+ | Tables | @tanstack/react-table, react-table (v7), react-table-sticky |
63
+ | Misc | dayjs, react-select, nouislider, classnames, uuid, react-lottie, @agilant/toga-blox, FontAwesome Pro |
64
+
65
+ E2E tests use **Cypress** (`cypress/`, `cypress.config.ts`).
66
+
67
+ ## Bootstrap & provider stack
68
+
69
+ - `src/main.tsx` — initializes **Sentry** (browser tracing + replay) and mounts `<App/>` in React
70
+ `StrictMode`; imports global CSS.
71
+ - `src/App.tsx` — creates a single `QueryClient` (default `staleTime: 24h`) and a
72
+ `createSyncStoragePersister` (localStorage key **`"commerce"`**). Provider nesting, **outermost → innermost**:
73
+
74
+ ```
75
+ PersistQueryClientProvider (client=queryClient, persister → localStorage "commerce")
76
+ └─ AuthProvider (contexts/AuthContext.tsx — auth state, tenant/login resolution)
77
+ └─ ThemeProvider (themeConfig/ThemeContext.tsx — CSS-variable theming)
78
+ └─ ToasterProvider (contexts/ToasterContext.tsx — toast notifications)
79
+ └─ RouterProvider (routes.tsx)
80
+ + ReactQueryDevtools (initialIsOpen=false)
81
+ ```
82
+
83
+ `App` also runs a mount effect managing **edit-order mode**: it reads a `localStorage.synced`
84
+ flag and calls `exitEditOrderModeGlobalSyncReset()` when an edit-order session was abandoned
85
+ outside `/cart`, and subscribes to `useReturnOriginalUserFromViewAsStore` (the "view-as" feature).
86
+
87
+ > The cache is **persisted**: query results survive refreshes via `PersistQueryClientProvider`.
88
+ > Combined with the 24h default `staleTime`, catalog data is aggressively cached — invalidate
89
+ > deliberately with `queryClient.invalidateQueries(...)` when freshness matters.
90
+
91
+ ## Routing (`src/routes.tsx`, createBrowserRouter)
92
+
93
+ Public:
94
+ - `/` → `AuthRedirect` (→ `/home` if authenticated, else renders nothing while the auth flow runs)
95
+ - `/login` → `LoginRoute` (→ `/home` if already authenticated, else `<Login/>`)
96
+ - `/reset-password` → `ResetPasswordPage`
97
+
98
+ Authenticated (wrapped by `PrivateRoute` → `AuthLayout`):
99
+ - `/home`, `/bundle-view`, `/filter`, `/cart`, `/get-support`, `/account`, `/item-view`,
100
+ `/order-details`, `/privacy-policy`, `/terms-conditions`; unmatched → `Navigate to="/home"`.
101
+
102
+ Mechanics:
103
+ - **`PrivateRoute`** gates on `useAuth().isAuthenticated` + `useAuthenticationFlow().isLoading`;
104
+ shows an `AuthLoading` spinner while loading, redirects to `/login` if unauthenticated.
105
+ - **`AuthLayout`** wraps every authenticated route and **stays mounted across route changes**
106
+ (header, footer, nav, notifications live here). Its `errorElement` renders `<ErrorMessage errorType={500}/>`.
107
+ ⚠️ Because it never remounts, React Query `refetchOnMount` only fires **once per session** for
108
+ queries owned by AuthLayout — route-change refreshes must be wired explicitly (e.g. the
109
+ notifications query invalidates on route change keyed on `user?.uuid`).
110
+ - Routes are **statically imported** (no `React.lazy`); Vite handles chunking at build time.
111
+
112
+ ## Page architecture — MVVM convention
113
+
114
+ Every page under `src/pages/<Page>/` follows the same shape (View ← ViewModel ← API ← FIELDS):
115
+
116
+ ```
117
+ src/pages/<Page>/
118
+ ├── <Page>Page.tsx / index.ts # View — layout & rendering only
119
+ ├── types.ts # page-local TS types
120
+ ├── view/ # sub-components, modals, loading skeletons
121
+ ├── viewModel/
122
+ │ ├── use<Page>ViewModel.ts # state + React Query hooks + business logic
123
+ │ ├── index.ts
124
+ │ └── FIELDS/<CLIENT>/[<LANGUAGE>/][<ROLE>/]*.json|*.ts # per-tenant content/config
125
+ ├── api/<Page>Api.ts # axios calls for this page
126
+ ├── hooks/ # page-scoped hooks
127
+ └── helpers/ # pure compute/format helpers
128
+ ```
129
+
130
+ Pages: `Account, BundleView, Cart, Filter, GetSupport, Home, ItemsView, Login, OrderDetails,
131
+ PrivacyPolicy, ResetPassword, TermsConditions`. The **ViewModel hook is the seam**: it calls
132
+ `useAssignClientFields(fieldKey, language, user)` to load the right tenant/role/language content,
133
+ runs the page's queries, and returns ready-to-render data to the View. (See client-fields doc.)
134
+
135
+ ## State management
136
+
137
+ ### Zustand stores (`src/stores/`)
138
+ Most use the `persist` middleware (localStorage). Verified files:
139
+
140
+ | Store | Responsibility |
141
+ |---|---|
142
+ | `useUserStore` | Logged-in user (persona ids, role flags, currency) |
143
+ | `useCartStoreZu` | Cart items + bundles, per-item quantity-change loading |
144
+ | `useCartSalesQuoteZu` | Active sales order (ship-to, contact, cost center, notes, emails) |
145
+ | `useCartOverlayZu` | Cart preview overlay state |
146
+ | `useEditOrderZu` | "Edit existing order" mode flag + original snapshot |
147
+ | `useEditOrderUuidStore` | UUID of the order being edited |
148
+ | `useSelectedUserZu` | "Order for" user selected in cart |
149
+ | `useReturnOriginalUserFromViewAsStore` | "View as another user" return-to-self tracking |
150
+ | `useEmailOptionsStore` | Notification CC email options (see cart-notification-emails) |
151
+ | `useBundleBuilderZu` | Bundle-builder wizard selections |
152
+ | `useFieldsStore` | **`fieldKey` (tenant) + `language`** — drives field/theme resolution (persist key `fields-key`) |
153
+ | `useSettingsModalStore` | Login/settings modal open flag |
154
+ | `useSettingsErrorStore` | Settings validation errors (suppresses data queries when set) |
155
+
156
+ ### React Query (`src/queries/`, `src/hooks/useApiQuery.ts`, `useApiMutation.ts`)
157
+ - Global default `staleTime` 24h; per-query overrides (user/persona data fetched fresh on login).
158
+ - Cache persisted to localStorage (`"commerce"`); rehydrated on load.
159
+ - Page queries are typically `enabled` on `isAuthenticated && user?.uuid && !settingsModalEnabled`.
160
+
161
+ **Two-layer state model:** React Query owns *server* state (catalog, orders, user); Zustand owns
162
+ *local* state (cart, selected user, edit mode, tenant/language). The cart is synced to the backend
163
+ sales order via `src/api/syncSalesOrder*.ts` helpers.
164
+
165
+ ## API layer (`src/api/`)
166
+
167
+ `src/api/axiosInstance.ts` is the shared client. Key behaviors (all verified):
168
+ - **Base URL by tenant:** `host = "VITE_API_" + window.location.hostname.toUpperCase().split(".")[0]`;
169
+ `baseURL = import.meta.env[host] || import.meta.env.VITE_API`. So `compass.togacommerce` →
170
+ `VITE_API_COMPASS`, falling back to `VITE_API`. `timeout: 180000`.
171
+ - **Transaction id:** a request interceptor stamps every call with `params.transactionId = uuidv4()`.
172
+ - **Auth token:** a second request interceptor attaches `Authorization: Bearer <token>` — using the
173
+ stored `accessToken` if a `user` exists in localStorage, otherwise fetching a **public token** via
174
+ `POST {baseURL}/auth/public`.
175
+ - **1 ms delay interceptor:** a third request interceptor `await delay(1)` before sending.
176
+ - **401 handling:** response interceptor attempts `POST {baseURL}/auth/refresh` (Bearer refresh
177
+ token) once (`_retry`), retries the original request on success, else calls `performLogout()`
178
+ (clears all auth + cart + `commerce` cache keys and redirects to `/`).
179
+ - Failures are reported to **Sentry** with `errorType: "API"` tags.
180
+
181
+ The backend (`api2`) returns the standard 2.0 envelope (`success`/`data`/`errors`; see api2 arch);
182
+ generic CRUD helpers live in `src/api/genericApi.ts` (paginated `getData`, `saveData`, `updateData`,
183
+ `deleteData`, optional `?depth=-1` for nested responses).
184
+
185
+ ## Authentication & roles
186
+
187
+ - `src/contexts/AuthContext.tsx` derives `host` from the hostname (`hostname.toUpperCase().split(".")[0]`),
188
+ loads `getClientLoginFields(host)` (tenant config), decides persona-switcher visibility
189
+ (`determineShouldShowPersonaSwitcher` — QUAD special-cased), then runs
190
+ `getLoginSettings(...)` which writes `useFieldsStore.fieldKey = host` and the language, and
191
+ hydrates `useUserStore`.
192
+ - `src/hooks/useAuthenticationFlow.ts` orchestrates the post-login fetch sequence
193
+ (user data → persona → contact → settings).
194
+ - **Role flags** on the user object are tenant-specific:
195
+ - COMPASS / COMPASSCANADA → `ADMIN` (`_isAdmin`), `SUPERUSER` (`_isSuperAdmin`),
196
+ `MANAGER` (`_isSupervisor`), else `USER`.
197
+ - QUAD → `GLOBALADMIN` (`_isGlobalAdmin`), `BUYER` (`_isBuyer`), `ITSHOPPER` (`_isItShopper`), else `USER`.
198
+ - **Personas:** `user._personaIds` is a colon-delimited string; `getLoginSettings` has special
199
+ handling collapsing persona-24 cases for non-QUAD tenants.
200
+
201
+ ## Build & deploy
202
+
203
+ - **Per-tenant dev/build scripts** (`package.json`): `compass`, `compasscanada`, `quad`,
204
+ `togacommerce` run `vite --mode development --host <tenant>.togacommerce`; production builds are
205
+ `build` / `buildAlpha` / `buildBeta` / `buildGamma` / `buildQcSecurity` (each installs the matching
206
+ `@agilant/toga-blox` npm channel, then `tsc` + `vite build --mode <env>`).
207
+ - `vite.config.ts`: React plugin + Sentry Vite plugin (sourcemap upload); dev `server.allowedHosts`
208
+ = `compass.togacommerce`, `compasscanada.togacommerce`, `quad.togacommerce`; PostCSS via
209
+ `postcss.config.cjs`; `optimizeDeps.include: ["react-router-dom"]`.
210
+ - Local dev requires `*.togacommerce` hostnames to resolve to localhost (hosts file) so tenant
211
+ resolution works.
212
+ - Deployment runs on **AWS Amplify** — see [amplify-build-and-deploy](workflows/amplify-build-and-deploy.md).
213
+ A `Dockerfile`/`docker-compose.yml` also exist for containerized dev.
214
+
215
+ ## Directory map (`src/`)
216
+
217
+ | Path | Purpose |
218
+ |---|---|
219
+ | `main.tsx` / `App.tsx` | bootstrap, Sentry, provider stack |
220
+ | `routes.tsx` | router + route guards |
221
+ | `api/` | axios instance, generic CRUD, sales-order sync helpers |
222
+ | `contexts/` | `AuthContext`, `ToasterContext` (+ `helpers/getLoginSettings.ts`) |
223
+ | `stores/` | Zustand stores (see table) |
224
+ | `queries/` | login/prefetch React Query definitions |
225
+ | `hooks/` | shared hooks (`useAssignClientFields`, `useAuthenticationFlow`, `useApiQuery/Mutation`, `useBreakpoint`, `useDynamicConditionalFieldOptions`, `useCancelEditOrder`) |
226
+ | `pages/` | feature pages (MVVM) |
227
+ | `components/` | shared UI (Base* atoms, `AuthLayout`, `Header`/`Footer`, `ThemeSwitcher`, `BaseDetailField`, guardrails, error messages, address forms, cards) |
228
+ | `themeConfig/` | `themes.json`, `ThemeContext.tsx`, `types.ts` (theming) |
229
+ | `fieldsConfig/` | global per-tenant client config + the master `FIELDS` registry |
230
+ | `styles/`, `fonts/`, `assets/` | global CSS, web fonts, images/SVGs (some per-tenant) |
231
+ | `utils/` | formatting, cart/address/persona helpers, query helpers |
232
+ | `globalTypes.ts` | shared TS types (API envelope, User, Address, Item/Bundle, SalesOrder, …) |
233
+
234
+ ## Gotchas
235
+
236
+ - **`AuthLayout` never remounts** → `refetchOnMount` fires once per session; wire explicit
237
+ invalidation for data that must refresh on navigation.
238
+ - **24h `staleTime` + persisted cache** → users can see stale catalog/pricing; invalidate on the
239
+ events that should bust it.
240
+ - **Tenant resolution depends on the hostname.** On `localhost` with no `*.togacommerce` host the
241
+ first DNS label won't match a tenant, so config falls back (`getClientLoginFields` → COMPASS,
242
+ theme → DEFAULT). Use the per-tenant dev scripts.
243
+ - **Theme vs. fields language keys differ** — theme/field *folder* names are uppercase
244
+ (`COMPASSCANADA`, `ENGLISH`/`FRENCH`) but the runtime `FIELDS` registry keys language as
245
+ lowercase `en`/`fr`. See the client-fields doc.
246
+ </content>
@@ -0,0 +1,187 @@
1
+ ---
2
+ title: Client Fields — per-tenant / language / role content & config
3
+ framework: "2.0"
4
+ repo: toga2-commerce
5
+ project: TOGa Commerce
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-23
10
+ owners: ["apeterson"]
11
+ files:
12
+ - src/fieldsConfig/index.ts
13
+ - src/fieldsConfig/getClientLoginFields.ts
14
+ - src/fieldsConfig/clientFields/COMPASS.json
15
+ - src/fieldsConfig/clientFields/COMPASSCANADA.json
16
+ - src/fieldsConfig/clientFields/QUAD.json
17
+ - src/hooks/useAssignClientFields.ts
18
+ - src/hooks/useDynamicConditionalFieldOptions.ts
19
+ - src/stores/useFieldsStore.ts
20
+ - src/components/BaseDetailField/BaseDetailField.tsx
21
+ related:
22
+ - 2.0/apps/toga2-commerce/architecture.md
23
+ - 2.0/apps/toga2-commerce/features/multi-tenant-theming.md
24
+ - 2.0/apps/toga2-commerce/features/cart-notification-emails.md
25
+ ---
26
+
27
+ ## Summary
28
+
29
+ Almost no user-facing text, field layout, or page config is hard-coded in `toga2-commerce`. Instead
30
+ each page's labels, field lists, validation, visibility, and static content come from **JSON config
31
+ resolved per tenant, per language, and per role**. There are **two layers**:
32
+
33
+ - **Layer A — global client config** (`src/fieldsConfig/clientFields/<TENANT>.json`): a small file
34
+ per tenant controlling the language switcher and which user API fields to fetch at login.
35
+ - **Layer B — the per-page `FIELDS` registry** (`src/fieldsConfig/index.ts` → `FIELDS`): the big
36
+ `tenant → language → role → page-section` object that every page ViewModel reads.
37
+
38
+ ## Layer A — global client config
39
+
40
+ `src/fieldsConfig/clientFields/{COMPASS,COMPASSCANADA,QUAD}.json`, each:
41
+
42
+ ```json
43
+ {
44
+ "languageSwitcher": { "uuid": "1", "isEnabled": true|false },
45
+ "userApiFields": ["firstName", "lastName", "email", "uuid", "_isAdmin", ...]
46
+ }
47
+ ```
48
+
49
+ - `languageSwitcher.isEnabled` — **only COMPASSCANADA is `true`** (English/French). COMPASS and
50
+ QUAD are `false`.
51
+ - `userApiFields` — the allow-list of user columns fetched at login; importantly it includes the
52
+ tenant's **role flags**: COMPASS/COMPASSCANADA carry `_isAdmin`, `_isSupervisor`, `_isSuperAdmin`;
53
+ QUAD carries `_isGlobalAdmin`, `_isItShopper`, `_isBuyer`. All include `_personaIds`.
54
+
55
+ Resolved by **`src/fieldsConfig/getClientLoginFields.ts`** — a `switch(client)` returning the JSON;
56
+ **default is `COMPASS`** (not DEFAULT). Called from `AuthContext` as `getClientLoginFields(host)`.
57
+ The `languageSwitcher.isEnabled` value feeds the login-settings branch in `getLoginSettings.ts`.
58
+
59
+ ## Layer B — the per-page FIELDS registry
60
+
61
+ `src/fieldsConfig/index.ts` (~1100 lines) imports every page's per-tenant field constants (which
62
+ themselves wrap the on-disk JSON under `pages/<Page>/viewModel/FIELDS/...`) and assembles one object:
63
+
64
+ ```ts
65
+ export const FIELDS: Record<string, any> = {
66
+ COMPASS: {
67
+ en: {
68
+ ADMIN: { HEADERFIELDS, HOMEPAGEFIELDS, ORDERDETAILS, CARTPAGEFIELDS, /* ~20 sections */ },
69
+ SUPERUSER: { ... },
70
+ MANAGER: { ... },
71
+ USER: { ... },
72
+ },
73
+ },
74
+ COMPASSCANADA: { en: { ADMIN, SUPERUSER, MANAGER, USER }, fr: { ... } },
75
+ QUAD: { en: { GLOBALADMIN, BUYER, ITSHOPPER /*, USER */ } },
76
+ };
77
+ ```
78
+
79
+ ### Resolution dimensions & order
80
+
81
+ `FIELDS[ tenant ][ language ][ role ]` → an object of **page-section** constants
82
+ (`HEADERFIELDS`, `HOMEPAGEFIELDS`, `FOOTERFIELDS`, `ITEMSPAGE`, `ORDERDETAILS`, `GETSUPPORTPAGE`,
83
+ `BUNDLEDETAILSPAGE`, `FILTERPAGEFIELDS`, `CARTPAGEFIELDS`, `NEWUSERFORMFIELDS`, `CANTFINDUSERFIELDS`,
84
+ `PRIVACYPOLICYPAGEFIELDS`, `TERMSCONDITIONSPAGEFIELDS`, `ACCOUNTTABFIELDS`, `MYACCOUNTFIELDS`,
85
+ `ORDERHISTORYFIELDS`, `ADD/EDIT/SELECTNEWPRIMARY/SHIPPINGADDRESSESFIELDS`, `CARTOVERLAY`,
86
+ `ERRORFIELDS`, `LOGINSETTINGSFIELDS`, …).
87
+
88
+ - **Tenant** keys: `COMPASS`, `COMPASSCANADA`, `QUAD`.
89
+ - **Language** keys are **lowercase `en` / `fr`** in the registry. ⚠️ This differs from the on-disk
90
+ **folder** names, which are uppercase `ENGLISH` / `FRENCH`. Only COMPASSCANADA has `fr`.
91
+ - **Role** keys: COMPASS/COMPASSCANADA → `ADMIN`, `SUPERUSER`, `MANAGER`, `USER`;
92
+ QUAD → `GLOBALADMIN`, `BUYER`, `ITSHOPPER` (+ `USER`).
93
+
94
+ On disk the JSON lives at `pages/<Page>/viewModel/FIELDS/<CLIENT>/[<LANGUAGE>/][<ROLE>/]<NAME>FIELDS.json`,
95
+ and **not every page uses every dimension**:
96
+ - Pages with full role split: Home, OrderDetails, BundleView, Filter, Cart, Account.
97
+ - Pages with **no role layer** (role-neutral content): PrivacyPolicy, GetSupport, ItemsView,
98
+ TermsConditions, ResetPassword (resolved at tenant/language level, shared across roles).
99
+ - Language layer (`ENGLISH`/`FRENCH` folders) only exists for COMPASSCANADA; COMPASS and QUAD put
100
+ files directly under the tenant (their registry language is `en`).
101
+
102
+ ## The resolver — `useAssignClientFields`
103
+
104
+ `src/hooks/useAssignClientFields.ts` is called by **every page ViewModel** (and several form
105
+ ViewModels) with `(fieldKey, language, user)`:
106
+
107
+ ```ts
108
+ let role: string;
109
+ if (clientName === "COMPASS" || clientName === "COMPASSCANADA") {
110
+ if (user?._isAdmin === "1") role = "ADMIN";
111
+ else if (user?._isSuperAdmin === "1") role = "SUPERUSER";
112
+ else if (user?._isSupervisor === "1") role = "MANAGER";
113
+ else role = "USER";
114
+ } else { // QUAD (and any non-Compass tenant)
115
+ if (user?._isGlobalAdmin === "1") role = "GLOBALADMIN";
116
+ else if (user?._isBuyer === "1") role = "BUYER";
117
+ else if (user?._isItShopper === "1") role = "ITSHOPPER";
118
+ else role = "USER";
119
+ }
120
+
121
+ // wrapped in React Query, keyed for caching:
122
+ const queryKey = ["clientFields", clientName, language, role];
123
+ const data = FIELDS?.[clientName]?.[language]?.[role] || null;
124
+ ```
125
+
126
+ Inputs:
127
+ - **`clientName`** = `useFieldsStore.fieldKey` (the tenant `host`, set at login — see architecture doc).
128
+ - **`language`** = `useFieldsStore.language` (default `"en"`; set to `"fr"` etc. via `setLanguage`
129
+ from the login settings switcher or `MySettingsView`).
130
+ - **`user`** = the logged-in user (role flags).
131
+
132
+ It returns `{ fields, loading, error, resetFieldsMapCache }`; `resetFieldsMapCache` invalidates the
133
+ `["clientFields"]` query (used when role/language/tenant changes mid-session). Page ViewModels then
134
+ pluck the section they need, e.g. `fieldsCall["MYACCOUNTFIELDS"]`.
135
+
136
+ ## What's inside a FIELDS JSON
137
+
138
+ Shape varies by page, but common forms:
139
+ - **Static content pages** (PrivacyPolicy, TermsConditions, GetSupport): headings + text/HTML blocks
140
+ (`label` may contain HTML).
141
+ - **Form pages** (ResetPassword, address forms, NewUser): `formFields[]` with
142
+ `inputType`, `isRequired`, `validationRequirements[]`, labels, placeholders.
143
+ - **Data/detail pages** (Account, OrderDetails): field descriptors mapping data to display, e.g.
144
+ `{ uuid, label, valueKey, valueType: "currency"|"address"|"status", icon, colSpan, classes }`,
145
+ plus `pageSettings` (e.g. which API fields to fetch: `fetchUserDataFields`, `fetchUserOrdersDetailsApiFields`).
146
+
147
+ Common field attributes: `uuid`, `label`, `valueKey` (path into the data object), `inputType`,
148
+ `valueType` (render hint), `colSpan`/`classes` (Tailwind), `isRequired`, `icon`, `options[]`.
149
+
150
+ ## Consumers
151
+
152
+ - **`src/components/BaseDetailField/`** (`BaseDetailField.tsx`, `renderBaseDetailFieldLabel`,
153
+ `renderBaseDetailFieldValue`) — generic renderer: takes a field descriptor + data object, applies
154
+ `valueType` formatting (currency/address/status) and the descriptor's Tailwind classes.
155
+ - **`src/pages/BundleView/hooks/useBundleFields.ts`** — wraps `useAssignClientFields` and reads
156
+ bundle quantity rules from `BUNDLEDETAILSPAGE.pageSettings`.
157
+ - **`src/hooks/useDynamicConditionalFieldOptions.ts`** — swaps dependent select options at runtime
158
+ (e.g. state/province options change with the selected country).
159
+
160
+ ## Onboarding / editing fields — practical notes
161
+
162
+ - **New tenant:** add `clientFields/<TENANT>.json`, a `getClientLoginFields` case, a `<TENANT>`
163
+ branch in `FIELDS` (with the tenant's role keys), and the per-page `FIELDS/<TENANT>/...` JSON +
164
+ the corresponding imports in `index.ts`.
165
+ - **New role:** extend the role-derivation in `useAssignClientFields.ts` **and** add the role key
166
+ under each `FIELDS[tenant][lang]` branch + per-page role folders.
167
+ - **New language:** set `languageSwitcher.isEnabled` in the tenant's `clientFields` JSON, add the
168
+ `fr`/`xx` branch to `FIELDS[tenant]`, and the `<LANGUAGE>` on-disk folders.
169
+ - **Editing copy:** change the page's `FIELDS/<CLIENT>/[<LANG>/][<ROLE>/]<NAME>FIELDS.json`. Remember
170
+ to update **all** affected role/language variants — there is no inheritance/fallback between roles.
171
+
172
+ ## Gotchas
173
+
174
+ - **`en`/`fr` (registry) vs `ENGLISH`/`FRENCH` (folders)** — mismatching these is the classic bug;
175
+ the runtime lookup uses lowercase.
176
+ - **No fallback** in `FIELDS[client][language][role]` — a missing tenant/role/language combo returns
177
+ `null` and the page renders empty. Keep all role variants in sync.
178
+ - Role is derived from string `"1"` flags on the user (`user?._isAdmin === "1"`), not booleans.
179
+ - `getClientLoginFields` defaults to **COMPASS** for an unknown host, which can mask a
180
+ misconfigured tenant in local dev.
181
+
182
+ ## Change history
183
+ - 2026-06-23 — Initial: documented the two-layer field system (global `clientFields/<TENANT>.json`
184
+ + the per-page `FIELDS` registry), the `tenant→language→role` resolution in `useAssignClientFields`,
185
+ the `en`/`fr` vs `ENGLISH`/`FRENCH` distinction, role derivation per tenant, and field JSON shapes
186
+ (apeterson)
187
+ </content>
@@ -0,0 +1,142 @@
1
+ ---
2
+ title: Multi-Tenant Resolution & Theming
3
+ framework: "2.0"
4
+ repo: toga2-commerce
5
+ project: TOGa Commerce
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-23
10
+ owners: ["apeterson"]
11
+ files:
12
+ - src/themeConfig/themes.json
13
+ - src/themeConfig/ThemeContext.tsx
14
+ - src/themeConfig/types.ts
15
+ - src/components/ThemeSwitcher/ThemeSwitcher.tsx
16
+ - src/components/AuthLayout/AuthLayout.tsx
17
+ - src/api/axiosInstance.ts
18
+ - src/contexts/AuthContext.tsx
19
+ - tailwind.config.js
20
+ related:
21
+ - 2.0/apps/toga2-commerce/architecture.md
22
+ - 2.0/apps/toga2-commerce/features/client-fields.md
23
+ ---
24
+
25
+ ## Summary
26
+
27
+ `toga2-commerce` serves multiple clients from one codebase. A request's **tenant** is derived
28
+ from the **hostname**, and that single tenant string fans out into three independent systems:
29
+
30
+ 1. **API base URL** — `axiosInstance.ts` picks `VITE_API_<TENANT>`.
31
+ 2. **Theme** — `ThemeContext` applies the tenant's color set as CSS variables.
32
+ 3. **Fields/content** — the per-tenant field config (separate doc: [client-fields](client-fields.md)).
33
+
34
+ Current tenants: **COMPASS**, **COMPASSCANADA**, **QUAD**, plus a **DEFAULT** fallback theme.
35
+
36
+ ## Tenant resolution — the hostname is the source of truth
37
+
38
+ The same derivation appears in three places (intentionally — there is no single shared tenant util):
39
+
40
+ ```ts
41
+ const fullHostName = window.location.hostname.toUpperCase(); // "COMPASS.TOGACOMMERCE"
42
+ const host = fullHostName.split(".")[0]; // "COMPASS"
43
+ ```
44
+
45
+ - **`src/api/axiosInstance.ts`** (lines ~8-11): `const host = "VITE_API_" + hostName[0];`
46
+ → `baseURL = import.meta.env[host] || import.meta.env.VITE_API`. So the API endpoint is
47
+ tenant-specific (`VITE_API_COMPASS`, `VITE_API_QUAD`, …) with `VITE_API` as fallback.
48
+ - **`src/contexts/AuthContext.tsx`** (lines ~68-70): derives `host` and uses it for
49
+ `getClientLoginFields(host)`, persona-switcher logic, and to set `fieldKey` (see below).
50
+ - **`src/themeConfig/ThemeContext.tsx`** (lines ~13-15): derives `host` as the **default** theme key.
51
+
52
+ This is why the per-tenant **dev scripts** matter: `npm run compass` runs
53
+ `vite --host compass.togacommerce`, and `vite.config.ts` `server.allowedHosts` permits
54
+ `compass.togacommerce`, `compasscanada.togacommerce`, `quad.togacommerce`. Plain `localhost`
55
+ won't match a tenant, so it falls back (theme → DEFAULT, fields → COMPASS).
56
+
57
+ ### From hostname to active theme — `fieldKey` is the real trigger
58
+
59
+ The theme is **not** applied directly from the raw hostname at mount. The chain is:
60
+
61
+ 1. On login, `AuthContext` → `getLoginSettings(...)` writes `useFieldsStore.fieldKey = host`.
62
+ 2. **`src/components/AuthLayout/AuthLayout.tsx`** runs:
63
+ ```ts
64
+ const { theme, loadThemeForClient } = useThemeContext();
65
+ useEffect(() => { loadThemeForClient(fieldKey); }, [fieldKey, loadThemeForClient]);
66
+ ```
67
+ So whenever `fieldKey` resolves, the matching theme loads.
68
+ 3. **On first mount**, `ThemeProvider` loads `localStorage["current-client-id"]` (set by a prior
69
+ `loadThemeForClient`) or **`"DEFAULT"`** — not the hostname — so the very first paint before
70
+ login uses the last-saved or default theme.
71
+
72
+ ## Theme config & data shape
73
+
74
+ - **`src/themeConfig/themes.json`** — top-level keys `DEFAULT`, `COMPASS`, `COMPASSCANADA`, `QUAD`.
75
+ Each entry: `{ id, name, colors: {...} }`.
76
+ - **`src/themeConfig/types.ts`** (`ClientTheme`) — the `colors` object is the only themed data.
77
+ Token groups (each typically with `default/hover/disabled/active` states, some with `bg-`,
78
+ `text-`, `border-`, `icon-` prefixes):
79
+ - `primary` (a numeric color scale, e.g. `50`/`500`/`900`)
80
+ - `btn` (`primary`, `tertiary`), `btn-nav`, `btn-nav-badge`, `btn-tabView-alt1`
81
+ - `input`, `checkBox`, `tile`, `text` (`body-primary`/`secondary`/`tertiary`)
82
+
83
+ > The theme controls **colors only**. Fonts, spacing, etc. are global Tailwind config / `@font-face`
84
+ > in `src/fonts/`, not per-tenant theme data.
85
+
86
+ ## How the theme is applied — CSS variables on `<html>`
87
+
88
+ `ThemeContext.applyThemeToDOM` recursively flattens `themeData.colors` into CSS custom properties
89
+ on `document.documentElement`:
90
+
91
+ ```ts
92
+ // leaf string → root.style.setProperty(`--${prefix}-${key}`, value)
93
+ // nested object → recurse with prefix `${prefix}-${key}`
94
+ applyNestedColors(themeData.colors, "color");
95
+ ```
96
+
97
+ So `colors.btn.primary["bg-default"]` becomes `--color-btn-primary-bg-default`. **`tailwind.config.js`**
98
+ maps semantic color tokens to these variables (`var(--color-...)`), so components style with normal
99
+ Tailwind classes and automatically pick up the active tenant's palette. `loadThemeForClient` also
100
+ persists the chosen key to `localStorage["current-client-id"]`.
101
+
102
+ ## ThemeSwitcher
103
+
104
+ `src/components/ThemeSwitcher/ThemeSwitcher.tsx` is a manual `<select>` over `availableThemes`
105
+ (from `Object.values(themes.json)`) calling `loadThemeForClient(e.target.value)`. It's an override
106
+ / dev-testing affordance for previewing tenants — normal users get their theme from `fieldKey`.
107
+
108
+ ## Per-tenant assets
109
+
110
+ Logos and some imagery are **hard-coded imports with conditional rendering** keyed on `theme?.id`
111
+ (e.g. AuthLayout selects the QUAD logo vs. the Compass logo). They are not part of `themes.json`.
112
+
113
+ ## Onboarding a new tenant — checklist
114
+
115
+ 1. **Theme** — add a new top-level key to `src/themeConfig/themes.json` with the full `colors` set
116
+ (copy DEFAULT/COMPASS and recolor); confirm `tailwind.config.js` tokens cover any new groups.
117
+ 2. **API env** — add `VITE_API_<TENANT>` to the `.env.<mode>` files (else it falls back to `VITE_API`).
118
+ 3. **Dev script + allowedHosts** — add `"<tenant>": "vite --mode development --host <tenant>.togacommerce"`
119
+ to `package.json` and `<tenant>.togacommerce` to `vite.config.ts` `server.allowedHosts`; add the
120
+ host to the local hosts file.
121
+ 4. **Client login config** — add `src/fieldsConfig/clientFields/<TENANT>.json` and a `case` in
122
+ `src/fieldsConfig/getClientLoginFields.ts` (see client-fields doc).
123
+ 5. **Fields** — add the `<TENANT>` branch to the master `FIELDS` registry and the per-page
124
+ `FIELDS/<TENANT>/...` JSON (see client-fields doc).
125
+ 6. **Roles** — if the tenant's role flags differ from COMPASS/QUAD, extend the role-derivation
126
+ in `src/hooks/useAssignClientFields.ts` and `AuthContext`'s persona logic.
127
+ 7. **Assets** — add tenant logos and wire the conditional `theme?.id` rendering.
128
+
129
+ ## Gotchas
130
+
131
+ - Tenant detection is **hostname-only** — there is no env-var or build-flag override of the tenant
132
+ at runtime. Wrong host = wrong tenant.
133
+ - The theme's first paint uses `localStorage["current-client-id"]` or DEFAULT, *then* corrects to
134
+ `fieldKey` once `AuthLayout` mounts — expect a brief default-themed flash pre-login.
135
+ - Adding a color token group to `themes.json` does nothing visually unless `tailwind.config.js`
136
+ references the corresponding `--color-*` variable.
137
+
138
+ ## Change history
139
+ - 2026-06-23 — Initial: documented hostname→tenant resolution (axios/AuthContext/ThemeContext),
140
+ `fieldKey`-driven theme application via AuthLayout, CSS-variable flattening of `themes.json` colors,
141
+ ThemeSwitcher, and the new-tenant onboarding checklist (apeterson)
142
+ </content>
@@ -26,7 +26,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
26
26
  - **talos** (TOGa IQ) — 6 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
27
27
  - **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
28
28
  - **ai-bdr** (AI-BDR) — 4 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
29
- - **toga2-commerce** (TOGa Commerce) — 2 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
29
+ - **toga2-commerce** (TOGa Commerce) — 5 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
30
30
  - **toga25-supply** (TOGa 2.5 Supply) — 5 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
31
31
  - **toga-blox** (TOGa Blox) — 7 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
32
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.166",
3
+ "version": "1.0.167",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",