@asteby/storefront-react 0.4.0 → 0.6.0

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.
@@ -0,0 +1,243 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { PromotionAnnouncement, PromotionMessages, CartPromotions, CartRecommendation, Money } from '@asteby/storefront';
4
+
5
+ /**
6
+ * Pure mechanics of the entry pop-up (frequency, exclusions, crawlers, triggers). No DOM, no React:
7
+ * every function takes its inputs, so each rule is a row in a table test.
8
+ */
9
+ type PromoFrequency = 'session' | 'days' | 'campaign';
10
+ /** Paths where a promotional pop-up is never shown (cart, payment, thanks, account, auth, legal, editor previews). */
11
+ declare const DEFAULT_PROMO_EXCLUDED_PATHS: readonly string[];
12
+ type PathPattern = string | RegExp;
13
+ /** `/mx/carrito` and `/carrito` both reduce to `/carrito` (an optional 2-letter market/locale segment is ignored). */
14
+ declare function stripMarketPrefix(pathname: string): string;
15
+ /** True when the pop-up must not appear on `pathname`. Case-insensitive; the market prefix does not matter. */
16
+ declare function isExcludedPath(pathname: string, patterns?: readonly PathPattern[]): boolean;
17
+ /** Known crawlers and automation (`navigator.webdriver`). They never get the pop-up. */
18
+ declare function isCrawler(userAgent: string | null | undefined, webdriver?: boolean): boolean;
19
+ /** Storage seam: cookie for cross-tab memory, sessionStorage for "once per session". Replaceable in tests and shops. */
20
+ type PromoSeenStore = {
21
+ get(name: string): string | null;
22
+ set(name: string, value: string, options: {
23
+ days?: number;
24
+ session?: boolean;
25
+ }): void;
26
+ };
27
+ declare const PROMO_COOKIE = "sf_promo";
28
+ declare function memorySeenStore(): PromoSeenStore & {
29
+ data: Map<string, string>;
30
+ };
31
+ /** Default store: `sessionStorage` for session frequency, a first-party cookie otherwise. Fails closed to memory. */
32
+ declare function browserSeenStore(): PromoSeenStore;
33
+ type SeenRecord = {
34
+ id: string;
35
+ version: number;
36
+ at: number;
37
+ };
38
+ declare function serializeSeen(r: SeenRecord): string;
39
+ declare function parseSeen(raw: string | null | undefined): SeenRecord | null;
40
+ type FrequencyInput = {
41
+ id: string;
42
+ version: number;
43
+ frequency: PromoFrequency;
44
+ /** Window for `days` (default 7). */
45
+ days?: number;
46
+ seen: SeenRecord | null;
47
+ now: number;
48
+ };
49
+ /**
50
+ * Frequency rule. A new `version` of the same announcement shows again.
51
+ * - `session`: once per browser session (the store is the session store).
52
+ * - `days`: once every N days.
53
+ * - `campaign`: once per campaign version.
54
+ */
55
+ declare function frequencyAllows({ id, version, frequency, days, seen, now }: FrequencyInput): boolean;
56
+ type PromoWindow = {
57
+ starts_at?: string | null;
58
+ ends_at?: string | null;
59
+ };
60
+ /** Inside the campaign dates (unparseable dates are ignored: the server decides, not the clock of a bad value). */
61
+ declare function withinWindow({ starts_at, ends_at }: PromoWindow, now: number): boolean;
62
+ type PromoTrigger = {
63
+ /** Show after N ms. `false` disables the timer. Default 5000. */
64
+ delayMs?: number | false;
65
+ /** Show on the Nth page view of the session. `false` disables. Default 2. */
66
+ pageviews?: number | false;
67
+ /** Show after scrolling this share of the page (0-100). `false` disables. Default 40. */
68
+ scrollPercent?: number | false;
69
+ };
70
+ declare const DEFAULT_PROMO_TRIGGER: Required<PromoTrigger>;
71
+ declare function resolveTrigger(t?: PromoTrigger | null): Required<PromoTrigger>;
72
+ /** Scroll depth as a percentage of the scrollable height (UI measure only). */
73
+ declare function scrollDepthPercent(scrollY: number, viewportHeight: number, docHeight: number): number;
74
+ /** Only same-site paths are valid CTAs (`/colecciones/x`). Anything else (`//evil`, `https:`, `javascript:`) is dropped. */
75
+ declare function safeInternalPath(href: string | null | undefined): string | null;
76
+
77
+ type PromoPopupEvent = {
78
+ type: 'view';
79
+ id: string;
80
+ version: number;
81
+ via: 'delay' | 'pageviews' | 'scroll';
82
+ } | {
83
+ type: 'close';
84
+ id: string;
85
+ how: 'esc' | 'x' | 'backdrop';
86
+ } | {
87
+ type: 'cta';
88
+ id: string;
89
+ };
90
+ type PromoPopupProps = {
91
+ /** Content as data from `GET /storefront/promotions`. `null`/absent: nothing happens. Amounts are the engine's. */
92
+ announcement: PromotionAnnouncement | null | undefined;
93
+ /** Current path, from the router (so route changes are seen). Default: `location.pathname`. */
94
+ pathname?: string;
95
+ /** Paths where the pop-up never appears. Default: cart, payment, thanks, account, auth, legal, editor. */
96
+ excludePaths?: readonly PathPattern[];
97
+ /** Overrides `announcement.frequency` (default `days`). */
98
+ frequency?: PromoFrequency;
99
+ /** Window for `days` (default `announcement.frequency_days` or 7). */
100
+ frequencyDays?: number;
101
+ /** Overrides `announcement.trigger`. Defaults: 5 s, second page view or 40 % scroll, whichever comes first. */
102
+ trigger?: PromoTrigger;
103
+ locale?: string;
104
+ messages?: Partial<PromotionMessages>;
105
+ /** Where "already seen" is remembered. Default: cookie `sf_promo` (7 days) or sessionStorage for `session`. */
106
+ store?: PromoSeenStore;
107
+ /** Analytics hook: view, close (how) and CTA. No personal data. */
108
+ onEvent?: (event: PromoPopupEvent) => void;
109
+ /** Renders the CTA (use your router's `Link`). Default: a plain `<a href>`. */
110
+ renderCta?: (props: {
111
+ href: string;
112
+ label: string;
113
+ onClick: () => void;
114
+ className?: string;
115
+ }) => ReactNode;
116
+ /** Test hooks. */
117
+ now?: () => number;
118
+ userAgent?: string;
119
+ webdriver?: boolean;
120
+ className?: string;
121
+ };
122
+ /**
123
+ * Entry pop-up mechanism. The CONTENT is data (`announcement`); this component only decides when to show it.
124
+ *
125
+ * - Client only: it renders nothing on the server and on the first client render, so SSR/SEO and hydration
126
+ * are untouched and the pop-up never delays the LCP.
127
+ * - Shown at most per `frequency` (session / every N days / once per campaign version), never on excluded
128
+ * paths, to crawlers or outside the campaign dates.
129
+ * - Accessible: native `<dialog>` (focus trap, inert background), Esc, a 44x44 close button, labelled title,
130
+ * focus returns to where it was, no entry animation (nothing to disable for `prefers-reduced-motion`).
131
+ */
132
+ declare function PromoPopup(props: PromoPopupProps): react.JSX.Element | null;
133
+
134
+ type ProgressBarProps = {
135
+ /** Visible text (also `aria-valuetext`). */
136
+ text: string;
137
+ /** 0-100, from the server. */
138
+ percent: number;
139
+ label: string;
140
+ showBar?: boolean;
141
+ milestones?: {
142
+ id: string;
143
+ label: string;
144
+ threshold: string;
145
+ reached: boolean;
146
+ position: number;
147
+ }[];
148
+ state: string;
149
+ kind: 'discount' | 'shipping';
150
+ className?: string;
151
+ };
152
+ /**
153
+ * One accessible bar: color is never the only signal (text states the remaining amount; milestones are text).
154
+ * The fill width is the server percentage; the transition is off for `prefers-reduced-motion`.
155
+ */
156
+ declare function PromoProgressBar({ text, percent, label, showBar, milestones, state, kind, className }: ProgressBarProps): react.JSX.Element;
157
+ type CartProgressBarsProps = {
158
+ /** `Cart.promotions` exactly as the server sent it. */
159
+ promotions: CartPromotions | null | undefined;
160
+ locale?: string;
161
+ messages?: Partial<PromotionMessages>;
162
+ /** Show the tier milestones under the discount bar. Default true. */
163
+ showMilestones?: boolean;
164
+ className?: string;
165
+ };
166
+ /**
167
+ * Automatic-discount bar and free-shipping bar of the cart (or drawer). Pure view of `Cart.promotions`:
168
+ * `remaining`, `percent` and `reached` come from OPS; the shop never subtracts or divides.
169
+ * Renders nothing without promotions.
170
+ */
171
+ declare function CartProgressBars({ promotions, locale: localeProp, messages, showMilestones, className }: CartProgressBarsProps): react.JSX.Element | null;
172
+
173
+ type CrossSellAddResult = void | {
174
+ ok?: boolean;
175
+ error?: 'sold_out' | 'failed';
176
+ };
177
+ type CrossSellModalProps = {
178
+ open: boolean;
179
+ /** `Cart.promotions` of the CURRENT cart; after an add, pass the new cart's and the modal updates. */
180
+ promotions: CartPromotions | null | undefined;
181
+ recommendations: CartRecommendation[];
182
+ /** Total saved so far, from the server (`cart.cost.discount`). */
183
+ totalSaved: Money;
184
+ /**
185
+ * The shop adds the variant (server action / fetcher) and then re-renders with the new `promotions` and
186
+ * `recommendations`. Return `{ error: 'sold_out' }` for a 409 so the card tells the buyer to pick another flavor.
187
+ */
188
+ onAdd: (input: {
189
+ productId: string;
190
+ variantId: string;
191
+ recommendation: CartRecommendation;
192
+ }) => Promise<CrossSellAddResult> | CrossSellAddResult;
193
+ /** "Continuar al pago". The shop navigates to payment. */
194
+ onContinue: () => void;
195
+ /** "Continuar sin agregar", X, Esc or backdrop. The shop marks the modal as seen and navigates to payment. */
196
+ onSkip: (reason: 'skip' | 'esc' | 'x' | 'backdrop') => void;
197
+ locale?: string;
198
+ messages?: Partial<PromotionMessages>;
199
+ /** Renders a card image (use your `Image`). Default: a lazy `<img>` of the featured image. */
200
+ renderImage?: (rec: CartRecommendation) => ReactNode;
201
+ className?: string;
202
+ };
203
+ /**
204
+ * Pre-payment upsell modal ("Completa tu compra y desbloquea más").
205
+ *
206
+ * Mechanism only: the shop decides when to open it (once per cart, JS only) and what "continue" means.
207
+ * Every number comes from the server: remaining amount, rewards, progress, total saved, and the
208
+ * "Desbloquea tu 5 %" tag (`unlocks`). Add errors stay on their card; the rest stays usable.
209
+ * Accessible: native dialog, labelled title, focus starts on the title (not on a buy button), Esc = skip,
210
+ * `role="progressbar"` with spoken remaining amount, level changes announced via `aria-live`.
211
+ */
212
+ declare function CrossSellModal(props: CrossSellModalProps): react.JSX.Element | null;
213
+
214
+ type OrderNoteStatus = 'idle' | 'dirty' | 'saving' | 'saved' | 'error';
215
+ type OrderNoteProps = {
216
+ /** Saved note (`Cart.note`). */
217
+ value: string | null | undefined;
218
+ /** Persists the note (`client.carts.setNote` through the shop action). Must resolve once stored. Throw to show the error. */
219
+ onSave: (note: string | null) => Promise<void> | void;
220
+ /** Autosave debounce in ms. Default 800. `false`: save only on blur / button. */
221
+ debounceMs?: number | false;
222
+ /**
223
+ * No-JS form: `<form method="post" action>` with a hidden `intent` and a "Guardar nota" button, hidden once the
224
+ * component hydrates (the autosave takes over). The shop's route action handles it.
225
+ */
226
+ form?: {
227
+ action?: string;
228
+ method?: 'post';
229
+ intent?: string;
230
+ intentName?: string;
231
+ };
232
+ locale?: string;
233
+ messages?: Partial<PromotionMessages>;
234
+ className?: string;
235
+ maxLength?: number;
236
+ };
237
+ /**
238
+ * "Añadir nota" for the order. A real `<textarea>` with a visible label; the server enforces 1000 characters and
239
+ * plain text. With JavaScript it autosaves (debounced, on blur) and announces "Guardada"; without it, the form posts.
240
+ */
241
+ declare function OrderNote({ value, onSave, debounceMs, form, locale: localeProp, messages, className, maxLength }: OrderNoteProps): react.JSX.Element;
242
+
243
+ export { CartProgressBars, type CartProgressBarsProps, type CrossSellAddResult, CrossSellModal, type CrossSellModalProps, DEFAULT_PROMO_EXCLUDED_PATHS, DEFAULT_PROMO_TRIGGER, type FrequencyInput, OrderNote, type OrderNoteProps, type OrderNoteStatus, PROMO_COOKIE, type PathPattern, type ProgressBarProps, type PromoFrequency, PromoPopup, type PromoPopupEvent, type PromoPopupProps, PromoProgressBar, type PromoSeenStore, type PromoTrigger, type PromoWindow, type SeenRecord, browserSeenStore, frequencyAllows, isCrawler, isExcludedPath, memorySeenStore, parseSeen, resolveTrigger, safeInternalPath, scrollDepthPercent, serializeSeen, stripMarketPrefix, withinWindow };