@gymmymac/bob-widget 3.1.19 → 3.1.20
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/CHANGELOG.md +17 -0
- package/README.md +435 -38
- package/bin/bob-widget.mjs +1 -1
- package/dist/__tests__/Bob.callbacks.test.d.ts +10 -0
- package/dist/__tests__/bundleDiscount.test.d.ts +7 -0
- package/dist/__tests__/rearBrakeFilter.test.d.ts +7 -0
- package/dist/components/ProductTile.d.ts +4 -5
- package/dist/index.js +34 -34
- package/dist/index.mjs +3777 -3642
- package/dist/types/product.d.ts +9 -0
- package/dist/utils/rearBrakeFilter.d.ts +15 -0
- package/install/carfix/CARFIX-INSTALLATION-BRIEF.md +367 -0
- package/package.json +1 -1
package/dist/types/product.d.ts
CHANGED
|
@@ -15,6 +15,12 @@ export interface Product {
|
|
|
15
15
|
image_url?: string;
|
|
16
16
|
image?: string;
|
|
17
17
|
quantity?: number;
|
|
18
|
+
webDescription?: string;
|
|
19
|
+
brandDescription?: string;
|
|
20
|
+
perCarQty?: number;
|
|
21
|
+
volume?: string;
|
|
22
|
+
viscosity?: string;
|
|
23
|
+
brandImageUrl?: string;
|
|
18
24
|
}
|
|
19
25
|
/**
|
|
20
26
|
* API response format from CARFIX retrieve-parts endpoint
|
|
@@ -77,6 +83,9 @@ export interface PreparedTier {
|
|
|
77
83
|
isRecommended: boolean;
|
|
78
84
|
isHidden: boolean;
|
|
79
85
|
totalPrice: number;
|
|
86
|
+
originalTotalPrice?: number;
|
|
87
|
+
savingsAmount?: number;
|
|
88
|
+
bundleDiscountPercentage?: number;
|
|
80
89
|
productCount: number;
|
|
81
90
|
dominantBrand: string | null;
|
|
82
91
|
brands: PreparedTierBrand[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rear Brake Service: Disc vs Drum filter utility
|
|
3
|
+
* Used by mobile ServicePackageDetailView
|
|
4
|
+
*/
|
|
5
|
+
export type RearBrakeType = 'disc' | 'drum';
|
|
6
|
+
export declare function isRearBrakePackage(pkg: {
|
|
7
|
+
id?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
}): boolean;
|
|
10
|
+
export declare function filterByBrakeType<T extends {
|
|
11
|
+
partslotName: string;
|
|
12
|
+
}>(products: T[], brakeType: RearBrakeType): T[];
|
|
13
|
+
export declare function recalcTierTotal<T extends {
|
|
14
|
+
displayPrice: number;
|
|
15
|
+
}>(products: T[]): number;
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# Bob Widget — CARFIX Installation Brief
|
|
2
|
+
|
|
3
|
+
**Package:** `@gymmymac/bob-widget@3.1.19`
|
|
4
|
+
**Date:** 2026-02-13
|
|
5
|
+
**Status:** Production-ready, 36 unit tests passing, E2E baseline locked
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Peer Dependencies (Critical for Build)
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"react": "^18.0.0",
|
|
15
|
+
"react-dom": "^18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"dependencies (bundled — no action needed)": {
|
|
18
|
+
"@supabase/supabase-js": "^2.84.0",
|
|
19
|
+
"@tanstack/react-query": "^5.0.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
> If CARFIX already uses `@tanstack/react-query`, ensure versions are compatible (v5+).
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 2. Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @gymmymac/bob-widget@latest
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 3. Minimal Integration (4 lines)
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { BobStandalone } from '@gymmymac/bob-widget';
|
|
40
|
+
|
|
41
|
+
function AskBobPage() {
|
|
42
|
+
const router = useRouter();
|
|
43
|
+
const sessionToken = router.query.session as string;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div style={{
|
|
47
|
+
height: 'calc(100dvh - 144px - env(safe-area-inset-bottom, 0px))',
|
|
48
|
+
position: 'relative',
|
|
49
|
+
/* DO NOT use overflow: hidden — clips PTT button and chat drawer */
|
|
50
|
+
}}>
|
|
51
|
+
<BobStandalone
|
|
52
|
+
partner="CARFIX"
|
|
53
|
+
sessionToken={sessionToken}
|
|
54
|
+
bottomOffset={0} /* Container already handles spacing */
|
|
55
|
+
zIndexBase={100} /* Above header z-40 and nav z-30 */
|
|
56
|
+
onAddToCart={async (item) => {
|
|
57
|
+
// See Section 6 for full item shape including bundle metadata
|
|
58
|
+
await carfixCart.add(item);
|
|
59
|
+
}}
|
|
60
|
+
onNavigate={(url) => router.push(url)}
|
|
61
|
+
onCheckout={(checkoutUrl) => window.location.href = checkoutUrl}
|
|
62
|
+
onError={(error) => console.error('[Bob Error]', error)}
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
> **Auto-configuration:** The `partner="CARFIX"` prop loads all API URLs, credentials, layout defaults, and feature flags from the `bob_partners` database table. No manual config needed.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 4. CARFIX API Configuration (Already in Database)
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"baseUrl": "https://flpzjbasdsfwoeruyxgp.supabase.co/functions/v1",
|
|
78
|
+
"apiKey": "eyJhbGciOiJIUzI1NiIs...wKoJ51_VPro_BrJz-A-NRpSmUW0XBP-7TJJcrhvYwxE",
|
|
79
|
+
"partnerCode": "CARFIX"
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Available Endpoints
|
|
84
|
+
|
|
85
|
+
| Endpoint | Purpose |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `partner-api` | Session creation, cart, user context, orders |
|
|
88
|
+
| `calculate-service-bundles` | Service packs with `preparedTiers[]` (incl. bundle discounts) |
|
|
89
|
+
| `retrieve-vehicle-info` | NZ rego lookup |
|
|
90
|
+
| `retrieve-parts` | Vehicle parts by category |
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 5. Layout Constraints
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
┌──────────────────────────────┐
|
|
98
|
+
│ CARFIX Header (72px, z-40) │
|
|
99
|
+
├──────────────────────────────┤
|
|
100
|
+
│ │
|
|
101
|
+
│ Bob Container │
|
|
102
|
+
│ height: calc(100dvh - 144px │
|
|
103
|
+
│ - safe-area-inset) │
|
|
104
|
+
│ position: relative │
|
|
105
|
+
│ NO overflow: hidden! │
|
|
106
|
+
│ │
|
|
107
|
+
├──────────────────────────────┤
|
|
108
|
+
│ Bottom Nav (72px, z-30) │
|
|
109
|
+
└──────────────────────────────┘
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Bob's internal z-index stack (relative to `zIndexBase={100}`):**
|
|
113
|
+
|
|
114
|
+
| Layer | z-index |
|
|
115
|
+
|---|---|
|
|
116
|
+
| Chat PTT Button | 145 |
|
|
117
|
+
| Chat Drawer | 130 |
|
|
118
|
+
| Counter Overlay | 70 |
|
|
119
|
+
| Bob Character | 60 |
|
|
120
|
+
| Product Shelf | 55 |
|
|
121
|
+
| Backdrop | 10 |
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 6. Callback Signatures
|
|
126
|
+
|
|
127
|
+
### onAddToCart
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
onAddToCart: (item: {
|
|
131
|
+
product_id: string;
|
|
132
|
+
product_name: string;
|
|
133
|
+
quantity: number;
|
|
134
|
+
unit_price: number; // Final price (discount already applied for bundle items)
|
|
135
|
+
sku?: string;
|
|
136
|
+
brand?: string;
|
|
137
|
+
image_url?: string;
|
|
138
|
+
vehicle_id?: string;
|
|
139
|
+
// Bundle metadata (present when item is part of a service package)
|
|
140
|
+
is_bundle_item?: boolean;
|
|
141
|
+
bundle_discount_percentage?: number;
|
|
142
|
+
service_package_name?: string;
|
|
143
|
+
service_package_id?: string;
|
|
144
|
+
quality_tier?: string; // "Economy" | "Standard" | "Premium" | "Performance"
|
|
145
|
+
}) => Promise<void> | void;
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### onNavigate
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
onNavigate: (url: string) => void;
|
|
152
|
+
// Example urls: "/product/SKU123", "/checkout"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### onCheckout
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
onCheckout: (checkoutUrl: string) => void;
|
|
159
|
+
// checkoutUrl is a full Stripe payment URL
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### onError
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
onError: (error: Error) => void;
|
|
166
|
+
// Bob shows toast by default — this is for host-side logging
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 7. Bundle Discount Fields (NEW)
|
|
172
|
+
|
|
173
|
+
The `calculate-service-bundles` API returns these fields per `PreparedTier`:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
interface PreparedTier {
|
|
177
|
+
tierName: string; // "Economy" | "Standard" | "Premium" | "Performance"
|
|
178
|
+
displayName: string;
|
|
179
|
+
description: string;
|
|
180
|
+
isRecommended: boolean; // true = CARFIX Value tier
|
|
181
|
+
isHidden: boolean; // true = filter out (duplicate price)
|
|
182
|
+
|
|
183
|
+
// Pricing (pre-calculated by API — never recompute)
|
|
184
|
+
totalPrice: number; // Discounted bundle price
|
|
185
|
+
originalTotalPrice?: number; // Full price before discount
|
|
186
|
+
savingsAmount?: number; // Dollar savings (originalTotalPrice - totalPrice)
|
|
187
|
+
bundleDiscountPercentage?: number; // Discount % applied (0-50)
|
|
188
|
+
|
|
189
|
+
productCount: number;
|
|
190
|
+
dominantBrand: string | null;
|
|
191
|
+
brands: PreparedTierBrand[];
|
|
192
|
+
products: PreparedTierProduct[];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
interface PreparedTierProduct {
|
|
196
|
+
partslotId: number;
|
|
197
|
+
partslotName: string; // "BRAKE PADS FRONT"
|
|
198
|
+
sku: string;
|
|
199
|
+
name: string;
|
|
200
|
+
brand: string;
|
|
201
|
+
brandFullName: string;
|
|
202
|
+
brandImageUrl: string; // Full URL — use directly in <img>
|
|
203
|
+
productImageUrl: string; // Full URL — use directly in <img>
|
|
204
|
+
price: number; // Legacy unit price
|
|
205
|
+
unitPrice: number; // Per-unit price
|
|
206
|
+
displayPrice: number; // Total (unitPrice × perCarQty) — USE THIS
|
|
207
|
+
isRotor: boolean; // Show "[Pair]" badge
|
|
208
|
+
isMultiQty: boolean; // Show quantity breakdown (e.g. spark plugs)
|
|
209
|
+
perCarQty: number; // Quantity needed
|
|
210
|
+
partNumber: string | null;
|
|
211
|
+
webDescription: string | null;
|
|
212
|
+
viscosity: string | null;
|
|
213
|
+
volume: number | null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
interface PreparedTierBrand {
|
|
217
|
+
name: string;
|
|
218
|
+
fullName: string;
|
|
219
|
+
imageUrl: string; // Full URL
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Rendering Rules
|
|
224
|
+
|
|
225
|
+
1. **Filter hidden tiers:** `preparedTiers.filter(t => !t.isHidden)`
|
|
226
|
+
2. **When `savingsAmount > 0`:** Show ~~$originalTotalPrice~~ → **$totalPrice** + "SAVE $XX — X% Bundle Deal"
|
|
227
|
+
3. **When `savingsAmount === 0`:** Show `totalPrice` normally, no discount UI
|
|
228
|
+
4. **Use `displayPrice`** for individual products (already includes quantity)
|
|
229
|
+
5. **Never calculate prices client-side** — all values arrive pre-calculated
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## 8. Session Handoff (Pre-authenticated Users)
|
|
234
|
+
|
|
235
|
+
To pass vehicle/customer context from CARFIX to Bob:
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
1. CARFIX calls partner-api with action: "create_session"
|
|
239
|
+
→ Body: { vehicle_id: 42899 } ← MUST be a NUMBER
|
|
240
|
+
→ Returns: { session_token: "abc123..." }
|
|
241
|
+
|
|
242
|
+
2. Redirect to Bob page: /ask-bob?session=abc123...
|
|
243
|
+
|
|
244
|
+
3. BobStandalone reads sessionToken from URL
|
|
245
|
+
→ Calls session-handoff edge function
|
|
246
|
+
→ Injects vehicle + customer context into chat
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
> **Critical:** `vehicle_id` must be numeric throughout the pipeline. String values cause silent API failures.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 9. Design Tokens
|
|
254
|
+
|
|
255
|
+
The full design token file is exported from the package:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
import {
|
|
259
|
+
CARFIX_COLORS,
|
|
260
|
+
QUALITY_TIER_CONFIG,
|
|
261
|
+
IMAGE_URLS,
|
|
262
|
+
BADGE_CONFIG,
|
|
263
|
+
TYPOGRAPHY,
|
|
264
|
+
isRotorProduct,
|
|
265
|
+
getDisplayPrice,
|
|
266
|
+
formatNZD,
|
|
267
|
+
} from '@gymmymac/bob-widget';
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Key Colors
|
|
271
|
+
|
|
272
|
+
| Token | Value | Usage |
|
|
273
|
+
|---|---|---|
|
|
274
|
+
| `primary` | `#0052CC` | Standard tier, CTAs, CARFIX Value |
|
|
275
|
+
| `secondary` | `#38BDF8` | Accents, links |
|
|
276
|
+
| `accent` | `#FF8C00` | Premium tier |
|
|
277
|
+
| `success` | `#22C55E` | "Fits Vehicle" badges, Add to Cart |
|
|
278
|
+
| `destructive` | `#EF4444` | Performance tier |
|
|
279
|
+
| `foreground` | `#0F172A` | Headers, primary text |
|
|
280
|
+
| `mutedForeground` | `#64748B` | Descriptions |
|
|
281
|
+
|
|
282
|
+
### Tier Visual Config
|
|
283
|
+
|
|
284
|
+
| Tier | Color | Badge |
|
|
285
|
+
|---|---|---|
|
|
286
|
+
| Economy | `#475569` on `#F1F5F9` | 💰 |
|
|
287
|
+
| Standard | `#0052CC` on `rgba(0,82,204,0.1)` | ⭐ CARFIX Value |
|
|
288
|
+
| Premium | `#D97706` on `#FEF3C7` | 🏆 |
|
|
289
|
+
| Performance | `#DC2626` on `#FEE2E2` | ⚡ |
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## 10. Exported Types (Full List)
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
// Core widget components
|
|
297
|
+
export { BobStandalone } from '@gymmymac/bob-widget';
|
|
298
|
+
export type { StandaloneWidgetProps } from '@gymmymac/bob-widget';
|
|
299
|
+
|
|
300
|
+
// Types available for CARFIX integration
|
|
301
|
+
export type {
|
|
302
|
+
// Context & Config
|
|
303
|
+
HostContext, HostUserContext, HostVehicleContext, HostCartContext,
|
|
304
|
+
BobConfig, HostApiConfig, BobCallbacks, BobProviderConfig, BobLayoutConfig,
|
|
305
|
+
|
|
306
|
+
// Products & Packages
|
|
307
|
+
Product, CartItem, ServicePackage, PreparedTier, PreparedTierProduct, PreparedTierBrand,
|
|
308
|
+
Partslot, QualityTiers, Part,
|
|
309
|
+
|
|
310
|
+
// Partner
|
|
311
|
+
PartnerConfig, PartnerFeatureFlags, EssentialCallbacks,
|
|
312
|
+
|
|
313
|
+
// Vehicle
|
|
314
|
+
Vehicle,
|
|
315
|
+
|
|
316
|
+
// Messages
|
|
317
|
+
Message, HighlightedProduct,
|
|
318
|
+
|
|
319
|
+
// Analytics
|
|
320
|
+
BobAnalyticsEvent, BobGA4Config,
|
|
321
|
+
} from '@gymmymac/bob-widget';
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## 11. Verification Checklist (Post-Install)
|
|
327
|
+
|
|
328
|
+
```
|
|
329
|
+
□ npm install completes without peer dependency warnings
|
|
330
|
+
□ BobStandalone renders loading spinner, then Bob appears
|
|
331
|
+
□ Bob character sits between header and bottom nav
|
|
332
|
+
□ Chat drawer opens above bottom navigation (z-index check)
|
|
333
|
+
□ PTT button is visible and not clipped
|
|
334
|
+
□ Vehicle lookup works (try rego: HZP550)
|
|
335
|
+
□ Service packages appear with tier cards
|
|
336
|
+
□ Bundle discount shows Was/Now pricing where applicable
|
|
337
|
+
□ "Add to Cart" callback fires with correct item shape
|
|
338
|
+
□ Session handoff works (pass ?session=TOKEN)
|
|
339
|
+
□ No console errors related to Bob
|
|
340
|
+
□ Mobile: safe-area-inset respected on notched devices
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## 12. Test Baseline
|
|
346
|
+
|
|
347
|
+
Bob ships with **36 unit tests** and **8+ E2E scenarios** covering:
|
|
348
|
+
|
|
349
|
+
- Callback mapping and stability
|
|
350
|
+
- Tier validation and empty states
|
|
351
|
+
- Rear Brake Disc/Drum filter logic
|
|
352
|
+
- Bundle discount display and cart pricing
|
|
353
|
+
- Vehicle lookup flow
|
|
354
|
+
- Service package rendering
|
|
355
|
+
- Chat drawer positioning
|
|
356
|
+
|
|
357
|
+
Run locally: `cd packages/bob-widget && npx vitest run`
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Support
|
|
362
|
+
|
|
363
|
+
For integration issues, the Bob team needs:
|
|
364
|
+
1. Browser console output (filter for `[Bob`)
|
|
365
|
+
2. Network tab showing failed API calls
|
|
366
|
+
3. Screenshot of layout issue
|
|
367
|
+
4. Device/browser/viewport info
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gymmymac/bob-widget",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.20",
|
|
4
4
|
"description": "Bob - AI-powered automotive parts assistant widget with multi-tenant support, RAF animations, swipeable interactions, and GA4 analytics",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|