toga-ai 1.0.468 → 1.0.469

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.
@@ -3,6 +3,7 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [TOGa View Frontend (toga2-view) Architecture](architecture.md) | `toga2-view` is the **React/TypeScript single-page frontend** for the TOGa 2.0 platform — the customer-facing web app (home warranty / tech-support portals). | toga2-view/src/main.tsx, toga2-view/src/App.tsx, toga2-view/src/routes.tsx, toga2-view/src/api/axiosInstance.ts, toga2-view/src/api/apiFunctions.ts, toga2-view/src/utils/queryHelpers.ts, toga2-view/src/contexts/AuthContext.tsx, toga2-view/src/contexts/useUserStore.ts, toga2-view/src/hooks/useAuthenticationFlow.ts, toga2-view/vite.config.ts, toga2-view/package.json |
6
+ | [Get Support — Subscription Details & Cancel Subscription Gating](features/get-support-cancel-subscription.md) | The Get Support page (`/get-support?itemUuid=…`) shows a **subscription details card** (plan · price · renewal date) for the service the user clicked, and — **o | toga2-view/src/pages/GetSupport/viewModels/getSupportPageViewModel.ts, toga2-view/src/pages/GetSupport/view/GetSupportPage.tsx, toga2-view/src/pages/GetSupport/viewModels/DUMMYFIELDS/SUPPORTDUMMYFIELDS.json, toga2-view/src/constants/featureFlags.ts, toga2-view/src/hooks/useBundleServices.ts |
6
7
  | [Mobile Nav Header](features/mobile-nav-header.md) | The `MobileNavToggle` component renders the fixed 72 px header (hamburger + Rate logo) and the slide-in drawer nav. | toga2-view/src/components/MobileNav/MobileNav.tsx, toga2-view/public/assets/Rate_Logo.svg |
7
8
  | [/v2 Query-String Builder (assembleOptions / where coercion)](features/query-string-builder.md) | `src/utils/queryHelpers.ts` converts a structured JS options object (`fields`, `where`, `join`/`ojoin`, `sort`, …) into the `api2` `/v2` query string. | toga2-view/src/utils/queryHelpers.ts, toga2-view/src/utils/queryHelpers.test.ts |
8
9
  | [Service Card Component](features/service-card.md) | The `ServiceCard` component renders a single service subscription (tech support or home warranty) on both the Home and Services pages. | toga2-view/src/components/ServiceCard/ServiceCard.tsx, toga2-view/src/pages/Services/view/ServicesPage.tsx, toga2-view/src/pages/Home/view/HomePage.tsx, toga2-view/src/constants/bundleConstants.ts, toga2-view/src/pages/Services/viewModels/DUMMYFIELDS/SERVICESDUMMYFIELDS.json |
@@ -0,0 +1,145 @@
1
+ ---
2
+ title: "Get Support — Subscription Details & Cancel Subscription Gating"
3
+ framework: "2.0"
4
+ repo: toga2-view
5
+ project: TOGa View Frontend
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-28
10
+ owners: ["tcox"]
11
+ files:
12
+ - toga2-view/src/pages/GetSupport/viewModels/getSupportPageViewModel.ts
13
+ - toga2-view/src/pages/GetSupport/view/GetSupportPage.tsx
14
+ - toga2-view/src/pages/GetSupport/viewModels/DUMMYFIELDS/SUPPORTDUMMYFIELDS.json
15
+ - toga2-view/src/constants/featureFlags.ts
16
+ - toga2-view/src/hooks/useBundleServices.ts
17
+ related:
18
+ - ../architecture.md
19
+ - ../../../../clients/rate/features/service-card-entitlements.md
20
+ - ../../../../clients/rate/profile.md
21
+ ---
22
+
23
+ ## Summary
24
+
25
+ The Get Support page (`/get-support?itemUuid=…`) shows a **subscription details card**
26
+ (plan · price · renewal date) for the service the user clicked, and — **only for tech-support
27
+ services** — a **Cancel Subscription** button plus a confirmation modal. Whole Home (WH) warranty
28
+ services and unclassified services get the subscription details card **without** any cancel path.
29
+
30
+ Two things about this flow are easy to get wrong and are the reason it is documented:
31
+
32
+ - The cancel affordance exists **only on this page** — `ServiceCard` has none.
33
+ - **There is no backend cancellation endpoint yet.** The viewModel's `cancelSubscription()` is a
34
+ stub that always rejects, so confirming surfaces an error instead of faking success.
35
+
36
+ ## Key files / entry points
37
+
38
+ - `pages/GetSupport/viewModels/getSupportPageViewModel.ts` — resolves `activeService`, builds the
39
+ `subscription` object, exposes `canCancelSubscription` and the `cancelSubscription()` stub
40
+ - `pages/GetSupport/view/GetSupportPage.tsx` — the **only** place a cancel affordance is rendered;
41
+ computes `showCancelUi` and owns the confirm-modal state (`showCancelModal`, `cancelError`)
42
+ - `constants/featureFlags.ts` — `CANCEL_SUBSCRIPTION_ENABLED` (currently `true`); flipping it to
43
+ `false` hides the whole cancel path in one place
44
+ - `viewModels/DUMMYFIELDS/SUPPORTDUMMYFIELDS.json` — copy: `renewsPrefix`, `cancelNote`
45
+ ("You can cancel anytime. Coverage continues until the end of your current period."),
46
+ `confirm.*` (including `confirm.errorMessage`)
47
+ - `hooks/useBundleServices.ts` — `detectServiceType()`, the classification the gate reuses
48
+
49
+ ## How it works
50
+
51
+ 1. **`activeService` resolution.** The viewModel picks the bundle service whose `uuid` matches the
52
+ URL's `itemUuid`, falls back to the first service with `isActive`, else `null`:
53
+ ```ts
54
+ const activeService =
55
+ bundleServices?.find((service) => service.uuid === itemUuid) ??
56
+ bundleServices?.find((service) => service.isActive) ??
57
+ null;
58
+ ```
59
+ The subscription card **and** the cancel gate both read this same `activeService`, so older links
60
+ without a resolvable `itemUuid` behave consistently across both.
61
+
62
+ 2. **Subscription details.** `subscription` = `{ plan, price, renewalDate, entitlementUuid }` from
63
+ `activeService`, else `null`. The card renders whenever `subscription` exists — **including for
64
+ warranty services**. Gating cancel does not hide the plan/price/renewal information.
65
+
66
+ 3. **The gate (positive, not negative).**
67
+ ```ts
68
+ const canCancelSubscription = activeService?.serviceType === "tech";
69
+ ```
70
+ `'warranty'` **and** `'other'` therefore both hide it — an unclassified service fails closed.
71
+ `serviceType` comes from `detectServiceType(bundleName || itemTitle)` in `useBundleServices`
72
+ (title-substring classification: contains "warranty"/"whole home" and not "tech" → `'warranty'`;
73
+ contains "tech"/"support" → `'tech'`; else `'other'`). This is the same signal that switches
74
+ `ServiceCard`'s second data row between **Price** (tech) and **Address** (warranty), so the cancel
75
+ gate and the service cards agree by construction.
76
+
77
+ 4. **The view gates three things with one boolean.**
78
+ ```ts
79
+ const showCancelUi = CANCEL_SUBSCRIPTION_ENABLED && canCancelSubscription;
80
+ ```
81
+ - the Cancel Subscription button
82
+ - the confirm modal (`showCancelUi && showCancelModal`)
83
+ - the `cancelNote` sentence appended to the `subscriptionDetails` string
84
+
85
+ The note is gated deliberately: leaving *"You can cancel anytime…"* on a card with no cancel
86
+ button is self-contradictory copy.
87
+
88
+ 5. **Confirm flow.** `handleConfirmCancel()` awaits `cancelSubscription()`; it closes the modal
89
+ **only** on a genuine resolve, and on rejection keeps the modal open and renders
90
+ `subscriptionFields.confirm.errorMessage`. Since the stub always throws, today's real-world
91
+ outcome is always the error state — intentional, so nothing reports a cancellation that did not
92
+ happen.
93
+
94
+ ## Decision — gate on `serviceType`, not on `isRateWarranty`
95
+
96
+ The page already carries a separate warranty signal: `isRateWarranty = bundleUuid ===
97
+ HOME_WARRANTY_BUNDLE_UUID`, used to route the **TICKETS** support method to the claims-style
98
+ create-ticket link. It was deliberately **not** reused for the cancel gate:
99
+
100
+ - `serviceType` is a *product classification*, so any service classified `'tech'` gets cancel —
101
+ it does not need a bundle uuid allowlist maintained per product.
102
+ - It keeps this page consistent with the service cards, which already branch on `serviceType`.
103
+ - A bundle-uuid check would silently fail open for any new warranty bundle uuid.
104
+
105
+ **Why WH is excluded at all:** Whole Home warranty contracts (the AIG-backed warranty product) are
106
+ not self-service cancellable from the portal; only tech-support subscriptions are.
107
+
108
+ ## Gotchas / known issues
109
+
110
+ - **The cancel UI lives only on `GetSupportPage.tsx`.** `ServiceCard` has no cancel affordance —
111
+ do not go looking for one in `components/`, and do not add a second entry point without
112
+ reproducing the same `showCancelUi` gate.
113
+ - **No backend endpoint exists.** `cancelSubscription()` throws
114
+ `"Subscription cancellation is not available yet."` by design (reject, never resolve). Swap the
115
+ body for the real API call when the endpoint ships; do not "fix" it by resolving.
116
+ - **`detectServiceType` now has a second consumer with a policy consequence.** Widening its
117
+ `'tech'` keywords does not just change a card row any more — it **exposes the cancel button** for
118
+ whatever newly matches. Check both call sites before touching the classifier.
119
+ - **The gate fails closed on `'other'`.** If a legitimately cancellable service stops matching
120
+ "tech"/"support" in its bundle/item title, its cancel button silently disappears. The title is the
121
+ only input.
122
+ - **`cancelNote` is not standalone copy** — it is concatenated into `subscriptionDetails` and gated
123
+ by `showCancelUi`. Any new surface rendering that string must gate it the same way.
124
+ - **`CANCEL_SUBSCRIPTION_ENABLED = false` hides button + modal + note but keeps the subscription
125
+ details card** — that is the intended kill-switch behavior.
126
+ - **Never close the confirm modal on failure.** Closing it reads to the user as a successful
127
+ cancellation.
128
+
129
+ ## Client variations
130
+
131
+ Rate is the consumer of this page today. Its product mix — Whole Home Warranty (AIG-backed) plus
132
+ tech support — is what makes the gate load-bearing: both products appear as active services for the
133
+ same user, so the page must distinguish them per service rather than per account. See
134
+ [Rate profile](../../../../clients/rate/profile.md) and
135
+ [Service Card Entitlement Display](../../../../clients/rate/features/service-card-entitlements.md).
136
+
137
+ ## Change history
138
+
139
+ - 2026-07-28 — Cancel Subscription UI gated by service type: viewModel exposes
140
+ `canCancelSubscription` (`activeService?.serviceType === "tech"`), and the view's `showCancelUi`
141
+ now gates the button, the confirm modal, and the `cancelNote` sentence. WH warranty and
142
+ unclassified services keep the subscription details card with no cancel path. Also first capture of
143
+ the pre-existing flow: cancel exists only on `GetSupportPage`, is flagged by
144
+ `CANCEL_SUBSCRIPTION_ENABLED`, and `cancelSubscription()` is a stub that always rejects until the
145
+ backend endpoint ships. (tcox)
@@ -23,7 +23,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
23
23
  - **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
24
24
  - **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
25
25
  - **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
26
- - **toga2-view** (TOGa View Frontend) — 6 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
26
+ - **toga2-view** (TOGa View Frontend) — 7 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
27
27
  - **toga2-hub** (TOGa Hub) — 2 doc(s) → [2.0/apps/toga2-hub/INDEX.md](2.0/apps/toga2-hub/INDEX.md)
28
28
  - **talos** (TOGa IQ) — 7 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
29
29
  - **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
@@ -6,7 +6,7 @@ project: TOGa View Frontend
6
6
  client: rate
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-07-27
9
+ updated: 2026-07-28
10
10
  owners: ["bala", "tcox", "mhammontree"]
11
11
  files:
12
12
  - src/components/ServiceCard/ServiceCard.tsx
@@ -24,6 +24,7 @@ files:
24
24
  related:
25
25
  - clients/rate/profile.md
26
26
  - whole-home-warranty-purchase-guard.md
27
+ - 2.0/apps/toga2-view/features/get-support-cancel-subscription.md
27
28
  ---
28
29
 
29
30
  ## Summary
@@ -101,6 +102,11 @@ This feature is Rate-specific. The warranty card showing Address instead of Pric
101
102
  by Rate's product mix (Whole Home Warranty + Tech Support). Other clients with different
102
103
  product types would need `detectServiceType` extended or overridden.
103
104
 
105
+ **`detectServiceType` is no longer display-only.** As of 2026-07-28 it also gates the
106
+ **Cancel Subscription** UI on the Get Support page (`serviceType === 'tech'` — WH warranty and
107
+ unclassified services get no cancel path). See
108
+ [Get Support — Subscription Details & Cancel Subscription Gating](../../../2.0/apps/toga2-view/features/get-support-cancel-subscription.md).
109
+
104
110
  ## Gotchas / known issues
105
111
 
106
112
  - **`ojoin` is required for address** — using INNER JOIN (`join`) drops every entitlement
@@ -115,6 +121,10 @@ product types would need `detectServiceType` extended or overridden.
115
121
  `AddSubscriptionSheet`) replaces the old "Get Started" inactive bundle cards.
116
122
  - **`useBundleServices` is shared** — both `useHomePageViewModel` and `useServicePageViewModel`
117
123
  call it. Changes to the hook affect both pages.
124
+ - **Widening `detectServiceType`'s `'tech'` keywords now changes entitlement *permissions*, not just
125
+ a card row.** The Get Support page gates its Cancel Subscription button on
126
+ `serviceType === 'tech'`, so anything newly classified as tech becomes self-service cancellable.
127
+ Review both consumers (`ServiceCard` row 2 and the cancel gate) before editing the classifier.
118
128
  - **Show the entitlement's OWN pin only — never the contact's shared address.** The removed
119
129
  `contact.primaryContactAddress` fallback made every unpinned card display the same (latest)
120
130
  address because that field is one shared, purchase-overwritten value per contact. An unpinned
@@ -137,6 +147,9 @@ product types would need `detectServiceType` extended or overridden.
137
147
 
138
148
  ## Change history
139
149
 
150
+ - 2026-07-28 — `detectServiceType` gained a second, non-display consumer: the Get Support page gates
151
+ its Cancel Subscription UI on `serviceType === 'tech'`, so the classifier now decides which
152
+ entitlements are self-service cancellable. (tcox)
140
153
  - 2026-07-27 — TRUE-79533: recorded that `fetchAddressesByIds`
141
154
  (`GET /v2/addresses?fields=id,line1,...`) returned **403 `EZ-2`** because the **`Addresses.id`
142
155
  field** had no read ACL grant in `Client_Rate` — the numeric `id` is ACL-gated like any field. Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.468",
3
+ "version": "1.0.469",
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",