@neowhale/storefront 0.1.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.
- package/dist/chunk-PR4PUHVN.js +273 -0
- package/dist/chunk-PR4PUHVN.js.map +1 -0
- package/dist/chunk-XMLH3TLA.cjs +275 -0
- package/dist/chunk-XMLH3TLA.cjs.map +1 -0
- package/dist/index.cjs +12 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +269 -0
- package/dist/index.d.ts +269 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/next/index.cjs +123 -0
- package/dist/next/index.cjs.map +1 -0
- package/dist/next/index.d.cts +117 -0
- package/dist/next/index.d.ts +117 -0
- package/dist/next/index.js +115 -0
- package/dist/next/index.js.map +1 -0
- package/dist/react/index.cjs +673 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +243 -0
- package/dist/react/index.d.ts +243 -0
- package/dist/react/index.js +657 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import { WhaleClient } from '../chunk-PR4PUHVN.js';
|
|
2
|
+
import { createContext, useContext, useRef, useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { usePathname } from 'next/navigation';
|
|
4
|
+
import { createStore } from 'zustand/vanilla';
|
|
5
|
+
import { persist } from 'zustand/middleware';
|
|
6
|
+
import { useStore } from 'zustand';
|
|
7
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
8
|
+
|
|
9
|
+
var WhaleContext = createContext(null);
|
|
10
|
+
function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart) {
|
|
11
|
+
return createStore()(
|
|
12
|
+
persist(
|
|
13
|
+
(set, get) => ({
|
|
14
|
+
// ── Initial state ────────────────────────────────────────────────
|
|
15
|
+
cartId: null,
|
|
16
|
+
items: [],
|
|
17
|
+
itemCount: 0,
|
|
18
|
+
subtotal: 0,
|
|
19
|
+
taxAmount: 0,
|
|
20
|
+
total: 0,
|
|
21
|
+
taxBreakdown: [],
|
|
22
|
+
cartOpen: false,
|
|
23
|
+
cartLoading: false,
|
|
24
|
+
productImages: {},
|
|
25
|
+
addItemInFlight: false,
|
|
26
|
+
// ── Cart UI ──────────────────────────────────────────────────────
|
|
27
|
+
openCart: () => set({ cartOpen: true }),
|
|
28
|
+
closeCart: () => set({ cartOpen: false }),
|
|
29
|
+
toggleCart: () => set((s) => ({ cartOpen: !s.cartOpen })),
|
|
30
|
+
// ── Cart data ────────────────────────────────────────────────────
|
|
31
|
+
initCart: async () => {
|
|
32
|
+
const { cartId, syncCart } = get();
|
|
33
|
+
if (cartId) {
|
|
34
|
+
try {
|
|
35
|
+
await syncCart();
|
|
36
|
+
} catch {
|
|
37
|
+
const cart = await client.createCart();
|
|
38
|
+
applyCart(set, get, cart);
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const cart = await client.createCart();
|
|
44
|
+
applyCart(set, get, cart);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error("[whale-storefront] initCart failed:", err);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
syncCart: async () => {
|
|
50
|
+
const { cartId, productImages } = get();
|
|
51
|
+
if (!cartId) return;
|
|
52
|
+
try {
|
|
53
|
+
const cart = await client.getCart(cartId);
|
|
54
|
+
const items = (cart.items ?? []).map((item) => ({
|
|
55
|
+
...item,
|
|
56
|
+
image_url: item.image_url || productImages[item.product_id] || null
|
|
57
|
+
}));
|
|
58
|
+
set({
|
|
59
|
+
items,
|
|
60
|
+
itemCount: cart.item_count ?? 0,
|
|
61
|
+
subtotal: cart.subtotal ?? 0,
|
|
62
|
+
taxAmount: cart.tax_amount ?? 0,
|
|
63
|
+
total: cart.total ?? 0,
|
|
64
|
+
taxBreakdown: cart.tax_breakdown ?? []
|
|
65
|
+
});
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error("[whale-storefront] syncCart failed:", err);
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
addItem: async (productId, quantity, tier, unitPrice, imageUrl, productName) => {
|
|
72
|
+
if (get().addItemInFlight) return;
|
|
73
|
+
set({ cartLoading: true, addItemInFlight: true });
|
|
74
|
+
try {
|
|
75
|
+
let { cartId } = get();
|
|
76
|
+
if (!cartId) {
|
|
77
|
+
await get().initCart();
|
|
78
|
+
cartId = get().cartId;
|
|
79
|
+
}
|
|
80
|
+
if (!cartId) throw new Error("Could not initialise cart");
|
|
81
|
+
if (imageUrl) {
|
|
82
|
+
set((s) => ({ productImages: { ...s.productImages, [productId]: imageUrl } }));
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
await client.addToCart(cartId, productId, quantity, { tier, unitPrice });
|
|
86
|
+
} catch (err) {
|
|
87
|
+
const status = err.status;
|
|
88
|
+
if (status === 404 || status === 410) {
|
|
89
|
+
const newCart = await client.createCart();
|
|
90
|
+
set({ cartId: newCart.id });
|
|
91
|
+
await client.addToCart(newCart.id, productId, quantity, { tier, unitPrice });
|
|
92
|
+
} else {
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
await get().syncCart();
|
|
97
|
+
onAddToCart?.(productId, productName || "", quantity, unitPrice || 0, tier);
|
|
98
|
+
} finally {
|
|
99
|
+
set({ cartLoading: false, addItemInFlight: false });
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
updateQuantity: async (itemId, quantity) => {
|
|
103
|
+
set({ cartLoading: true });
|
|
104
|
+
try {
|
|
105
|
+
const { cartId } = get();
|
|
106
|
+
if (!cartId) return;
|
|
107
|
+
await client.updateCartItem(cartId, itemId, quantity);
|
|
108
|
+
await get().syncCart();
|
|
109
|
+
} finally {
|
|
110
|
+
set({ cartLoading: false });
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
removeItem: async (itemId, productName) => {
|
|
114
|
+
set({ cartLoading: true });
|
|
115
|
+
try {
|
|
116
|
+
const { cartId, items } = get();
|
|
117
|
+
if (!cartId) return;
|
|
118
|
+
const item = items.find((i) => i.id === itemId);
|
|
119
|
+
await client.removeCartItem(cartId, itemId);
|
|
120
|
+
await get().syncCart();
|
|
121
|
+
if (item) {
|
|
122
|
+
onRemoveFromCart?.(item.product_id, productName || item.product_name);
|
|
123
|
+
}
|
|
124
|
+
} finally {
|
|
125
|
+
set({ cartLoading: false });
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
clearCart: () => {
|
|
129
|
+
set({
|
|
130
|
+
cartId: null,
|
|
131
|
+
items: [],
|
|
132
|
+
itemCount: 0,
|
|
133
|
+
subtotal: 0,
|
|
134
|
+
taxAmount: 0,
|
|
135
|
+
total: 0,
|
|
136
|
+
taxBreakdown: [],
|
|
137
|
+
productImages: {}
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
checkout: async (customerEmail, payment) => {
|
|
141
|
+
const { cartId } = get();
|
|
142
|
+
if (!cartId) throw new Error("No active cart");
|
|
143
|
+
set({ cartLoading: true });
|
|
144
|
+
try {
|
|
145
|
+
const order = await client.checkout(cartId, customerEmail, payment);
|
|
146
|
+
set({
|
|
147
|
+
cartId: null,
|
|
148
|
+
items: [],
|
|
149
|
+
itemCount: 0,
|
|
150
|
+
subtotal: 0,
|
|
151
|
+
taxAmount: 0,
|
|
152
|
+
total: 0,
|
|
153
|
+
taxBreakdown: [],
|
|
154
|
+
productImages: {},
|
|
155
|
+
cartOpen: false
|
|
156
|
+
});
|
|
157
|
+
return order;
|
|
158
|
+
} finally {
|
|
159
|
+
set({ cartLoading: false });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}),
|
|
163
|
+
// ── Persist config ─────────────────────────────────────────────────
|
|
164
|
+
{
|
|
165
|
+
name: `${storagePrefix}-cart`,
|
|
166
|
+
partialize: (state) => ({
|
|
167
|
+
cartId: state.cartId,
|
|
168
|
+
productImages: state.productImages
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
function applyCart(set, get, cart) {
|
|
175
|
+
const productImages = get().productImages;
|
|
176
|
+
const items = (cart.items ?? []).map((item) => ({
|
|
177
|
+
...item,
|
|
178
|
+
image_url: item.image_url || productImages[item.product_id] || null
|
|
179
|
+
}));
|
|
180
|
+
set({
|
|
181
|
+
cartId: cart.id,
|
|
182
|
+
items,
|
|
183
|
+
itemCount: cart.item_count ?? 0,
|
|
184
|
+
subtotal: cart.subtotal ?? 0,
|
|
185
|
+
taxAmount: cart.tax_amount ?? 0,
|
|
186
|
+
total: cart.total ?? 0,
|
|
187
|
+
taxBreakdown: cart.tax_breakdown ?? []
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
function createAuthStore(client, storagePrefix) {
|
|
191
|
+
return createStore()(
|
|
192
|
+
persist(
|
|
193
|
+
(set, get) => ({
|
|
194
|
+
// ── Initial state ────────────────────────────────────────────────
|
|
195
|
+
customer: null,
|
|
196
|
+
sessionToken: null,
|
|
197
|
+
sessionExpiresAt: null,
|
|
198
|
+
authLoading: false,
|
|
199
|
+
// ── Actions ──────────────────────────────────────────────────────
|
|
200
|
+
sendOTP: async (email) => {
|
|
201
|
+
set({ authLoading: true });
|
|
202
|
+
try {
|
|
203
|
+
const res = await client.sendCode(email);
|
|
204
|
+
return res.sent;
|
|
205
|
+
} catch (err) {
|
|
206
|
+
const message = err instanceof Error ? err.message : "Could not send code";
|
|
207
|
+
throw new Error(message);
|
|
208
|
+
} finally {
|
|
209
|
+
set({ authLoading: false });
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
verifyOTP: async (email, code) => {
|
|
213
|
+
set({ authLoading: true });
|
|
214
|
+
try {
|
|
215
|
+
const res = await client.verifyCode(email, code);
|
|
216
|
+
client.setSessionToken(res.token_hash);
|
|
217
|
+
set({
|
|
218
|
+
sessionToken: res.token_hash,
|
|
219
|
+
sessionExpiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString()
|
|
220
|
+
});
|
|
221
|
+
if (res.customer?.id) {
|
|
222
|
+
try {
|
|
223
|
+
const full = await client.getCustomer(res.customer.id);
|
|
224
|
+
set({ customer: full });
|
|
225
|
+
} catch {
|
|
226
|
+
set({ customer: res.customer });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return res.needs_profile ?? false;
|
|
230
|
+
} catch (err) {
|
|
231
|
+
const message = err instanceof Error ? err.message : "Verification failed";
|
|
232
|
+
throw new Error(message);
|
|
233
|
+
} finally {
|
|
234
|
+
set({ authLoading: false });
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
restoreSession: async () => {
|
|
238
|
+
const { sessionToken, sessionExpiresAt, customer } = get();
|
|
239
|
+
if (!sessionToken || !sessionExpiresAt) return;
|
|
240
|
+
if (new Date(sessionExpiresAt) <= /* @__PURE__ */ new Date()) {
|
|
241
|
+
client.setSessionToken(null);
|
|
242
|
+
set({ sessionToken: null, sessionExpiresAt: null, customer: null });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
client.setSessionToken(sessionToken);
|
|
246
|
+
if (customer?.id) {
|
|
247
|
+
try {
|
|
248
|
+
const fresh = await client.getCustomer(customer.id);
|
|
249
|
+
set({ customer: fresh });
|
|
250
|
+
} catch {
|
|
251
|
+
client.setSessionToken(null);
|
|
252
|
+
set({ sessionToken: null, sessionExpiresAt: null, customer: null });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
isSessionValid: () => {
|
|
257
|
+
const { sessionToken, sessionExpiresAt } = get();
|
|
258
|
+
if (!sessionToken || !sessionExpiresAt) return false;
|
|
259
|
+
return new Date(sessionExpiresAt) > /* @__PURE__ */ new Date();
|
|
260
|
+
},
|
|
261
|
+
logout: () => {
|
|
262
|
+
client.setSessionToken(null);
|
|
263
|
+
set({ customer: null, sessionToken: null, sessionExpiresAt: null });
|
|
264
|
+
},
|
|
265
|
+
fetchCustomer: async (id) => {
|
|
266
|
+
try {
|
|
267
|
+
const customer = await client.getCustomer(id);
|
|
268
|
+
set({ customer });
|
|
269
|
+
} catch (err) {
|
|
270
|
+
console.error("[whale-storefront] fetchCustomer failed:", err);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}),
|
|
274
|
+
// ── Persist config ─────────────────────────────────────────────────
|
|
275
|
+
{
|
|
276
|
+
name: `${storagePrefix}-auth`,
|
|
277
|
+
partialize: (state) => ({
|
|
278
|
+
sessionToken: state.sessionToken,
|
|
279
|
+
sessionExpiresAt: state.sessionExpiresAt,
|
|
280
|
+
customer: state.customer ? {
|
|
281
|
+
id: state.customer.id,
|
|
282
|
+
email: state.customer.email,
|
|
283
|
+
first_name: state.customer.first_name,
|
|
284
|
+
last_name: state.customer.last_name,
|
|
285
|
+
phone: state.customer.phone,
|
|
286
|
+
loyalty_points: state.customer.loyalty_points,
|
|
287
|
+
loyalty_tier: state.customer.loyalty_tier,
|
|
288
|
+
total_spent: state.customer.total_spent,
|
|
289
|
+
total_orders: state.customer.total_orders
|
|
290
|
+
} : null
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
)
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
var SESSION_KEY_SUFFIX = "-analytics-session";
|
|
297
|
+
function useAnalytics() {
|
|
298
|
+
const ctx = useContext(WhaleContext);
|
|
299
|
+
if (!ctx) throw new Error("useAnalytics must be used within <WhaleProvider>");
|
|
300
|
+
const { client, config } = ctx;
|
|
301
|
+
const sessionPromiseRef = useRef(null);
|
|
302
|
+
const sessionKey = `${config.storagePrefix}${SESSION_KEY_SUFFIX}`;
|
|
303
|
+
const getOrCreateSession = useCallback(async () => {
|
|
304
|
+
if (sessionPromiseRef.current) return sessionPromiseRef.current;
|
|
305
|
+
sessionPromiseRef.current = (async () => {
|
|
306
|
+
try {
|
|
307
|
+
const raw = localStorage.getItem(sessionKey);
|
|
308
|
+
if (raw) {
|
|
309
|
+
const stored = JSON.parse(raw);
|
|
310
|
+
if (Date.now() - stored.createdAt < config.sessionTtl) {
|
|
311
|
+
client.updateSession(stored.id, { last_active_at: (/* @__PURE__ */ new Date()).toISOString() }).catch(() => {
|
|
312
|
+
});
|
|
313
|
+
return stored.id;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
const session = await client.createSession({
|
|
320
|
+
user_agent: navigator.userAgent,
|
|
321
|
+
referrer: document.referrer || void 0
|
|
322
|
+
});
|
|
323
|
+
if (session?.id) {
|
|
324
|
+
localStorage.setItem(sessionKey, JSON.stringify({ id: session.id, createdAt: Date.now() }));
|
|
325
|
+
return session.id;
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
}
|
|
329
|
+
const fallbackId = `local-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
330
|
+
localStorage.setItem(sessionKey, JSON.stringify({ id: fallbackId, createdAt: Date.now() }));
|
|
331
|
+
return fallbackId;
|
|
332
|
+
})();
|
|
333
|
+
sessionPromiseRef.current.finally(() => {
|
|
334
|
+
sessionPromiseRef.current = null;
|
|
335
|
+
});
|
|
336
|
+
return sessionPromiseRef.current;
|
|
337
|
+
}, [client, config.sessionTtl, sessionKey]);
|
|
338
|
+
const track = useCallback(
|
|
339
|
+
async (eventType, data = {}) => {
|
|
340
|
+
try {
|
|
341
|
+
const sessionId = await getOrCreateSession();
|
|
342
|
+
await client.trackEvent({ session_id: sessionId, event_type: eventType, event_data: data });
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
[client, getOrCreateSession]
|
|
347
|
+
);
|
|
348
|
+
const linkCustomer = useCallback(
|
|
349
|
+
async (customerId) => {
|
|
350
|
+
try {
|
|
351
|
+
const sessionId = await getOrCreateSession();
|
|
352
|
+
if (sessionId.startsWith("local-")) return;
|
|
353
|
+
await client.updateSession(sessionId, { customer_id: customerId });
|
|
354
|
+
} catch {
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
[client, getOrCreateSession]
|
|
358
|
+
);
|
|
359
|
+
const trackPageView = useCallback(
|
|
360
|
+
(url, referrer) => {
|
|
361
|
+
track("page_view", { url, referrer });
|
|
362
|
+
},
|
|
363
|
+
[track]
|
|
364
|
+
);
|
|
365
|
+
const trackProductView = useCallback(
|
|
366
|
+
(productId, productName, category, price) => {
|
|
367
|
+
track("product_view", { product_id: productId, product_name: productName, category, price });
|
|
368
|
+
},
|
|
369
|
+
[track]
|
|
370
|
+
);
|
|
371
|
+
const trackCategoryView = useCallback(
|
|
372
|
+
(categoryId, categoryName) => {
|
|
373
|
+
track("category_view", { category_id: categoryId, category_name: categoryName });
|
|
374
|
+
},
|
|
375
|
+
[track]
|
|
376
|
+
);
|
|
377
|
+
const trackSearch = useCallback(
|
|
378
|
+
(query, resultCount) => {
|
|
379
|
+
track("search", { query, result_count: resultCount });
|
|
380
|
+
},
|
|
381
|
+
[track]
|
|
382
|
+
);
|
|
383
|
+
const trackBeginCheckout = useCallback(
|
|
384
|
+
(cartId, total, itemCount) => {
|
|
385
|
+
track("begin_checkout", { cart_id: cartId, total, item_count: itemCount });
|
|
386
|
+
},
|
|
387
|
+
[track]
|
|
388
|
+
);
|
|
389
|
+
const trackPurchase = useCallback(
|
|
390
|
+
(orderId, orderNumber, total) => {
|
|
391
|
+
track("purchase", { order_id: orderId, order_number: orderNumber, total });
|
|
392
|
+
},
|
|
393
|
+
[track]
|
|
394
|
+
);
|
|
395
|
+
const trackAddToCart = useCallback(
|
|
396
|
+
(productId, productName, quantity, price, tier) => {
|
|
397
|
+
track("add_to_cart", { product_id: productId, product_name: productName, quantity, price, tier });
|
|
398
|
+
},
|
|
399
|
+
[track]
|
|
400
|
+
);
|
|
401
|
+
const trackRemoveFromCart = useCallback(
|
|
402
|
+
(productId, productName) => {
|
|
403
|
+
track("remove_from_cart", { product_id: productId, product_name: productName });
|
|
404
|
+
},
|
|
405
|
+
[track]
|
|
406
|
+
);
|
|
407
|
+
return {
|
|
408
|
+
track,
|
|
409
|
+
trackPageView,
|
|
410
|
+
trackProductView,
|
|
411
|
+
trackCategoryView,
|
|
412
|
+
trackSearch,
|
|
413
|
+
trackBeginCheckout,
|
|
414
|
+
trackPurchase,
|
|
415
|
+
trackAddToCart,
|
|
416
|
+
trackRemoveFromCart,
|
|
417
|
+
linkCustomer,
|
|
418
|
+
getOrCreateSession
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function useAuth() {
|
|
422
|
+
const ctx = useContext(WhaleContext);
|
|
423
|
+
if (!ctx) throw new Error("useAuth must be used within <WhaleProvider>");
|
|
424
|
+
return useStore(ctx.authStore, (s) => ({
|
|
425
|
+
customer: s.customer,
|
|
426
|
+
authLoading: s.authLoading,
|
|
427
|
+
sessionToken: s.sessionToken,
|
|
428
|
+
isAuthenticated: s.isSessionValid(),
|
|
429
|
+
sendCode: s.sendOTP,
|
|
430
|
+
verifyCode: s.verifyOTP,
|
|
431
|
+
restoreSession: s.restoreSession,
|
|
432
|
+
logout: s.logout,
|
|
433
|
+
fetchCustomer: s.fetchCustomer
|
|
434
|
+
}));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/react/components/analytics-tracker.tsx
|
|
438
|
+
function AnalyticsTracker({ pathname }) {
|
|
439
|
+
const { trackPageView, linkCustomer } = useAnalytics();
|
|
440
|
+
const { customer } = useAuth();
|
|
441
|
+
const prevPathname = useRef(null);
|
|
442
|
+
const linkedCustomerId = useRef(null);
|
|
443
|
+
useEffect(() => {
|
|
444
|
+
if (pathname === prevPathname.current) return;
|
|
445
|
+
const referrer = prevPathname.current || (typeof document !== "undefined" ? document.referrer : "");
|
|
446
|
+
prevPathname.current = pathname;
|
|
447
|
+
trackPageView(pathname, referrer || void 0);
|
|
448
|
+
}, [pathname, trackPageView]);
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
if (customer?.id && customer.id !== linkedCustomerId.current) {
|
|
451
|
+
linkedCustomerId.current = customer.id;
|
|
452
|
+
linkCustomer(customer.id);
|
|
453
|
+
}
|
|
454
|
+
}, [customer?.id, linkCustomer]);
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
function useCart() {
|
|
458
|
+
const ctx = useContext(WhaleContext);
|
|
459
|
+
if (!ctx) throw new Error("useCart must be used within <WhaleProvider>");
|
|
460
|
+
return useStore(ctx.cartStore, (s) => ({
|
|
461
|
+
cartId: s.cartId,
|
|
462
|
+
items: s.items,
|
|
463
|
+
itemCount: s.itemCount,
|
|
464
|
+
subtotal: s.subtotal,
|
|
465
|
+
taxAmount: s.taxAmount,
|
|
466
|
+
total: s.total,
|
|
467
|
+
taxBreakdown: s.taxBreakdown,
|
|
468
|
+
cartOpen: s.cartOpen,
|
|
469
|
+
cartLoading: s.cartLoading,
|
|
470
|
+
productImages: s.productImages,
|
|
471
|
+
addItem: s.addItem,
|
|
472
|
+
removeItem: s.removeItem,
|
|
473
|
+
updateQuantity: s.updateQuantity,
|
|
474
|
+
toggleCart: s.toggleCart,
|
|
475
|
+
openCart: s.openCart,
|
|
476
|
+
closeCart: s.closeCart,
|
|
477
|
+
initCart: s.initCart,
|
|
478
|
+
syncCart: s.syncCart,
|
|
479
|
+
clearCart: s.clearCart,
|
|
480
|
+
checkout: s.checkout
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
483
|
+
function useCartItemCount() {
|
|
484
|
+
const ctx = useContext(WhaleContext);
|
|
485
|
+
if (!ctx) throw new Error("useCartItemCount must be used within <WhaleProvider>");
|
|
486
|
+
return useStore(ctx.cartStore, (s) => s.itemCount);
|
|
487
|
+
}
|
|
488
|
+
function useCartTotal() {
|
|
489
|
+
const ctx = useContext(WhaleContext);
|
|
490
|
+
if (!ctx) throw new Error("useCartTotal must be used within <WhaleProvider>");
|
|
491
|
+
return useStore(ctx.cartStore, (s) => s.total);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/react/components/cart-initializer.tsx
|
|
495
|
+
function CartInitializer() {
|
|
496
|
+
const { cartId, syncCart } = useCart();
|
|
497
|
+
const initialized = useRef(false);
|
|
498
|
+
useEffect(() => {
|
|
499
|
+
if (initialized.current) return;
|
|
500
|
+
initialized.current = true;
|
|
501
|
+
if (cartId) {
|
|
502
|
+
syncCart().catch(() => {
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}, []);
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
function AuthInitializer() {
|
|
509
|
+
const { restoreSession } = useAuth();
|
|
510
|
+
const restored = useRef(false);
|
|
511
|
+
useEffect(() => {
|
|
512
|
+
if (restored.current) return;
|
|
513
|
+
restored.current = true;
|
|
514
|
+
restoreSession();
|
|
515
|
+
}, []);
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
function WhaleProvider({
|
|
519
|
+
children,
|
|
520
|
+
products = [],
|
|
521
|
+
storeId,
|
|
522
|
+
apiKey,
|
|
523
|
+
gatewayUrl,
|
|
524
|
+
proxyPath,
|
|
525
|
+
mediaSigningSecret,
|
|
526
|
+
supabaseHost,
|
|
527
|
+
storagePrefix,
|
|
528
|
+
sessionTtl,
|
|
529
|
+
debug
|
|
530
|
+
}) {
|
|
531
|
+
const pathname = usePathname();
|
|
532
|
+
const ctx = useMemo(() => {
|
|
533
|
+
const resolvedConfig = {
|
|
534
|
+
storeId,
|
|
535
|
+
apiKey,
|
|
536
|
+
gatewayUrl: gatewayUrl || "https://whale-gateway.fly.dev",
|
|
537
|
+
proxyPath: proxyPath || "/api/gw",
|
|
538
|
+
mediaSigningSecret: mediaSigningSecret || "",
|
|
539
|
+
supabaseHost: supabaseHost || "",
|
|
540
|
+
storagePrefix: storagePrefix || "whale",
|
|
541
|
+
sessionTtl: sessionTtl || 30 * 60 * 1e3,
|
|
542
|
+
debug: debug || false
|
|
543
|
+
};
|
|
544
|
+
const client = new WhaleClient({
|
|
545
|
+
storeId,
|
|
546
|
+
apiKey,
|
|
547
|
+
gatewayUrl: resolvedConfig.gatewayUrl,
|
|
548
|
+
proxyPath: resolvedConfig.proxyPath
|
|
549
|
+
});
|
|
550
|
+
const cartStore = createCartStore(client, resolvedConfig.storagePrefix);
|
|
551
|
+
const authStore = createAuthStore(client, resolvedConfig.storagePrefix);
|
|
552
|
+
return {
|
|
553
|
+
client,
|
|
554
|
+
config: resolvedConfig,
|
|
555
|
+
cartStore,
|
|
556
|
+
authStore,
|
|
557
|
+
products
|
|
558
|
+
};
|
|
559
|
+
}, [storeId, apiKey]);
|
|
560
|
+
const value = useMemo(
|
|
561
|
+
() => ({ ...ctx, products }),
|
|
562
|
+
[ctx, products]
|
|
563
|
+
);
|
|
564
|
+
return /* @__PURE__ */ jsxs(WhaleContext.Provider, { value, children: [
|
|
565
|
+
/* @__PURE__ */ jsx(AuthInitializer, {}),
|
|
566
|
+
/* @__PURE__ */ jsx(CartInitializer, {}),
|
|
567
|
+
/* @__PURE__ */ jsx(AnalyticsTracker, { pathname }),
|
|
568
|
+
children
|
|
569
|
+
] });
|
|
570
|
+
}
|
|
571
|
+
function useProducts(opts) {
|
|
572
|
+
const ctx = useContext(WhaleContext);
|
|
573
|
+
if (!ctx) throw new Error("useProducts must be used within <WhaleProvider>");
|
|
574
|
+
const allProducts = ctx.products;
|
|
575
|
+
const products = useMemo(() => {
|
|
576
|
+
let result = allProducts;
|
|
577
|
+
if (opts?.categoryId) {
|
|
578
|
+
result = result.filter((p) => p.primary_category_id === opts.categoryId);
|
|
579
|
+
}
|
|
580
|
+
if (opts?.search) {
|
|
581
|
+
const q = opts.search.toLowerCase();
|
|
582
|
+
result = result.filter(
|
|
583
|
+
(p) => p.name.toLowerCase().includes(q) || p.description?.toLowerCase().includes(q) || p.slug.toLowerCase().includes(q)
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
return result;
|
|
587
|
+
}, [allProducts, opts?.categoryId, opts?.search]);
|
|
588
|
+
return {
|
|
589
|
+
products,
|
|
590
|
+
allProducts,
|
|
591
|
+
loading: false
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function useProduct(slug) {
|
|
595
|
+
const ctx = useContext(WhaleContext);
|
|
596
|
+
if (!ctx) throw new Error("useProduct must be used within <WhaleProvider>");
|
|
597
|
+
const product = useMemo(() => {
|
|
598
|
+
if (!slug) return null;
|
|
599
|
+
return ctx.products.find((p) => p.slug === slug) ?? null;
|
|
600
|
+
}, [ctx.products, slug]);
|
|
601
|
+
return {
|
|
602
|
+
product,
|
|
603
|
+
loading: false
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function useWhaleClient() {
|
|
607
|
+
const ctx = useContext(WhaleContext);
|
|
608
|
+
if (!ctx) throw new Error("useWhaleClient must be used within <WhaleProvider>");
|
|
609
|
+
return ctx.client;
|
|
610
|
+
}
|
|
611
|
+
function useCustomerOrders() {
|
|
612
|
+
const ctx = useContext(WhaleContext);
|
|
613
|
+
if (!ctx) throw new Error("useCustomerOrders must be used within <WhaleProvider>");
|
|
614
|
+
const customer = useStore(ctx.authStore, (s) => s.customer);
|
|
615
|
+
const [orders, setOrders] = useState([]);
|
|
616
|
+
const [loading, setLoading] = useState(false);
|
|
617
|
+
const refresh = useCallback(async () => {
|
|
618
|
+
if (!customer?.id) {
|
|
619
|
+
setOrders([]);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
setLoading(true);
|
|
623
|
+
try {
|
|
624
|
+
const data = await ctx.client.getCustomerOrders(customer.id);
|
|
625
|
+
setOrders(data);
|
|
626
|
+
} catch {
|
|
627
|
+
setOrders([]);
|
|
628
|
+
} finally {
|
|
629
|
+
setLoading(false);
|
|
630
|
+
}
|
|
631
|
+
}, [customer?.id, ctx.client]);
|
|
632
|
+
useEffect(() => {
|
|
633
|
+
refresh();
|
|
634
|
+
}, [refresh]);
|
|
635
|
+
return { orders, loading, refresh };
|
|
636
|
+
}
|
|
637
|
+
function useCustomerAnalytics() {
|
|
638
|
+
const ctx = useContext(WhaleContext);
|
|
639
|
+
if (!ctx) throw new Error("useCustomerAnalytics must be used within <WhaleProvider>");
|
|
640
|
+
const customer = useStore(ctx.authStore, (s) => s.customer);
|
|
641
|
+
const [analytics, setAnalytics] = useState(null);
|
|
642
|
+
const [loading, setLoading] = useState(false);
|
|
643
|
+
useEffect(() => {
|
|
644
|
+
if (!customer?.id) {
|
|
645
|
+
setAnalytics(null);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
setLoading(true);
|
|
649
|
+
const name = `${customer.first_name} ${customer.last_name}`.trim();
|
|
650
|
+
ctx.client.getCustomerAnalytics(customer.id, name || void 0).then(setAnalytics).catch(() => setAnalytics(null)).finally(() => setLoading(false));
|
|
651
|
+
}, [customer?.id, customer?.first_name, customer?.last_name, ctx.client]);
|
|
652
|
+
return { analytics, loading };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export { AnalyticsTracker, AuthInitializer, CartInitializer, WhaleContext, WhaleProvider, useAnalytics, useAuth, useCart, useCartItemCount, useCartTotal, useCustomerAnalytics, useCustomerOrders, useProduct, useProducts, useWhaleClient };
|
|
656
|
+
//# sourceMappingURL=index.js.map
|
|
657
|
+
//# sourceMappingURL=index.js.map
|