@fayz-ai/storefront 0.7.0 → 0.8.2

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.
Files changed (48) hide show
  1. package/README.md +3 -1
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/components/CartDrawer.d.ts.map +1 -1
  4. package/dist/components/DeliveryEstimator.d.ts +17 -0
  5. package/dist/components/DeliveryEstimator.d.ts.map +1 -0
  6. package/dist/components/ProductGallery.d.ts +22 -0
  7. package/dist/components/ProductGallery.d.ts.map +1 -0
  8. package/dist/components/StorefrontHeader.d.ts.map +1 -1
  9. package/dist/config.d.ts +8 -0
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/createStorefrontApp.d.ts.map +1 -1
  12. package/dist/index.cjs +886 -370
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +790 -275
  17. package/dist/index.js.map +1 -1
  18. package/dist/pages/CheckoutPage.d.ts.map +1 -1
  19. package/dist/pages/ProductDetailPage.d.ts.map +1 -1
  20. package/dist/presets.d.ts +5 -0
  21. package/dist/presets.d.ts.map +1 -1
  22. package/dist/stores/cart.store.d.ts +21 -0
  23. package/dist/stores/cart.store.d.ts.map +1 -1
  24. package/dist/stores/delivery.store.d.ts +64 -0
  25. package/dist/stores/delivery.store.d.ts.map +1 -0
  26. package/dist/testids.d.ts +12 -0
  27. package/dist/testids.d.ts.map +1 -1
  28. package/dist/theme.d.ts +18 -2
  29. package/dist/theme.d.ts.map +1 -1
  30. package/dist/workflows/checkout.d.ts +5 -2
  31. package/dist/workflows/checkout.d.ts.map +1 -1
  32. package/package.json +13 -7
  33. package/src/auth.ts +57 -1
  34. package/src/components/CartDrawer.tsx +23 -1
  35. package/src/components/DeliveryEstimator.tsx +157 -0
  36. package/src/components/ProductGallery.tsx +174 -0
  37. package/src/components/StorefrontHeader.tsx +35 -8
  38. package/src/config.ts +15 -3
  39. package/src/createStorefrontApp.tsx +18 -1
  40. package/src/index.ts +1 -0
  41. package/src/pages/CheckoutPage.tsx +240 -134
  42. package/src/pages/ProductDetailPage.tsx +17 -13
  43. package/src/presets.ts +66 -0
  44. package/src/stores/cart.store.ts +37 -1
  45. package/src/stores/delivery.store.ts +152 -0
  46. package/src/testids.ts +13 -0
  47. package/src/theme.ts +33 -3
  48. package/src/workflows/checkout.ts +34 -6
@@ -1 +1 @@
1
- {"version":3,"file":"CheckoutPage.d.ts","sourceRoot":"","sources":["../../src/pages/CheckoutPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAA;AAgGlD,wBAAgB,YAAY,sBAke3B"}
1
+ {"version":3,"file":"CheckoutPage.d.ts","sourceRoot":"","sources":["../../src/pages/CheckoutPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAA;AA0GlD,wBAAgB,YAAY,sBAkkB3B"}
@@ -1 +1 @@
1
- {"version":3,"file":"ProductDetailPage.d.ts","sourceRoot":"","sources":["../../src/pages/ProductDetailPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAA;AAmBlD,wBAAgB,iBAAiB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,qBAgP3D"}
1
+ {"version":3,"file":"ProductDetailPage.d.ts","sourceRoot":"","sources":["../../src/pages/ProductDetailPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAA;AAqBlD,wBAAgB,iBAAiB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,qBAkP3D"}
package/dist/presets.d.ts CHANGED
@@ -23,11 +23,16 @@ export declare const sertaoTemplate: StorefrontTemplate;
23
23
  export declare const voltTemplate: StorefrontTemplate;
24
24
  /** atelier — classic premium retail (Flex): white, near-black, gold accent, slider hero, trust badges. */
25
25
  export declare const atelierTemplate: StorefrontTemplate;
26
+ /** food — warm appetizing delivery (steakhouse / burger / comfort food): dark inviting
27
+ * header, ember CTA, cream canvas, rounded cards, menu-first home with "mais pedidos"
28
+ * rail and social proof. Built for restaurant/food stores — override to go fully dark. */
29
+ export declare const foodTemplate: StorefrontTemplate;
26
30
  export declare const storefrontTemplates: {
27
31
  readonly mare: StorefrontTemplate;
28
32
  readonly sertao: StorefrontTemplate;
29
33
  readonly volt: StorefrontTemplate;
30
34
  readonly atelier: StorefrontTemplate;
35
+ readonly food: StorefrontTemplate;
31
36
  };
32
37
  export type StorefrontTemplateId = keyof typeof storefrontTemplates;
33
38
  //# sourceMappingURL=presets.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../src/presets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAS5C;qFACqF;AACrF,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IAChC,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CACpC;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,eAAe,CAAA;IACtB,0EAA0E;IAC1E,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,cAAc,KAAK,UAAU,CAAA;IAChE,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,yFAAyF;AACzF,eAAO,MAAM,YAAY,EAAE,kBA0C1B,CAAA;AAED,gGAAgG;AAChG,eAAO,MAAM,cAAc,EAAE,kBA2D5B,CAAA;AAED,kHAAkH;AAClH,eAAO,MAAM,YAAY,EAAE,kBAkD1B,CAAA;AAED,0GAA0G;AAC1G,eAAO,MAAM,eAAe,EAAE,kBA4D7B,CAAA;AAED,eAAO,MAAM,mBAAmB;;;;;CAKtB,CAAA;AAEV,MAAM,MAAM,oBAAoB,GAAG,MAAM,OAAO,mBAAmB,CAAA"}
1
+ {"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../src/presets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAS5C;qFACqF;AACrF,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IAChC,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CACpC;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,eAAe,CAAA;IACtB,0EAA0E;IAC1E,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,cAAc,KAAK,UAAU,CAAA;IAChE,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,yFAAyF;AACzF,eAAO,MAAM,YAAY,EAAE,kBA0C1B,CAAA;AAED,gGAAgG;AAChG,eAAO,MAAM,cAAc,EAAE,kBA2D5B,CAAA;AAED,kHAAkH;AAClH,eAAO,MAAM,YAAY,EAAE,kBAkD1B,CAAA;AAED,0GAA0G;AAC1G,eAAO,MAAM,eAAe,EAAE,kBA4D7B,CAAA;AAED;;2FAE2F;AAC3F,eAAO,MAAM,YAAY,EAAE,kBA4D1B,CAAA;AAED,eAAO,MAAM,mBAAmB;;;;;;CAMtB,CAAA;AAEV,MAAM,MAAM,oBAAoB,GAAG,MAAM,OAAO,mBAAmB,CAAA"}
@@ -30,6 +30,11 @@ export interface CartState {
30
30
  clear(): void;
31
31
  openDrawer(): void;
32
32
  closeDrawer(): void;
33
+ /** Drop persisted lines the active backend can no longer resolve (e.g. a cart
34
+ * built in an earlier mock-mode session, whose ids don't exist live). The
35
+ * resolver returns false only for a definitive "not found"; on error it must
36
+ * return true so a transient failure never discards a legit line. */
37
+ reconcile(resolve: (line: CartLine) => Promise<boolean>): Promise<void>;
33
38
  }
34
39
  export declare const useCartStore: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<CartState>, "persist"> & {
35
40
  persist: {
@@ -53,6 +58,22 @@ export declare const useCartStore: import("zustand").UseBoundStore<Omit<import("
53
58
  export declare const selectCount: (s: Pick<CartState, "lines">) => number;
54
59
  export declare const selectSubtotal: (s: Pick<CartState, "lines">) => number;
55
60
  export declare const selectDiscountTotal: (s: Pick<CartState, "lines" | "discountPercent">) => number;
61
+ /**
62
+ * Freight to display.
63
+ *
64
+ * When the shopper has given a CEP and the store quoted it, that quote wins —
65
+ * it came from the same shipping_zones rows shop_place_order will charge from.
66
+ * Otherwise this falls back to the store-wide rate in config, which is what
67
+ * every storefront did before zones existed.
68
+ *
69
+ * The subtotal is PRE-discount on both sides of this. That is 0017's rule, and
70
+ * the quote is requested against the same number, so a coupon can never make
71
+ * the cart and the order disagree.
72
+ *
73
+ * Read through getState() rather than a hook because this is a plain selector
74
+ * called from several screens; components that render the value subscribe to
75
+ * useDeliveryStore themselves so a fresh quote re-renders them.
76
+ */
56
77
  export declare const selectShipping: (s: Pick<CartState, "lines">, cfg: ResolvedStorefrontConfig) => number;
57
78
  export declare const selectTotal: (s: Pick<CartState, "lines" | "discountPercent">, cfg: ResolvedStorefrontConfig) => number;
58
79
  //# sourceMappingURL=cart.store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cart.store.d.ts","sourceRoot":"","sources":["../../src/stores/cart.store.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAA;AACzD,OAAO,EAIL,KAAK,sBAAsB,EAC5B,MAAM,oBAAoB,CAAA;AAE3B,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,OAAO,EAAE,sBAAsB,CAAA;IAC/B,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,QAAQ,EAAE,CAAA;IACjB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,eAAe,EAAE,MAAM,CAAA;IACvB,MAAM,EAAE,OAAO,CAAA;IACf,uEAAuE;IACvE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAAA;IAC/E,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9C,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAClD,aAAa,IAAI,IAAI,CAAA;IACrB,KAAK,IAAI,IAAI,CAAA;IACb,UAAU,IAAI,IAAI,CAAA;IAClB,WAAW,IAAI,IAAI,CAAA;CACpB;AAED,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;EA0ExB,CAAA;AAOD,eAAO,MAAM,WAAW,GAAI,GAAG,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,KAAG,MACb,CAAA;AAE7C,eAAO,MAAM,cAAc,GAAI,GAAG,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,KAAG,MACc,CAAA;AAE3E,eAAO,MAAM,mBAAmB,GAAI,GAAG,IAAI,CAAC,SAAS,EAAE,OAAO,GAAG,iBAAiB,CAAC,KAAG,MAC3B,CAAA;AAE3D,eAAO,MAAM,cAAc,GACzB,GAAG,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,EAC3B,KAAK,wBAAwB,KAC5B,MAKF,CAAA;AAED,eAAO,MAAM,WAAW,GACtB,GAAG,IAAI,CAAC,SAAS,EAAE,OAAO,GAAG,iBAAiB,CAAC,EAC/C,KAAK,wBAAwB,KAC5B,MAC8E,CAAA"}
1
+ {"version":3,"file":"cart.store.d.ts","sourceRoot":"","sources":["../../src/stores/cart.store.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAA;AAEzD,OAAO,EAIL,KAAK,sBAAsB,EAC5B,MAAM,oBAAoB,CAAA;AAE3B,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,OAAO,EAAE,sBAAsB,CAAA;IAC/B,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,QAAQ,EAAE,CAAA;IACjB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,eAAe,EAAE,MAAM,CAAA;IACvB,MAAM,EAAE,OAAO,CAAA;IACf,uEAAuE;IACvE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAAA;IAC/E,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9C,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAClD,aAAa,IAAI,IAAI,CAAA;IACrB,KAAK,IAAI,IAAI,CAAA;IACb,UAAU,IAAI,IAAI,CAAA;IAClB,WAAW,IAAI,IAAI,CAAA;IACnB;;;0EAGsE;IACtE,SAAS,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACxE;AAED,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;EAoFxB,CAAA;AAOD,eAAO,MAAM,WAAW,GAAI,GAAG,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,KAAG,MACb,CAAA;AAE7C,eAAO,MAAM,cAAc,GAAI,GAAG,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,KAAG,MACc,CAAA;AAE3E,eAAO,MAAM,mBAAmB,GAAI,GAAG,IAAI,CAAC,SAAS,EAAE,OAAO,GAAG,iBAAiB,CAAC,KAAG,MAC3B,CAAA;AAE3D;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,cAAc,GACzB,GAAG,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,EAC3B,KAAK,wBAAwB,KAC5B,MASF,CAAA;AAED,eAAO,MAAM,WAAW,GACtB,GAAG,IAAI,CAAC,SAAS,EAAE,OAAO,GAAG,iBAAiB,CAAC,EAC/C,KAAK,wBAAwB,KAC5B,MAC8E,CAAA"}
@@ -0,0 +1,64 @@
1
+ import { type PostalAddress } from '@fayz-ai/core';
2
+ import type { ShippingQuoteOption } from '@fayz-ai/shop/types';
3
+ /**
4
+ * Where the shopper wants it delivered, decided before the cart exists.
5
+ *
6
+ * Two different things live here and they are persisted differently on purpose:
7
+ *
8
+ * the ADDRESS is durable — a CEP maps to the same street for years, and
9
+ * carrying it to checkout is the whole point (the buyer types the number and
10
+ * nothing else);
11
+ *
12
+ * the QUOTE is not persisted at all. It is a price, and a price restored from
13
+ * localStorage is a price the shopper can edit with devtools. It is re-asked
14
+ * on demand, and resolved for the current cart through resolveOptionRate —
15
+ * the same rule shop_shipping_for applies — so it can never go stale. The
16
+ * order is charged from shop_place_order, which recomputes from the same
17
+ * zones and ignores anything the client sends.
18
+ */
19
+ export type DeliveryStatus = 'idle' | 'loading' | 'served' | 'unserved' | 'error';
20
+ export interface DeliveryState {
21
+ postalCode: string;
22
+ address: PostalAddress | null;
23
+ options: ShippingQuoteOption[];
24
+ selectedZoneId: string | null;
25
+ status: DeliveryStatus;
26
+ error: string | null;
27
+ /** Look the CEP up and quote it. `subtotal` decides free-above thresholds. */
28
+ resolve: (postalCode: string, subtotal: number) => Promise<void>;
29
+ selectZone: (zoneId: string) => void;
30
+ clear: () => void;
31
+ }
32
+ export declare const useDeliveryStore: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<DeliveryState>, "persist"> & {
33
+ persist: {
34
+ setOptions: (options: Partial<import("zustand/middleware").PersistOptions<DeliveryState, {
35
+ postalCode: string;
36
+ address: PostalAddress | null;
37
+ }>>) => void;
38
+ clearStorage: () => void;
39
+ rehydrate: () => Promise<void> | void;
40
+ hasHydrated: () => boolean;
41
+ onHydrate: (fn: (state: DeliveryState) => void) => () => void;
42
+ onFinishHydration: (fn: (state: DeliveryState) => void) => () => void;
43
+ getOptions: () => Partial<import("zustand/middleware").PersistOptions<DeliveryState, {
44
+ postalCode: string;
45
+ address: PostalAddress | null;
46
+ }>>;
47
+ };
48
+ }>;
49
+ /**
50
+ * The freight for the chosen zone at THIS subtotal, or null when there is no
51
+ * zone in play (no CEP yet, or a store that configured none) and the caller
52
+ * should use the store-wide rate. Null never means free.
53
+ *
54
+ * The free-above rule is applied here rather than trusting the `rate` the
55
+ * server resolved, because that one was resolved for whatever the cart held at
56
+ * quote time. Re-deriving from `baseRate`/`freeAbove` — the same CASE
57
+ * shop_shipping_for runs — means adding an item can never leave a stale price
58
+ * on screen. An earlier version fell back to the config flat rate when the
59
+ * quote went stale, which showed R$ 8 (the flat rate) for an address in an
60
+ * R$ 18 zone: shown one number, charged another.
61
+ */
62
+ export declare function resolveOptionRate(option: ShippingQuoteOption, subtotal: number): number;
63
+ export declare const selectQuotedShipping: (state: Pick<DeliveryState, "options" | "selectedZoneId" | "status">, subtotal: number) => number | null;
64
+ //# sourceMappingURL=delivery.store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delivery.store.d.ts","sourceRoot":"","sources":["../../src/stores/delivery.store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAyC,KAAK,aAAa,EAAE,MAAM,eAAe,CAAA;AAEzF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAE9D;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAA;AAEjF,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,aAAa,GAAG,IAAI,CAAA;IAC7B,OAAO,EAAE,mBAAmB,EAAE,CAAA;IAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,MAAM,EAAE,cAAc,CAAA;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IAEpB,8EAA8E;IAC9E,OAAO,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAChE,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAWD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;EA4E5B,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGvF;AAED,eAAO,MAAM,oBAAoB,GAC/B,OAAO,IAAI,CAAC,aAAa,EAAE,SAAS,GAAG,gBAAgB,GAAG,QAAQ,CAAC,EACnE,UAAU,MAAM,KACf,MAAM,GAAG,IAIX,CAAA"}
package/dist/testids.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export declare const TID: {
2
2
  readonly headerSearch: "header-search";
3
+ readonly headerSearchToggle: "header-search-toggle";
3
4
  readonly cartButton: "cart-button";
4
5
  readonly cartCount: "cart-count";
5
6
  readonly accountLink: "account-link";
@@ -51,6 +52,9 @@ export declare const TID: {
51
52
  readonly pdpAddToCart: "pdp-add-to-cart";
52
53
  readonly pdpRoot: "pdp-root";
53
54
  readonly pdpGallery: "pdp-gallery";
55
+ readonly pdpGalleryImage: "pdp-gallery-image";
56
+ readonly pdpGalleryThumbs: "pdp-gallery-thumbs";
57
+ readonly pdpGalleryThumb: "pdp-gallery-thumb";
54
58
  readonly pdpActions: "pdp-actions";
55
59
  readonly pdpEnquiryButton: "pdp-enquiry-button";
56
60
  readonly enquiryForm: "enquiry-form";
@@ -78,6 +82,14 @@ export declare const TID: {
78
82
  readonly cartTotal: "cart-total";
79
83
  readonly goCheckout: "go-checkout";
80
84
  readonly cartEmpty: "cart-empty";
85
+ readonly deliveryEstimator: "delivery-estimator";
86
+ readonly deliveryCepInput: "delivery-cep-input";
87
+ readonly deliveryCepSubmit: "delivery-cep-submit";
88
+ readonly deliveryOption: "delivery-option";
89
+ readonly deliveryUnserved: "delivery-unserved";
90
+ readonly deliveryError: "delivery-error";
91
+ readonly deliveryAddress: "delivery-address";
92
+ readonly deliveryClear: "delivery-clear";
81
93
  readonly checkoutEmail: "checkout-email";
82
94
  readonly checkoutName: "checkout-name";
83
95
  readonly checkoutStreet: "checkout-street";
@@ -1 +1 @@
1
- {"version":3,"file":"testids.d.ts","sourceRoot":"","sources":["../src/testids.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,GAAG;;;;;;;8BAQG,MAAM;iCACH,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oCAgCH,MAAM;;;;;;;;;;sCAWJ,MAAM;sCACN,MAAM,SAAS,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uCAiDpB,MAAM;;;;;;;;;;;;;;;;;CAkBxB,CAAA"}
1
+ {"version":3,"file":"testids.d.ts","sourceRoot":"","sources":["../src/testids.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,GAAG;;;;;;;;8BASG,MAAM;iCACH,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oCAgCH,MAAM;;;;;;;;;;sCAWJ,MAAM;sCACN,MAAM,SAAS,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uCA6DpB,MAAM;;;;;;;;;;;;;;;;;CAkBxB,CAAA"}
package/dist/theme.d.ts CHANGED
@@ -20,13 +20,21 @@ export interface StorefrontThemeColors {
20
20
  /** Announcement bar */
21
21
  announcementBackground?: string;
22
22
  announcementForeground?: string;
23
+ /** Secondary brand accent (e.g. a gourmet gold beside the primary). Emitted
24
+ * as `--sf-accent` / `--sf-accent-foreground` and the `accent` tailwind token. */
25
+ accent?: string;
26
+ accentForeground?: string;
23
27
  }
24
28
  export interface StorefrontThemeFont {
25
29
  /** Display font for headings, e.g. 'Marcellus' */
26
30
  heading: string;
27
31
  /** Body font, e.g. 'Rubik' */
28
32
  body: string;
29
- /** Google Fonts families to load (defaults to heading+body) */
33
+ /** Optional oversized display face for hero words (emitted as `--sf-font-display`). */
34
+ display?: string;
35
+ /** Optional serif accent for quotes/editorial (emitted as `--sf-font-serif`). */
36
+ serif?: string;
37
+ /** Google Fonts families to load (defaults to heading+body+display+serif) */
30
38
  googleFonts?: string[];
31
39
  /** Fallback stack — default sans-serif; use 'serif' for editorial themes */
32
40
  fallback?: 'sans-serif' | 'serif';
@@ -43,8 +51,11 @@ export interface StorefrontTheme {
43
51
  uppercaseNav?: boolean;
44
52
  /** Show category shortcuts after primary nav links. Default true. */
45
53
  showCategories?: boolean;
46
- /** Show the storefront search input in the header. Default true. */
54
+ /** Show the storefront search in the header. Default true. */
47
55
  showSearch?: boolean;
56
+ /** Header search presentation: a full input ('full', default) or a compact
57
+ * icon button that expands into an input on click ('icon'). */
58
+ searchStyle?: 'full' | 'icon';
48
59
  };
49
60
  productCard?: {
50
61
  style?: CardStyle;
@@ -52,6 +63,11 @@ export interface StorefrontTheme {
52
63
  };
53
64
  /** Uppercase CTA buttons with letter-spacing (Uyuni/Flex tone) */
54
65
  uppercaseButtons?: boolean;
66
+ /** Brand-token passthrough. Each entry is emitted verbatim as a `--<key>` CSS
67
+ * custom property, so a store can add gradients, glows, extra colors, or
68
+ * letter-spacing the fixed schema doesn't enumerate — without editing the SDK.
69
+ * e.g. { 'grad-ember': 'linear-gradient(...)', 'glow-ember': '0 0 0 1px ...' }. */
70
+ tokens?: Record<string, string>;
55
71
  }
56
72
  export declare function themeToCss(theme: StorefrontTheme): string;
57
73
  /** Injects theme CSS variables + Google Fonts. Mount once inside the app root. */
@@ -1 +1 @@
1
- {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAoB,MAAM,OAAO,CAAA;AAaxC,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAA;AACjE,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,SAAS,CAAA;AACzE,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAA;AACvD,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,WAAW,CAAA;AAE5C,MAAM,WAAW,qBAAqB;IACpC,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,2DAA2D;IAC3D,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,uBAAuB;IACvB,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,sBAAsB,CAAC,EAAE,MAAM,CAAA;CAChC;AAED,MAAM,WAAW,mBAAmB;IAClC,kDAAkD;IAClD,OAAO,EAAE,MAAM,CAAA;IACf,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,YAAY,GAAG,OAAO,CAAA;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,qBAAqB,CAAA;IAC9B,IAAI,CAAC,EAAE,mBAAmB,CAAA;IAC1B,MAAM,CAAC,EAAE,gBAAgB,CAAA;IACzB,yBAAyB;IACzB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,aAAa,CAAA;QACvB,gDAAgD;QAChD,YAAY,CAAC,EAAE,OAAO,CAAA;QACtB,qEAAqE;QACrE,cAAc,CAAC,EAAE,OAAO,CAAA;QACxB,oEAAoE;QACpE,UAAU,CAAC,EAAE,OAAO,CAAA;KACrB,CAAA;IACD,WAAW,CAAC,EAAE;QACZ,KAAK,CAAC,EAAE,SAAS,CAAA;QACjB,WAAW,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAA;KACpC,CAAA;IACD,kEAAkE;IAClE,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B;AAyBD,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAuCzD;AAYD,kFAAkF;AAClF,wBAAgB,oBAAoB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,eAAe,CAAA;CAAE;;8CAiBzE"}
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAoB,MAAM,OAAO,CAAA;AAaxC,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAA;AACjE,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,SAAS,CAAA;AACzE,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAA;AACvD,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,WAAW,CAAA;AAE5C,MAAM,WAAW,qBAAqB;IACpC,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,2DAA2D;IAC3D,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,uBAAuB;IACvB,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B;uFACmF;IACnF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC,kDAAkD;IAClD,OAAO,EAAE,MAAM,CAAA;IACf,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,uFAAuF;IACvF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,iFAAiF;IACjF,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,YAAY,GAAG,OAAO,CAAA;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,qBAAqB,CAAA;IAC9B,IAAI,CAAC,EAAE,mBAAmB,CAAA;IAC1B,MAAM,CAAC,EAAE,gBAAgB,CAAA;IACzB,yBAAyB;IACzB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,aAAa,CAAA;QACvB,gDAAgD;QAChD,YAAY,CAAC,EAAE,OAAO,CAAA;QACtB,qEAAqE;QACrE,cAAc,CAAC,EAAE,OAAO,CAAA;QACxB,8DAA8D;QAC9D,UAAU,CAAC,EAAE,OAAO,CAAA;QACpB;wEACgE;QAChE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAC9B,CAAA;IACD,WAAW,CAAC,EAAE;QACZ,KAAK,CAAC,EAAE,SAAS,CAAA;QACjB,WAAW,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAA;KACpC,CAAA;IACD,kEAAkE;IAClE,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B;;;wFAGoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AA2BD,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CA+CzD;AAgBD,kFAAkF;AAClF,wBAAgB,oBAAoB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,eAAe,CAAA;CAAE;;8CAiBzE"}
@@ -1,4 +1,4 @@
1
- import type { Order } from '@fayz-ai/shop/types';
1
+ import type { Order, ShippingAddressInput, PaymentMethodKind } from '@fayz-ai/shop/types';
2
2
  import type { ResolvedStorefrontConfig } from '../config';
3
3
  import type { CartState } from '../stores/cart.store';
4
4
  import type { SessionState } from '../stores/session.store';
@@ -18,6 +18,9 @@ export interface PlaceStorefrontOrderInput {
18
18
  session?: Pick<SessionState, 'customerId' | 'email'>;
19
19
  customer: StorefrontCheckoutCustomer;
20
20
  address?: StorefrontCheckoutAddress;
21
+ /** Structured address; without it the order keeps only the free-text note. */
22
+ shippingAddress?: ShippingAddressInput;
23
+ paymentMethod?: PaymentMethodKind;
21
24
  markPaid?: boolean;
22
25
  }
23
26
  export interface PlaceStorefrontOrderResult {
@@ -29,5 +32,5 @@ export interface PlaceStorefrontOrderResult {
29
32
  * It owns the order payload math and customer-session handshake so app-owned
30
33
  * routes can customize UX without copying provider/business logic.
31
34
  */
32
- export declare function placeStorefrontOrder({ config, cart, session, customer, address, markPaid, }: PlaceStorefrontOrderInput): Promise<PlaceStorefrontOrderResult>;
35
+ export declare function placeStorefrontOrder({ config, cart, session, customer, address, shippingAddress, paymentMethod, markPaid, }: PlaceStorefrontOrderInput): Promise<PlaceStorefrontOrderResult>;
33
36
  //# sourceMappingURL=checkout.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"checkout.d.ts","sourceRoot":"","sources":["../../src/workflows/checkout.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAEhD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAA;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAErD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAE3D,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,wBAAwB,CAAA;IAChC,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,OAAO,GAAG,cAAc,GAAG,iBAAiB,CAAC,CAAA;IACnE,OAAO,CAAC,EAAE,IAAI,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,CAAA;IACpD,QAAQ,EAAE,0BAA0B,CAAA;IACpC,OAAO,CAAC,EAAE,yBAAyB,CAAA;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,KAAK,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;CACnB;AAaD;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,EACzC,MAAM,EACN,IAAI,EACJ,OAAO,EACP,QAAQ,EACR,OAAO,EACP,QAAgB,GACjB,EAAE,yBAAyB,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAiCjE"}
1
+ {"version":3,"file":"checkout.d.ts","sourceRoot":"","sources":["../../src/workflows/checkout.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAEzF,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAA;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAErD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAE3D,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,wBAAwB,CAAA;IAChC,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,OAAO,GAAG,cAAc,GAAG,iBAAiB,CAAC,CAAA;IACnE,OAAO,CAAC,EAAE,IAAI,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,CAAA;IACpD,QAAQ,EAAE,0BAA0B,CAAA;IACpC,OAAO,CAAC,EAAE,yBAAyB,CAAA;IACnC,8EAA8E;IAC9E,eAAe,CAAC,EAAE,oBAAoB,CAAA;IACtC,aAAa,CAAC,EAAE,iBAAiB,CAAA;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,KAAK,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;CACnB;AA4BD;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,EACzC,MAAM,EACN,IAAI,EACJ,OAAO,EACP,QAAQ,EACR,OAAO,EACP,eAAe,EACf,aAAa,EACb,QAAgB,GACjB,EAAE,yBAAyB,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAyCjE"}
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@fayz-ai/storefront",
3
- "version": "0.7.0",
3
+ "fayz": {
4
+ "status": "beta"
5
+ },
6
+ "version": "0.8.2",
4
7
  "description": "Fayz SDK storefront kit — Shopify-style customer-facing ecommerce template components",
5
8
  "type": "module",
6
9
  "main": "./dist/index.cjs",
@@ -40,12 +43,12 @@
40
43
  "@supabase/supabase-js": "^2.45.0",
41
44
  "lucide-react": "^0.400.0",
42
45
  "zustand": "^4.5.0",
43
- "@fayz-ai/core": "^0.6.6",
44
- "@fayz-ai/plugin-auth": "^0.1.2",
45
- "@fayz-ai/shop": "^0.7.0",
46
- "@fayz-ai/ui": "^0.6.6",
47
- "@fayz-ai/sdk": "^0.6.6",
48
- "@fayz-ai/auth": "^0.6.6"
46
+ "@fayz-ai/auth": "^0.8.2",
47
+ "@fayz-ai/core": "^0.8.2",
48
+ "@fayz-ai/plugin-auth": "^0.8.0",
49
+ "@fayz-ai/shop": "^0.8.2",
50
+ "@fayz-ai/sdk": "^0.8.2",
51
+ "@fayz-ai/ui": "^0.8.1"
49
52
  },
50
53
  "peerDependencies": {
51
54
  "react": "^18.0.0 || ^19.0.0",
@@ -64,6 +67,9 @@
64
67
  "template"
65
68
  ],
66
69
  "license": "MIT",
70
+ "publishConfig": {
71
+ "access": "public"
72
+ },
67
73
  "scripts": {
68
74
  "build": "tsup && tsc --emitDeclarationOnly --declaration --declarationMap --noEmit false",
69
75
  "dev": "tsup --watch",
package/src/auth.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  type AuthPluginOptions,
6
6
  } from '@fayz-ai/plugin-auth'
7
7
  import { getShopProvider } from '@fayz-ai/shop/runtime'
8
+ import { setShopAccessTokenResolver } from '@fayz-ai/sdk/shop'
8
9
  import { useSessionStore } from './stores/session.store'
9
10
 
10
11
  // ---------------------------------------------------------------------------
@@ -64,13 +65,41 @@ export function resolveAuthAdapter(
64
65
  }).adapter
65
66
  }
66
67
 
68
+ // The shopper's current access token, handed to the shop REST client so its
69
+ // requests reach PostgREST as the signed-in user instead of as `anon`. Held
70
+ // here rather than in the session store because it is a credential: it must not
71
+ // be persisted to localStorage alongside the customer id. The Supabase client
72
+ // owns its own refresh; this is only a mirror of the live value.
73
+ let _accessToken: string | null = null
74
+
75
+ /** Whether an authenticated identity was ever seen this session. Distinguishes
76
+ * "signed out" from "was never signed in" — see the onAuthStateChange guard. */
77
+ let _hadAuthUser = false
78
+
79
+ /** Refresh the mirrored token from the adapter. Safe to call on any event. */
80
+ async function syncAccessToken(adapter: AuthAdapter): Promise<void> {
81
+ try {
82
+ const existing = await adapter.getSession()
83
+ _accessToken = existing?.session.accessToken ?? null
84
+ } catch {
85
+ _accessToken = null
86
+ }
87
+ }
88
+
67
89
  /** Called once by createStorefrontApp. Restores any persisted session. */
68
90
  export function initCustomerAuth(adapter: AuthAdapter): void {
69
91
  _adapter = adapter
92
+
93
+ // Registered before the first await so no request can slip out anonymously
94
+ // while the session is still being restored.
95
+ setShopAccessTokenResolver(() => _accessToken)
96
+
70
97
  adapter
71
98
  .getSession()
72
99
  .then((existing) => {
100
+ _accessToken = existing?.session.accessToken ?? null
73
101
  if (!existing) return
102
+ _hadAuthUser = true
74
103
  const store = useSessionStore.getState()
75
104
  // Re-link only when the persisted shop session doesn't match the auth user
76
105
  if (store.email !== existing.user.email) {
@@ -78,8 +107,26 @@ export function initCustomerAuth(adapter: AuthAdapter): void {
78
107
  }
79
108
  })
80
109
  .catch(() => {})
110
+
81
111
  adapter.onAuthStateChange((user) => {
82
- if (!user) useSessionStore.getState().signOut()
112
+ if (!user) {
113
+ _accessToken = null
114
+ // Only tear down the shop session if there WAS an authenticated identity
115
+ // to lose. The Supabase adapter fires this once on boot with a null user
116
+ // (INITIAL_SESSION), and clearing unconditionally wiped the guest's local
117
+ // session on every page load — a guest who checked out could no longer
118
+ // find their order after a reload. The mock adapter never surfaced this
119
+ // because it always restored a session.
120
+ if (_hadAuthUser) {
121
+ _hadAuthUser = false
122
+ useSessionStore.getState().signOut()
123
+ }
124
+ return
125
+ }
126
+ _hadAuthUser = true
127
+ // Covers token refresh too, not just sign-in: the callback carries the user
128
+ // but not the token, and a stale token would 401 every read an hour in.
129
+ void syncAccessToken(adapter)
83
130
  })
84
131
  }
85
132
 
@@ -137,6 +184,11 @@ export async function establishCustomerSession(
137
184
  if (opts?.password) {
138
185
  const adapter = getCustomerAuthAdapter()
139
186
  const session = await adapter.signIn(normalized, opts.password)
187
+ // Before linkCustomer, not after: linkCustomer calls shop_resolve_customer,
188
+ // which reads auth.uid() to fill auth_user_id. Sent with the old token that
189
+ // link silently no-ops and the account stays unlinked forever.
190
+ _accessToken = session.accessToken ?? null
191
+ _hadAuthUser = true
140
192
  return linkCustomer(session.user.email ? session.user : { ...session.user, email: normalized }, opts.name)
141
193
  }
142
194
  // Guest / passwordless checkout: do NOT create an auth account (that made
@@ -159,6 +211,8 @@ export async function signUpCustomer(
159
211
  const adapter = getCustomerAuthAdapter()
160
212
  const normalized = email.trim().toLowerCase()
161
213
  const session = await adapter.signUp(normalized, password, name)
214
+ _accessToken = session.accessToken ?? null
215
+ _hadAuthUser = true
162
216
  return linkCustomer(session.user.email ? session.user : { ...session.user, email: normalized }, name)
163
217
  }
164
218
 
@@ -166,6 +220,8 @@ export async function signOutCustomer(): Promise<void> {
166
220
  try {
167
221
  await getCustomerAuthAdapter().signOut()
168
222
  } finally {
223
+ _accessToken = null
224
+ _hadAuthUser = false
169
225
  useSessionStore.getState().signOut()
170
226
  }
171
227
  }
@@ -13,6 +13,9 @@ import { useStorefrontConfig } from '../config'
13
13
  import { navigateTo } from '../router'
14
14
  import { formatMoney } from '../format'
15
15
  import { QuantityInput } from './QuantityInput'
16
+ import { formatPostalCode } from '@fayz-ai/core'
17
+ import { useDeliveryStore } from '../stores/delivery.store'
18
+ import { DeliveryEstimator } from './DeliveryEstimator'
16
19
  import { TID } from '../testids'
17
20
  import { SmoothImage } from './SmoothImage'
18
21
 
@@ -34,9 +37,15 @@ export function CartDrawer() {
34
37
  return () => document.removeEventListener('keydown', onKey)
35
38
  }, [cart.isOpen, cart.closeDrawer])
36
39
 
40
+ // Subscribed, not just read: selectShipping consults this store through
41
+ // getState(), which does not subscribe, so without this hook a fresh quote
42
+ // would change the price and leave the drawer showing the old one.
43
+ const delivery = useDeliveryStore()
44
+
37
45
  const subtotal = selectSubtotal(cart)
38
46
  const discountTotal = selectDiscountTotal(cart)
39
47
  const shipping = selectShipping(cart, config)
48
+
40
49
  const total = selectTotal(cart, config)
41
50
  const money = (v: number) => formatMoney(v, config.currency, config.locale)
42
51
 
@@ -198,6 +207,12 @@ export function CartDrawer() {
198
207
  </div>
199
208
  )}
200
209
 
210
+ {/* Second chance to give a CEP for anyone who added to cart from
211
+ the catalogue and never opened a product page. */}
212
+ <div className="mb-3">
213
+ <DeliveryEstimator compact />
214
+ </div>
215
+
201
216
  <dl className="space-y-1.5 text-sm">
202
217
  <div className="flex justify-between">
203
218
  <dt className="text-muted-foreground">Subtotal</dt>
@@ -219,7 +234,14 @@ export function CartDrawer() {
219
234
  </div>
220
235
  )}
221
236
  <div className="flex justify-between">
222
- <dt className="text-muted-foreground">Frete</dt>
237
+ <dt className="text-muted-foreground">
238
+ Frete
239
+ {delivery.status === 'served' && delivery.postalCode && (
240
+ <span className="ml-1 text-xs">
241
+ · {formatPostalCode(delivery.postalCode)}
242
+ </span>
243
+ )}
244
+ </dt>
223
245
  <dd data-testid={TID.cartShipping} data-price={shipping.toFixed(2)}>
224
246
  {shipping === 0 ? 'Grátis' : money(shipping)}
225
247
  </dd>
@@ -0,0 +1,157 @@
1
+ import React, { useState } from 'react'
2
+ import { MapPin, Truck } from 'lucide-react'
3
+ import { formatPostalCode } from '@fayz-ai/core'
4
+ import { useDeliveryStore, resolveOptionRate } from '../stores/delivery.store'
5
+ import { useCartStore, selectSubtotal } from '../stores/cart.store'
6
+ import { useStorefrontConfig } from '../config'
7
+ import { formatMoney } from '../format'
8
+ import { TID } from '../testids'
9
+
10
+ /**
11
+ * "Do you deliver to me, for how much, and when" — asked on the product page,
12
+ * before the shopper has invested anything in a cart.
13
+ *
14
+ * The CEP is kept in the delivery store, so the answer follows them to the cart
15
+ * and lands at checkout with street/district/city/UF already filled. That is
16
+ * the actual payoff: the buyer types a house number instead of an address.
17
+ *
18
+ * `subtotal` is the PRE-discount cart subtotal, the same number shop_place_order
19
+ * uses to resolve free-above thresholds. Quoting against anything else is how
20
+ * the cart and the order end up disagreeing (see migration 0017).
21
+ */
22
+ export function DeliveryEstimator({ compact = false }: { compact?: boolean }) {
23
+ const config = useStorefrontConfig()
24
+ const cart = useCartStore()
25
+ const delivery = useDeliveryStore()
26
+ const [draft, setDraft] = useState(() => formatPostalCode(delivery.postalCode))
27
+
28
+ const subtotal = selectSubtotal(cart)
29
+ const money = (value: number) => formatMoney(value, config.currency, config.locale)
30
+ const loading = delivery.status === 'loading'
31
+
32
+ async function submit(event: React.FormEvent) {
33
+ event.preventDefault()
34
+ await delivery.resolve(draft, subtotal)
35
+ }
36
+
37
+ function eta(option: { etaMinDays?: number; etaMaxDays?: number }): string | null {
38
+ const { etaMinDays: min, etaMaxDays: max } = option
39
+ if (min == null && max == null) return null
40
+ if (min === 0 && (max === 0 || max === 1)) return 'hoje ou amanhã'
41
+ if (min != null && max != null && min !== max) return `${min} a ${max} dias úteis`
42
+ return `${max ?? min} dia(s) útil(eis)`
43
+ }
44
+
45
+ return (
46
+ <section
47
+ data-testid={TID.deliveryEstimator}
48
+ className={compact ? 'rounded-lg border p-3' : 'mt-6 rounded-lg border p-4'}
49
+ >
50
+ <div className="flex items-center gap-2 text-sm font-semibold">
51
+ <Truck className="h-4 w-4 text-primary" />
52
+ Calcular entrega
53
+ </div>
54
+
55
+ <form onSubmit={submit} className="mt-3 flex gap-2">
56
+ <input
57
+ data-testid={TID.deliveryCepInput}
58
+ aria-label="CEP"
59
+ inputMode="numeric"
60
+ autoComplete="postal-code"
61
+ placeholder="00000-000"
62
+ value={draft}
63
+ onChange={(event) => setDraft(formatPostalCode(event.target.value))}
64
+ className="min-w-0 flex-1 rounded-lg border bg-background px-3 py-2 text-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/10"
65
+ />
66
+ <button
67
+ type="submit"
68
+ data-testid={TID.deliveryCepSubmit}
69
+ disabled={loading}
70
+ className="shrink-0 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground transition hover:opacity-90 disabled:opacity-50"
71
+ >
72
+ {loading ? '...' : 'Calcular'}
73
+ </button>
74
+ </form>
75
+
76
+ {/* Shown whenever there is one, including alongside a price: the range
77
+ may be covered even though the CEP itself was mistyped. */}
78
+ {delivery.error && (
79
+ <p data-testid={TID.deliveryError} className="mt-3 text-sm text-destructive">
80
+ {delivery.error}
81
+ </p>
82
+ )}
83
+
84
+ {delivery.address && (
85
+ <p data-testid={TID.deliveryAddress} className="mt-3 flex items-start gap-1.5 text-xs text-muted-foreground">
86
+ <MapPin className="mt-0.5 h-3.5 w-3.5 shrink-0" />
87
+ <span>
88
+ {[delivery.address.street, delivery.address.district].filter(Boolean).join(', ')}
89
+ {delivery.address.street || delivery.address.district ? ' — ' : ''}
90
+ {delivery.address.city}/{delivery.address.state}
91
+ </span>
92
+ </p>
93
+ )}
94
+
95
+ {delivery.status === 'served' && (
96
+ <ul className="mt-3 space-y-2">
97
+ {delivery.options.map((option) => {
98
+ const selected = delivery.selectedZoneId === option.zoneId
99
+ const label = eta(option)
100
+ // Resolved for the cart as it stands right now, not for whatever it
101
+ // held when the CEP was typed — the same rule the server applies.
102
+ const rate = resolveOptionRate(option, subtotal)
103
+ return (
104
+ <li key={option.zoneId}>
105
+ <button
106
+ type="button"
107
+ data-testid={TID.deliveryOption}
108
+ data-zone={option.zoneId}
109
+ data-rate={rate.toFixed(2)}
110
+ aria-pressed={selected}
111
+ onClick={() => delivery.selectZone(option.zoneId)}
112
+ className={`flex w-full items-center justify-between gap-3 rounded-lg border px-3 py-2 text-left text-sm transition ${
113
+ selected ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted/40'
114
+ }`}
115
+ >
116
+ <span className="min-w-0">
117
+ <span className="block truncate font-medium">
118
+ {option.carrier ? `${option.carrier} · ${option.name}` : option.name}
119
+ </span>
120
+ {label && <span className="block text-xs text-muted-foreground">{label}</span>}
121
+ </span>
122
+ <span className="shrink-0 font-semibold">
123
+ {rate === 0 ? 'Grátis' : money(rate)}
124
+ </span>
125
+ </button>
126
+ </li>
127
+ )
128
+ })}
129
+ </ul>
130
+ )}
131
+
132
+ {/*
133
+ No options for a valid address. For a store that configured zones this
134
+ means "outside our delivery area" and checkout will refuse the order, so
135
+ it is said plainly here rather than at the end of the funnel. A store
136
+ with no zones at all never reaches this state — it quotes nothing and
137
+ keeps its flat rate.
138
+ */}
139
+ {delivery.status === 'unserved' && (
140
+ <p data-testid={TID.deliveryUnserved} className="mt-3 text-sm text-destructive">
141
+ Ainda não entregamos nesse CEP.
142
+ </p>
143
+ )}
144
+
145
+ {delivery.postalCode && (
146
+ <button
147
+ type="button"
148
+ data-testid={TID.deliveryClear}
149
+ onClick={() => { delivery.clear(); setDraft('') }}
150
+ className="mt-3 text-xs text-muted-foreground underline-offset-2 hover:underline"
151
+ >
152
+ Trocar CEP
153
+ </button>
154
+ )}
155
+ </section>
156
+ )
157
+ }