toga-ai 1.0.832 → 1.0.834
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.
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -1
- package/knowledge/2.0/apps/_underscore/workflows/rotating-public-tls-certificates.md +91 -5
- package/knowledge/2.0/apps/toga2-commerce/INDEX.md +5 -4
- package/knowledge/2.0/apps/toga2-commerce/architecture.md +34 -98
- package/knowledge/2.0/apps/toga2-commerce/features/cart-notification-emails.md +58 -142
- package/knowledge/2.0/apps/toga2-commerce/features/client-fields.md +121 -393
- package/knowledge/2.0/apps/toga2-commerce/features/header-order-for-dropdown.md +106 -0
- package/knowledge/2.0/apps/toga2-commerce/workflows/cypress-testing.md +68 -144
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/compass-usa/INDEX.md +1 -1
- package/knowledge/clients/compass-usa/profile.md +15 -2
- package/package.json +1 -1
|
@@ -61,5 +61,5 @@
|
|
|
61
61
|
| [USPS DPV Deliverability Verdict (is this address actually insurable/shippable?)](features/usps-dpv-deliverability.md) | **USPS returning HTTP 200 with a populated address is NOT evidence that the address is deliverable.** The authoritative signal is USPS's **DPV (Delivery Point V |
|
|
62
62
|
| [Refreshing a Local Dev Database from Beta (dev-sandbox)](workflows/local-db-refresh-from-beta.md) | How to reset a local 2.0 dev database from the **beta / dev-sandbox** environment: dump each schema (`Core`, `Client_<Id>`, `Logs_<Id>`, …) from the beta host, |
|
|
63
63
|
| [Deleting a shared branch does not remove bad commits — a stale local clone merges them back](workflows/recreated-shared-branch-stale-local-remerge.md) | **Deleting and recreating a shared environment branch removes only the *ref*.** Every teammate who still has that branch checked out locally keeps the full pre- |
|
|
64
|
-
| [Rotating Public TLS Certificates (ACM, CloudFront, ALB, Elastic Beanstalk)](workflows/rotating-public-tls-certificates.md) | How to replace the public TLS certificates that terminate HTTPS for TOGa front-end domains (togahub, togacommerce, togadesk, togaretail, togasupply, togaview, t |
|
|
64
|
+
| [Rotating Public TLS Certificates (ACM, CloudFront, ALB, Elastic Beanstalk, API Gateway)](workflows/rotating-public-tls-certificates.md) | How to replace the public TLS certificates that terminate HTTPS for TOGa front-end domains (togahub, togacommerce, togadesk, togaretail, togasupply, togaview, t |
|
|
65
65
|
| [Running a 2.0 App Locally (browser, end-to-end via api2)](workflows/running-a-2.0-app-locally.md) | The full dependency chain required to run a 2.0 client app **through the browser**, end-to-end, against a **local `api2`** (e.g. |
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Rotating Public TLS Certificates (ACM, CloudFront, ALB, Elastic Beanstalk)
|
|
2
|
+
title: Rotating Public TLS Certificates (ACM, CloudFront, ALB, Elastic Beanstalk, API Gateway)
|
|
3
3
|
framework: "2.0"
|
|
4
4
|
repo: _underscore
|
|
5
5
|
project: _Underscore
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-17
|
|
10
10
|
owners: ["rgirish"]
|
|
11
11
|
files: []
|
|
12
12
|
related:
|
|
@@ -27,13 +27,15 @@ This runbook was written from the 2026-09-16 rotation in AWS account **654654170
|
|
|
27
27
|
one IMPORTED multi-domain cert (14 SANs, copied into us-east-1, us-west-2 and eu-west-1) served
|
|
28
28
|
4 CloudFront distributions and 12 ALB listeners and expired the same day.
|
|
29
29
|
|
|
30
|
-
**The
|
|
30
|
+
**The four things that bite you:**
|
|
31
31
|
1. **Elastic Beanstalk keeps its own saved cert setting**, separate from the ALB listener. Fixing
|
|
32
32
|
the listener is not enough — the next deploy puts the old cert straight back.
|
|
33
33
|
2. **CloudFront holds exactly ONE cert per distribution.** There is no staging. The swap *is* the
|
|
34
34
|
cutover, and it takes 5-15 minutes.
|
|
35
35
|
3. **An ACM wildcard matches exactly one label.** `*.togasupply.com` covers
|
|
36
36
|
`elite.togasupply.com` but **not** `compass.beta.togasupply.com`.
|
|
37
|
+
4. **API Gateway custom domains hold their own cert** and are invisible to a CloudFront/ALB/EB
|
|
38
|
+
sweep. This is what the 2026-09-16 rotation missed — see step 6b.
|
|
37
39
|
|
|
38
40
|
**Root cause to avoid repeating:** an **IMPORTED** ACM cert never auto-renews. Always replace with
|
|
39
41
|
**Amazon-issued, DNS-validated** ACM certs, which renew themselves.
|
|
@@ -123,6 +125,60 @@ one), and every environment stayed Green/Ok through the update — no traffic di
|
|
|
123
125
|
**Gotcha:** an EB environment whose CloudFormation stack is in `DELETE_FAILED` cannot be updated at
|
|
124
126
|
all — `update-environment` is refused. That needs separate stack cleanup.
|
|
125
127
|
|
|
128
|
+
## Step 6b — Audit API Gateway custom domains (the 2026-09-16 miss)
|
|
129
|
+
|
|
130
|
+
**API Gateway is a FOURTH resource type.** The 2026-09-16 rotation swept CloudFront, ALB listeners
|
|
131
|
+
and EB saved configs, and reported "0 endpoints at risk". The next morning `webhook.togahub.com`
|
|
132
|
+
was hard-down — it is an API Gateway custom domain, so none of those three sweeps could ever have
|
|
133
|
+
found it. Proof of the failure, not just a stale cert:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
$ curl -sS -o /dev/null -w "http=%{http_code} ssl=%{ssl_verify_result}\n" https://webhook.togahub.com/
|
|
137
|
+
curl: (60) SSL certificate problem: certificate has expired
|
|
138
|
+
http=000 ssl=10
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Find them from DNS — an API Gateway domain CNAMEs to `*.execute-api.<region>.amazonaws.com`:**
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
aws route53 list-resource-record-sets --hosted-zone-id <zid> --max-items 400 \
|
|
145
|
+
--query "ResourceRecordSets[?Type=='CNAME'].[Name,ResourceRecords[0].Value]" --output text \
|
|
146
|
+
| grep -i execute-api
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Across all seven TOGa zones this found exactly one: `webhook.togahub.com` →
|
|
150
|
+
`d-yiy4od2ade.execute-api.us-east-1.amazonaws.com`.
|
|
151
|
+
|
|
152
|
+
**An unknown account ID in `InUseBy` can be AWS itself.** The expired cert's `InUseBy` listed 3
|
|
153
|
+
ALBs in account **250044486744**, which no one has a profile for. That is AWS-managed edge
|
|
154
|
+
infrastructure — an edge-optimized API Gateway domain terminates TLS on AWS's own load balancers.
|
|
155
|
+
Treat it as a pointer to API Gateway, **not** as a rogue account to go hunt credentials for.
|
|
156
|
+
|
|
157
|
+
**Permissions block this from the CLI.** The `GoAgilant-Developers` SSO role has **no**
|
|
158
|
+
`apigateway:GET` in any of the three accounts (654654170868, 502614707982, 975050298201) —
|
|
159
|
+
`get-domain-names`, `get-domain-name` and the `apigatewayv2` equivalents all return
|
|
160
|
+
`AccessDeniedException`. The domain cannot be listed, read or fixed with the standard developer
|
|
161
|
+
role. Use the Console, or get `apigateway:GET` + `apigateway:PATCH` added first.
|
|
162
|
+
|
|
163
|
+
**Check the endpoint type FIRST — it decides which field you patch.** `get-domain-name` returns
|
|
164
|
+
`endpointConfiguration.types`. An **EDGE** domain reads its cert from `us-east-1` and uses
|
|
165
|
+
`/certificateArn`; a **REGIONAL** domain reads from its own region and uses
|
|
166
|
+
`/regionalCertificateArn`. Patching the wrong field silently does nothing useful.
|
|
167
|
+
`webhook.togahub.com` turned out to be **REGIONAL** (us-east-1), despite `InUseBy` showing
|
|
168
|
+
AWS-managed edge ALBs — so do not infer the type from `InUseBy`.
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
aws apigateway get-domain-name --domain-name webhook.togahub.com \
|
|
172
|
+
--query 'endpointConfiguration.types'
|
|
173
|
+
|
|
174
|
+
aws apigateway update-domain-name --domain-name webhook.togahub.com \
|
|
175
|
+
--patch-operations op=replace,path=/regionalCertificateArn,value=<new-arn>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The domain goes `UPDATING` and takes a few minutes to return to `AVAILABLE` (~4 min observed).
|
|
179
|
+
Poll `domainNameStatus` before verifying — checking too early shows the old cert and looks like a
|
|
180
|
+
failed patch. No downtime beyond the already-broken TLS; base path mappings are untouched.
|
|
181
|
+
|
|
126
182
|
## Step 7 — Check alias coverage before any CloudFront swap
|
|
127
183
|
|
|
128
184
|
CloudFront **rejects** an update if any alias on the distribution is not covered by the new cert.
|
|
@@ -155,11 +211,22 @@ echo | openssl s_client -servername HOST -connect HOST:443 2>/dev/null \
|
|
|
155
211
|
| openssl x509 -noout -issuer -enddate
|
|
156
212
|
```
|
|
157
213
|
|
|
214
|
+
`openssl` shows you the cert, but it does **not** prove a client can connect. Add a real verify —
|
|
215
|
+
and treat a known-bad host as the control that proves your check actually fails:
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
curl -sS -o /dev/null -w "http=%{http_code} ssl=%{ssl_verify_result}\n" https://HOST/
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
A live expired cert returns `http=000` with `SSL certificate problem: certificate has expired`.
|
|
222
|
+
|
|
158
223
|
Also run it **without** `-servername` to check the no-SNI / default-cert path — that path can still
|
|
159
224
|
be serving the old cert after everything else looks fine.
|
|
160
225
|
|
|
161
|
-
Finish by re-running the step 1 audit: every CloudFront distribution, every ALB listener,
|
|
162
|
-
EB saved config should name a cert with a future
|
|
226
|
+
Finish by re-running the step 1 audit: every CloudFront distribution, every ALB listener, every
|
|
227
|
+
EB saved config **and every API Gateway custom domain** should name a cert with a future
|
|
228
|
+
`NotAfter`. A sweep that omits any one of those four resource types is not a clean result — it is
|
|
229
|
+
an untested one.
|
|
163
230
|
|
|
164
231
|
## Gotchas
|
|
165
232
|
|
|
@@ -170,9 +237,28 @@ EB saved config should name a cert with a future `NotAfter`.
|
|
|
170
237
|
- **CloudFront takes one cert; ALBs take many.** Two completely different risk profiles in the same
|
|
171
238
|
rotation — plan them separately.
|
|
172
239
|
- **Wildcards match one label only.** Verify alias coverage with code, not by eye.
|
|
240
|
+
- **API Gateway custom domains are a fourth resource type.** A CloudFront + ALB + EB sweep misses
|
|
241
|
+
them completely. Find them by `execute-api` CNAMEs in Route 53. See step 6b.
|
|
242
|
+
- **An unknown account ID in `InUseBy` may be AWS itself.** `250044486744` is AWS-managed edge
|
|
243
|
+
infrastructure for API Gateway, not a rogue account. See step 6b.
|
|
244
|
+
- **The `GoAgilant-Developers` role needs `apigateway:GET` + `apigateway:PATCH`** on
|
|
245
|
+
`arn:aws:apigateway:*::/domainnames*`. Added to the permission set 2026-09-17; it was missing
|
|
246
|
+
during the rotation. Granted via IAM Identity Center, then **provisioned** to the account — the
|
|
247
|
+
provision step is what actually applies it. See step 6b.
|
|
248
|
+
- **Patch the field that matches the endpoint type.** REGIONAL uses `/regionalCertificateArn`,
|
|
249
|
+
EDGE uses `/certificateArn`. See step 6b.
|
|
250
|
+
- **`openssl` is not proof of health.** It prints the cert even when clients cannot connect. Use
|
|
251
|
+
`curl` and check for `http=000`. See step 9.
|
|
173
252
|
- **Expired certs hide.** Sweep the whole account; do not trust `InUseBy`.
|
|
174
253
|
|
|
175
254
|
## Change history
|
|
255
|
+
- 2026-09-17 — Added step 6b (API Gateway custom domains) after `webhook.togahub.com` was found
|
|
256
|
+
serving an expired cert and failing TLS the day after the rotation reported all-clear; added the
|
|
257
|
+
`curl` proof step, the AWS-owned-account `InUseBy` tell, and the missing `apigateway` permission.
|
|
258
|
+
**Fixed the same day:** `apigateway:GET`/`PATCH` added to the permission set, domain confirmed
|
|
259
|
+
REGIONAL, `regionalCertificateArn` swapped to the Amazon-issued `*.togahub.com` cert
|
|
260
|
+
(`a53d1ff6-...`, expires 2027-03-31). Verified `http=404 ssl=0` (was `http=000 ssl=10`), with
|
|
261
|
+
`expired.badssl.com` as the control that still fails. (rgirish)
|
|
176
262
|
- 2026-09-16 — Created from the account 654654170868 rotation: replaced an expiring 14-SAN IMPORTED
|
|
177
263
|
cert with 6 per-domain Amazon-issued DNS-validated certs, relinked 12 ALB listeners, 6 CloudFront
|
|
178
264
|
distributions and 11 EB environment configs; documented the EB saved-config trap. (rgirish)
|
|
@@ -2,22 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Summary |
|
|
4
4
|
|-----|---------|
|
|
5
|
-
| [TOGa Commerce (toga2-commerce / commerce2-react) Architecture](architecture.md) |
|
|
5
|
+
| [TOGa Commerce (toga2-commerce / commerce2-react) Architecture](architecture.md) | Whole-repo map of the toga2-commerce React storefront (bootstrap, routing, MVVM pages, state, API, multi-tenant resolution); open when working anywhere in this |
|
|
6
6
|
| [Bundle item visibility & selectability (the three flags, and why they are enforced nowhere but the client)](features/bundle-item-visibility-and-selectability.md) | Which components of a kit a shopper can see and choose is decided **entirely in the browser**. |
|
|
7
7
|
| [Cart Bundle Submission & the bundleUuid Identity Contract](features/cart-bundle-submission-and-identity.md) | How cart **bundles** (kits) are turned into `SalesOrderItems` when a cart is submitted or an existing order is edited, and the **identity-field contract** every |
|
|
8
|
-
| [Cart Notification Emails — auto-add/remove lifecycle + duplicate prevention](features/cart-notification-emails.md) |
|
|
8
|
+
| [Cart Notification Emails — auto-add/remove lifecycle + duplicate prevention](features/cart-notification-emails.md) | Cart Notifications section: CC-email duplicate prevention + auto-add/remove of order-for-user & delegate-manager emails; open when touching notification emails |
|
|
9
9
|
| [Cart Order-Total & Shipping Computation](features/cart-order-total-computation.md) | The Cart summary section (subtotal / shipping / tax / total) is **data-driven** from `cartData`. |
|
|
10
10
|
| [Cart Page — config-driven form architecture (current state + planned refactor)](features/cart-page-config-architecture.md) | The Cart page (`src/pages/Cart/`) is the most config-heavy page in `toga2-commerce`. |
|
|
11
11
|
| [Catalog cache freshness — the 24h persisted query cache, and how to opt a query out of it](features/catalog-cache-freshness.md) | TOGa Commerce runs a **single `QueryClient` with a 24-hour default `staleTime`**, and persists it to **`localStorage["commerce"]`** through `PersistQueryClientP |
|
|
12
12
|
| [Category Tile Order (AssortmentItems.sortOrder) — merchandising a storefront category](features/category-tile-sort-order.md) | **"Move item X to the front of category Y" is a DATA change, not a code change.** The order of item tiles on a storefront category page is driven by exactly one |
|
|
13
|
-
| [Client Fields — per-tenant / language / role content & config](features/client-fields.md) |
|
|
13
|
+
| [Client Fields — per-tenant / language / role content & config](features/client-fields.md) | Per-tenant/language/role JSON config that drives almost all commerce UI text, fields, and login fetches (Layer A global + Layer B FIELDS registry); open before |
|
|
14
14
|
| [Config-Driven Expedited Shipping Gating (Cart)](features/expedited-shipping-gating.md) | On the toga2-commerce **Cart** page, expedited shipping options (**"2nd Day EOB"** and **"Next Day Air"**) are only offered in the *Shipping Method* dropdown wh |
|
|
15
15
|
| [Filter / Search-Results Page & the Two Search Entry Points](features/filter-search-results-page.md) | The storefront has **two distinct search entry points that render the same card component through completely different code paths and different FIELDS files**. |
|
|
16
|
+
| [Header "Order for <Name>" dropdown + the pending-pick hand-off to the cart](features/header-order-for-dropdown.md) | Header pill that lets a buyer pick who they are ordering for from any page, and the in-memory store that hands that pick to the cart's order-for cascade; open b |
|
|
16
17
|
| [Inactive-item purchase gating (standalone lines only — kits are exempt by design)](features/inactive-item-purchase-gating.md) | An item with **`Items.isActive = 0`** must not be viewable, addable to a cart, or orderable **as a standalone line** on the storefront — but the **same flag is |
|
|
17
18
|
| [Multi-Tenant Resolution & Theming](features/multi-tenant-theming.md) | `toga2-commerce` serves multiple clients from one codebase. |
|
|
18
19
|
| [Order-submit sync sequencing (useSubmitOrder) — why these calls must not run in parallel](features/order-submit-sync-sequencing.md) | Submitting an order from the cart fires **two independent sync routines** — one for the sales-order header (`syncSalesOrderData`) and one for the line items (`s |
|
|
19
20
|
| [Config-Driven Shipping Cost Waiver (Standard Ground free for computer kits)](features/shipping-cost-waiver-gating.md) | On the toga2-commerce **Cart** page, a shipping option's **cost** can be waived by config using the same `PrimaryItemShippingRule` vocabulary that drives expedi |
|
|
20
21
|
| [Storefront persona gating — the Home page's kit / category / item counts, and the persona switcher](features/storefront-persona-gating-and-home-counts.md) | What a storefront user sees on the Home page is decided **in the front end**, from `user._personaIds`, not by the API's ACL. |
|
|
21
22
|
| [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**. |
|
|
22
|
-
| [Cart e2e — Cypress conventions & harness (toga2-commerce)](workflows/cypress-testing.md) |
|
|
23
|
+
| [Cart e2e — Cypress conventions & harness (toga2-commerce)](workflows/cypress-testing.md) | Cypress e2e conventions + the Cart harness (cartV2.cy.ts) for toga2-commerce; read before adding any spec here, and note it doubles as the config-cart refactor |
|
|
23
24
|
| [Diagnosing ERR_HTTP2_PROTOCOL_ERROR (one client fails, everyone else is fine)](workflows/http2-protocol-error-diagnosis.md) | When a Chromium browser (Chrome / Edge) shows **`ERR_HTTP2_PROTOCOL_ERROR`** loading a `*.togacommerce.com` tenant for **one client/network but works for the TO |
|
|
@@ -6,7 +6,7 @@ project: TOGa Commerce
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-17
|
|
10
10
|
owners: ["apeterson", "tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- src/main.tsx
|
|
@@ -29,22 +29,15 @@ related:
|
|
|
29
29
|
- 2.0/apps/toga2-commerce/features/filter-search-results-page.md
|
|
30
30
|
---
|
|
31
31
|
|
|
32
|
+
Whole-repo map of the toga2-commerce React storefront (bootstrap, routing, MVVM pages, state, API, multi-tenant resolution); open when working anywhere in this repo.
|
|
33
|
+
|
|
32
34
|
## Summary
|
|
33
35
|
|
|
34
|
-
`toga2-commerce` (npm package
|
|
35
|
-
customer-facing **B2B commerce storefront** of the 2.0 platform. It is a **React 18 + TypeScript +
|
|
36
|
-
Vite 5 single-page app** styled with **Tailwind**, and it is the React front end in the 2.0
|
|
37
|
-
"decoupled" architecture: it talks to **`api2`** (the TOGa v2 JSON API) for all data, and renders
|
|
38
|
-
nothing server-side.
|
|
36
|
+
`toga2-commerce` (npm package **`commerce2-react`**, product **TOGa Commerce**) is the customer-facing **B2B commerce storefront** of the 2.0 platform: a **React 18 + TypeScript + Vite 5 SPA** styled with **Tailwind**. It is the React front end in the 2.0 "decoupled" architecture — talks to **`api2`** (the TOGa v2 JSON API) for all data, renders nothing server-side. It is a 2.0 *app with no PHP*: registry `dependsOn` is `api2`; it consumes `_underscore`/`api2` over HTTP but contains no framework PHP classes.
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
**COMPASSCANADA**, **QUAD**, plus a **DEFAULT** fallback). The tenant is resolved at runtime from
|
|
42
|
-
the **hostname** and drives three independent systems — the API base URL, the theme, and the
|
|
43
|
-
per-tenant field/content config. See [multi-tenant-theming](features/multi-tenant-theming.md) and
|
|
44
|
-
[client-fields](features/client-fields.md) for those subsystems in depth.
|
|
38
|
+
**Multi-tenant:** one codebase serves several clients (**COMPASS**, **COMPASSCANADA**, **QUAD**, plus a **DEFAULT** fallback). The tenant is resolved at runtime from the **hostname** and drives three independent systems — API base URL, theme, per-tenant field/content config. See [multi-tenant-theming](features/multi-tenant-theming.md) and [client-fields](features/client-fields.md).
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
> `_underscore`/`api2` backend over HTTP but contains no framework PHP classes itself.
|
|
40
|
+
**Critical rules:** Tenant is derived from hostname only (`hostname.toUpperCase().split(".")[0]`) — wrong host = wrong tenant, and `localhost` falls back (fields→COMPASS, theme→DEFAULT). The single `QueryClient` uses a **24h default `staleTime`** and persists the whole cache to **`localStorage["commerce"]`**, so catalog AND config/copy (FIELDS) can be up to 24h stale for a returning user — verify any data/copy change after `localStorage.removeItem('commerce')` or logout, never on a warm browser. `AuthLayout` never remounts, so `refetchOnMount` fires once per session — route-change refreshes must be wired explicitly. The registry French language key is **`fr-CA`**, not `fr` — branch on `startsWith("fr")`. api2 returns the standard `{success,data,errors}` envelope; do not break the shape.
|
|
48
41
|
|
|
49
42
|
## Tech stack (verified versions, package.json)
|
|
50
43
|
|
|
@@ -67,10 +60,8 @@ E2E tests use **Cypress** (`cypress/`, `cypress.config.ts`).
|
|
|
67
60
|
|
|
68
61
|
## Bootstrap & provider stack
|
|
69
62
|
|
|
70
|
-
- `src/main.tsx` — initializes **Sentry** (browser tracing + replay)
|
|
71
|
-
|
|
72
|
-
- `src/App.tsx` — creates a single `QueryClient` (default `staleTime: 24h`) and a
|
|
73
|
-
`createSyncStoragePersister` (localStorage key **`"commerce"`**). Provider nesting, **outermost → innermost**:
|
|
63
|
+
- `src/main.tsx` — initializes **Sentry** (browser tracing + replay), mounts `<App/>` in React `StrictMode`, imports global CSS.
|
|
64
|
+
- `src/App.tsx` — creates a single `QueryClient` (default `staleTime: 24h`) and a `createSyncStoragePersister` (localStorage key **`"commerce"`**). Provider nesting, **outermost → innermost**:
|
|
74
65
|
|
|
75
66
|
```
|
|
76
67
|
PersistQueryClientProvider (client=queryClient, persister → localStorage "commerce")
|
|
@@ -81,13 +72,8 @@ E2E tests use **Cypress** (`cypress/`, `cypress.config.ts`).
|
|
|
81
72
|
+ ReactQueryDevtools (initialIsOpen=false)
|
|
82
73
|
```
|
|
83
74
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
outside `/cart`, and subscribes to `useReturnOriginalUserFromViewAsStore` (the "view-as" feature).
|
|
87
|
-
|
|
88
|
-
> The cache is **persisted**: query results survive refreshes via `PersistQueryClientProvider`.
|
|
89
|
-
> Combined with the 24h default `staleTime`, catalog data is aggressively cached — invalidate
|
|
90
|
-
> deliberately with `queryClient.invalidateQueries(...)` when freshness matters.
|
|
75
|
+
- `App` runs a mount effect managing **edit-order mode**: reads a `localStorage.synced` flag and calls `exitEditOrderModeGlobalSyncReset()` when an edit-order session was abandoned outside `/cart`; subscribes to `useReturnOriginalUserFromViewAsStore` (the "view-as" feature).
|
|
76
|
+
- The cache is **persisted**: query results survive refreshes. Combined with the 24h default `staleTime`, catalog data is aggressively cached — invalidate deliberately with `queryClient.invalidateQueries(...)` when freshness matters.
|
|
91
77
|
|
|
92
78
|
## Routing (`src/routes.tsx`, createBrowserRouter)
|
|
93
79
|
|
|
@@ -97,17 +83,11 @@ Public:
|
|
|
97
83
|
- `/reset-password` → `ResetPasswordPage`
|
|
98
84
|
|
|
99
85
|
Authenticated (wrapped by `PrivateRoute` → `AuthLayout`):
|
|
100
|
-
- `/home`, `/bundle-view`, `/filter`, `/cart`, `/get-support`, `/account`, `/item-view`,
|
|
101
|
-
`/order-details`, `/privacy-policy`, `/terms-conditions`; unmatched → `Navigate to="/home"`.
|
|
86
|
+
- `/home`, `/bundle-view`, `/filter`, `/cart`, `/get-support`, `/account`, `/item-view`, `/order-details`, `/privacy-policy`, `/terms-conditions`; unmatched → `Navigate to="/home"`.
|
|
102
87
|
|
|
103
88
|
Mechanics:
|
|
104
|
-
- **`PrivateRoute`** gates on `useAuth().isAuthenticated` + `useAuthenticationFlow().isLoading`;
|
|
105
|
-
|
|
106
|
-
- **`AuthLayout`** wraps every authenticated route and **stays mounted across route changes**
|
|
107
|
-
(header, footer, nav, notifications live here). Its `errorElement` renders `<ErrorMessage errorType={500}/>`.
|
|
108
|
-
⚠️ Because it never remounts, React Query `refetchOnMount` only fires **once per session** for
|
|
109
|
-
queries owned by AuthLayout — route-change refreshes must be wired explicitly (e.g. the
|
|
110
|
-
notifications query invalidates on route change keyed on `user?.uuid`).
|
|
89
|
+
- **`PrivateRoute`** gates on `useAuth().isAuthenticated` + `useAuthenticationFlow().isLoading`; shows an `AuthLoading` spinner while loading, redirects to `/login` if unauthenticated.
|
|
90
|
+
- **`AuthLayout`** wraps every authenticated route and **stays mounted across route changes** (header, footer, nav, notifications live here). Its `errorElement` renders `<ErrorMessage errorType={500}/>`. Because it never remounts, `refetchOnMount` fires **once per session** for queries owned by AuthLayout — route-change refreshes must be wired explicitly (e.g. the notifications query invalidates on route change keyed on `user?.uuid`).
|
|
111
91
|
- Routes are **statically imported** (no `React.lazy`); Vite handles chunking at build time.
|
|
112
92
|
|
|
113
93
|
## Page architecture — MVVM convention
|
|
@@ -128,10 +108,7 @@ src/pages/<Page>/
|
|
|
128
108
|
└── helpers/ # pure compute/format helpers
|
|
129
109
|
```
|
|
130
110
|
|
|
131
|
-
Pages: `Account, BundleView, Cart, Filter, GetSupport, Home, ItemsView, Login, OrderDetails,
|
|
132
|
-
PrivacyPolicy, ResetPassword, TermsConditions`. The **ViewModel hook is the seam**: it calls
|
|
133
|
-
`useAssignClientFields(fieldKey, language, user)` to load the right tenant/role/language content,
|
|
134
|
-
runs the page's queries, and returns ready-to-render data to the View. (See client-fields doc.)
|
|
111
|
+
Pages: `Account, BundleView, Cart, Filter, GetSupport, Home, ItemsView, Login, OrderDetails, PrivacyPolicy, ResetPassword, TermsConditions`. The **ViewModel hook is the seam**: it calls `useAssignClientFields(fieldKey, language, user)` to load the right tenant/role/language content, runs the page's queries, and returns ready-to-render data to the View. (See client-fields doc.)
|
|
135
112
|
|
|
136
113
|
## State management
|
|
137
114
|
|
|
@@ -147,6 +124,7 @@ Most use the `persist` middleware (localStorage). Verified files:
|
|
|
147
124
|
| `useEditOrderZu` | "Edit existing order" mode flag + original snapshot |
|
|
148
125
|
| `useEditOrderUuidStore` | UUID of the order being edited |
|
|
149
126
|
| `useSelectedUserZu` | "Order for" user selected in cart |
|
|
127
|
+
| `useOrderForZu` | Header "order for" **pending pick** handed to the cart cascade — **in memory only, never `persist`ed** |
|
|
150
128
|
| `useReturnOriginalUserFromViewAsStore` | "View as another user" return-to-self tracking |
|
|
151
129
|
| `useEmailOptionsStore` | Notification CC email options (see cart-notification-emails) |
|
|
152
130
|
| `useBundleBuilderZu` | Bundle-builder wizard selections |
|
|
@@ -159,59 +137,35 @@ Most use the `persist` middleware (localStorage). Verified files:
|
|
|
159
137
|
- Cache persisted to localStorage (`"commerce"`); rehydrated on load.
|
|
160
138
|
- Page queries are typically `enabled` on `isAuthenticated && user?.uuid && !settingsModalEnabled`.
|
|
161
139
|
|
|
162
|
-
**Two-layer state model:** React Query owns *server* state (catalog, orders, user); Zustand owns
|
|
163
|
-
*local* state (cart, selected user, edit mode, tenant/language). The cart is synced to the backend
|
|
164
|
-
sales order via `src/api/syncSalesOrder*.ts` helpers.
|
|
140
|
+
**Two-layer state model:** React Query owns *server* state (catalog, orders, user); Zustand owns *local* state (cart, selected user, edit mode, tenant/language). The cart is synced to the backend sales order via `src/api/syncSalesOrder*.ts` helpers.
|
|
165
141
|
|
|
166
142
|
## API layer (`src/api/`)
|
|
167
143
|
|
|
168
|
-
`src/api/axiosInstance.ts` is the shared client.
|
|
169
|
-
- **Base URL by tenant:** `host = "VITE_API_" + window.location.hostname.toUpperCase().split(".")[0]`;
|
|
170
|
-
`baseURL = import.meta.env[host] || import.meta.env.VITE_API`. So `compass.togacommerce` →
|
|
171
|
-
`VITE_API_COMPASS`, falling back to `VITE_API`. `timeout: 180000`.
|
|
144
|
+
`src/api/axiosInstance.ts` is the shared client. Verified behaviors:
|
|
145
|
+
- **Base URL by tenant:** `host = "VITE_API_" + window.location.hostname.toUpperCase().split(".")[0]`; `baseURL = import.meta.env[host] || import.meta.env.VITE_API`. So `compass.togacommerce` → `VITE_API_COMPASS`, falling back to `VITE_API`. `timeout: 180000`.
|
|
172
146
|
- **Transaction id:** a request interceptor stamps every call with `params.transactionId = uuidv4()`.
|
|
173
|
-
- **Auth token:** a second request interceptor attaches `Authorization: Bearer <token>` — using the
|
|
174
|
-
stored `accessToken` if a `user` exists in localStorage, otherwise fetching a **public token** via
|
|
175
|
-
`POST {baseURL}/auth/public`.
|
|
147
|
+
- **Auth token:** a second request interceptor attaches `Authorization: Bearer <token>` — using the stored `accessToken` if a `user` exists in localStorage, otherwise fetching a **public token** via `POST {baseURL}/auth/public`.
|
|
176
148
|
- **1 ms delay interceptor:** a third request interceptor `await delay(1)` before sending.
|
|
177
|
-
- **401 handling:** response interceptor attempts `POST {baseURL}/auth/refresh` (Bearer refresh
|
|
178
|
-
token) once (`_retry`), retries the original request on success, else calls `performLogout()`
|
|
179
|
-
(clears all auth + cart + `commerce` cache keys and redirects to `/`).
|
|
149
|
+
- **401 handling:** response interceptor attempts `POST {baseURL}/auth/refresh` (Bearer refresh token) once (`_retry`), retries the original request on success, else calls `performLogout()` (clears all auth + cart + `commerce` cache keys and redirects to `/`).
|
|
180
150
|
- Failures are reported to **Sentry** with `errorType: "API"` tags.
|
|
181
151
|
|
|
182
|
-
The backend (`api2`) returns the standard 2.0 envelope (`success`/`data`/`errors`; see api2 arch);
|
|
183
|
-
generic CRUD helpers live in `src/api/genericApi.ts` (paginated `getData`, `saveData`, `updateData`,
|
|
184
|
-
`deleteData`, optional `?depth=-1` for nested responses).
|
|
152
|
+
The backend (`api2`) returns the standard 2.0 envelope (`success`/`data`/`errors`; see api2 arch); generic CRUD helpers live in `src/api/genericApi.ts` (paginated `getData`, `saveData`, `updateData`, `deleteData`, optional `?depth=-1` for nested responses).
|
|
185
153
|
|
|
186
154
|
## Authentication & roles
|
|
187
155
|
|
|
188
|
-
- `src/contexts/AuthContext.tsx` derives `host` from the hostname (`hostname.toUpperCase().split(".")[0]`),
|
|
189
|
-
|
|
190
|
-
(`determineShouldShowPersonaSwitcher` — QUAD special-cased), then runs
|
|
191
|
-
`getLoginSettings(...)` which writes `useFieldsStore.fieldKey = host` and the language, and
|
|
192
|
-
hydrates `useUserStore`.
|
|
193
|
-
- `src/hooks/useAuthenticationFlow.ts` orchestrates the post-login fetch sequence
|
|
194
|
-
(user data → persona → contact → settings).
|
|
156
|
+
- `src/contexts/AuthContext.tsx` derives `host` from the hostname (`hostname.toUpperCase().split(".")[0]`), loads `getClientLoginFields(host)` (tenant config), decides persona-switcher visibility (`determineShouldShowPersonaSwitcher` — QUAD special-cased), then runs `getLoginSettings(...)` which writes `useFieldsStore.fieldKey = host` and the language, and hydrates `useUserStore`.
|
|
157
|
+
- `src/hooks/useAuthenticationFlow.ts` orchestrates the post-login fetch sequence (user data → persona → contact → settings).
|
|
195
158
|
- **Role flags** on the user object are tenant-specific:
|
|
196
|
-
- COMPASS / COMPASSCANADA → `ADMIN` (`_isAdmin`), `SUPERUSER` (`_isSuperAdmin`),
|
|
197
|
-
`MANAGER` (`_isSupervisor`), else `USER`.
|
|
159
|
+
- COMPASS / COMPASSCANADA → `ADMIN` (`_isAdmin`), `SUPERUSER` (`_isSuperAdmin`), `MANAGER` (`_isSupervisor`), else `USER`.
|
|
198
160
|
- QUAD → `GLOBALADMIN` (`_isGlobalAdmin`), `BUYER` (`_isBuyer`), `ITSHOPPER` (`_isItShopper`), else `USER`.
|
|
199
|
-
- **Personas:** `user._personaIds` is a colon-delimited string; `getLoginSettings` has special
|
|
200
|
-
handling collapsing persona-24 cases for non-QUAD tenants.
|
|
161
|
+
- **Personas:** `user._personaIds` is a colon-delimited string; `getLoginSettings` has special handling collapsing persona-24 cases for non-QUAD tenants.
|
|
201
162
|
|
|
202
163
|
## Build & deploy
|
|
203
164
|
|
|
204
|
-
- **Per-tenant dev/build scripts** (`package.json`): `compass`, `compasscanada`, `quad`,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
- `vite.config.ts`: React plugin + Sentry Vite plugin (sourcemap upload); dev `server.allowedHosts`
|
|
209
|
-
= `compass.togacommerce`, `compasscanada.togacommerce`, `quad.togacommerce`; PostCSS via
|
|
210
|
-
`postcss.config.cjs`; `optimizeDeps.include: ["react-router-dom"]`.
|
|
211
|
-
- Local dev requires `*.togacommerce` hostnames to resolve to localhost (hosts file) so tenant
|
|
212
|
-
resolution works.
|
|
213
|
-
- Deployment runs on **AWS Amplify** — see [amplify-build-and-deploy](workflows/amplify-build-and-deploy.md).
|
|
214
|
-
A `Dockerfile`/`docker-compose.yml` also exist for containerized dev.
|
|
165
|
+
- **Per-tenant dev/build scripts** (`package.json`): `compass`, `compasscanada`, `quad`, `togacommerce` run `vite --mode development --host <tenant>.togacommerce`; production builds are `build` / `buildAlpha` / `buildBeta` / `buildGamma` / `buildQcSecurity` (each installs the matching `@agilant/toga-blox` npm channel, then `tsc` + `vite build --mode <env>`).
|
|
166
|
+
- `vite.config.ts`: React plugin + Sentry Vite plugin (sourcemap upload); dev `server.allowedHosts` = `compass.togacommerce`, `compasscanada.togacommerce`, `quad.togacommerce`; PostCSS via `postcss.config.cjs`; `optimizeDeps.include: ["react-router-dom"]`.
|
|
167
|
+
- Local dev requires `*.togacommerce` hostnames to resolve to localhost (hosts file) so tenant resolution works.
|
|
168
|
+
- Deployment runs on **AWS Amplify** — see [amplify-build-and-deploy](workflows/amplify-build-and-deploy.md). A `Dockerfile`/`docker-compose.yml` also exist for containerized dev.
|
|
215
169
|
|
|
216
170
|
## Directory map (`src/`)
|
|
217
171
|
|
|
@@ -234,26 +188,8 @@ generic CRUD helpers live in `src/api/genericApi.ts` (paginated `getData`, `save
|
|
|
234
188
|
|
|
235
189
|
## Gotchas
|
|
236
190
|
|
|
237
|
-
- **`AuthLayout` never remounts** → `refetchOnMount` fires once per session; wire explicit
|
|
238
|
-
|
|
239
|
-
- **
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
`localStorage["commerce"]` and sees stale copy for up to 24h after a build that contains the new
|
|
243
|
-
JSON. Verify any FIELDS change after `localStorage.removeItem('commerce')` or a logout, never on a
|
|
244
|
-
warm browser. A build-version `buster` in `persistOptions` would fix this at deploy time but
|
|
245
|
-
invalidates every persisted query app-wide — **open team decision, not implemented.**
|
|
246
|
-
- **Tenant resolution depends on the hostname.** On `localhost` with no `*.togacommerce` host the
|
|
247
|
-
first DNS label won't match a tenant, so config falls back (`getClientLoginFields` → COMPASS,
|
|
248
|
-
theme → DEFAULT). Use the per-tenant dev scripts.
|
|
249
|
-
- **Theme vs. fields language keys differ** — theme/field *folder* names are uppercase
|
|
250
|
-
(`COMPASSCANADA`, `ENGLISH`/`FRENCH`) but the runtime `FIELDS` registry keys language as
|
|
251
|
-
`en` / **`fr-CA`** (not `fr`). Any language branch must use `startsWith("fr")` — a `=== "fr"`
|
|
252
|
-
comparison silently never matches. See the client-fields doc.
|
|
253
|
-
|
|
254
|
-
## Change history
|
|
255
|
-
- 2026-07-28 — Gotchas: recorded that the 24h persisted React Query cache also masks `FIELDS`
|
|
256
|
-
config/copy deploys (verify after clearing `localStorage["commerce"]`; `persistOptions.buster` is
|
|
257
|
-
an open team decision), and corrected the runtime language key to `fr-CA` requiring
|
|
258
|
-
`startsWith("fr")` (tcox)
|
|
259
|
-
</content>
|
|
191
|
+
- **`AuthLayout` never remounts** → `refetchOnMount` fires once per session; wire explicit invalidation for data that must refresh on navigation.
|
|
192
|
+
- **24h `staleTime` + persisted cache** → users can see stale catalog/pricing; invalidate on the events that should bust it. **This also masks config/copy deploys:** `FIELDS` is served *through* the `["clientFields", …]` query, so a returning user rehydrates the old labels from `localStorage["commerce"]` and sees stale copy for up to 24h after a build that contains the new JSON. Verify any FIELDS change after `localStorage.removeItem('commerce')` or a logout, never on a warm browser. A build-version `buster` in `persistOptions` would fix this at deploy time but invalidates every persisted query app-wide — **open team decision, not implemented.**
|
|
193
|
+
- **Tenant resolution depends on the hostname.** On `localhost` with no `*.togacommerce` host the first DNS label won't match a tenant, so config falls back (`getClientLoginFields` → COMPASS, theme → DEFAULT). Use the per-tenant dev scripts.
|
|
194
|
+
- **Theme vs. fields language keys differ** — theme/field *folder* names are uppercase (`COMPASSCANADA`, `ENGLISH`/`FRENCH`) but the runtime `FIELDS` registry keys language as `en` / **`fr-CA`** (not `fr`). Any language branch must use `startsWith("fr")` — a `=== "fr"` comparison silently never matches. See the client-fields doc.
|
|
195
|
+
- **Tailwind: not every `<n>px` name is a token.** `max-h-300px` does not exist (300px is only on the height/width scales) — write `max-h-[300px]`. `w-345px`, `h-300px`, `border-1` and `shadow-subMenu` do exist. A non-existent class silently renders unstyled.
|