@fruga/sdk 1.0.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/index.mjs ADDED
@@ -0,0 +1,725 @@
1
+ // packages/core/src/domain/entities/Theme.ts
2
+ var DEFAULT_THEME = {
3
+ widgetTitle: "Savings Box",
4
+ launcherLabel: "Insurance Savings",
5
+ launcherIcon: "arrow",
6
+ showLauncher: true,
7
+ mode: "light",
8
+ allowUserThemeControl: false,
9
+ defaultTheme: "light",
10
+ uiVersion: "current",
11
+ primary: "#1E40AF",
12
+ accent: "#2648C9",
13
+ accentTextLight: "#FFFFFF",
14
+ accentTextDark: "#FFFFFF",
15
+ bg: "#0B1220",
16
+ panel: "#0F1A2E",
17
+ panel2: "#111F38",
18
+ itemBg: "rgba(255,255,255,0.03)",
19
+ text: "#E6EDF7",
20
+ muted: "#A7B3C6",
21
+ border: "rgba(255,255,255,0.08)",
22
+ success: "#34D399",
23
+ danger: "#EF4444",
24
+ warning: "#FBBF24",
25
+ lightBg: "#F8FAFC",
26
+ lightPanel: "#FFFFFF",
27
+ lightPanel2: "#F1F5F9",
28
+ lightItemBg: "rgba(0,0,0,0.04)",
29
+ lightText: "#0F172A",
30
+ lightMuted: "#64748B",
31
+ lightBorder: "rgba(0,0,0,0.1)"
32
+ };
33
+ function getResolvedTheme(theme) {
34
+ const isLight = theme.mode === "light";
35
+ const resolvedText = isLight ? theme.textColorLight?.trim() || theme.lightText : theme.textColorDark?.trim() || theme.text;
36
+ return {
37
+ ...theme,
38
+ bg: isLight ? theme.lightBg : theme.bg,
39
+ panel: isLight ? theme.lightPanel : theme.panel,
40
+ panel2: isLight ? theme.lightPanel2 : theme.panel2,
41
+ itemBg: isLight ? theme.lightItemBg : theme.itemBg,
42
+ text: resolvedText,
43
+ muted: isLight ? theme.lightMuted : theme.muted,
44
+ border: isLight ? theme.lightBorder : theme.border,
45
+ subtleBg: isLight ? "rgba(0,0,0,0.02)" : "rgba(255,255,255,0.03)",
46
+ tooltipBg: isLight ? "#FFFFFF" : "#0B1220",
47
+ tooltipBorder: isLight ? "rgba(0,0,0,0.12)" : "rgba(255,255,255,0.12)",
48
+ accentText: isLight ? theme.accentTextLight?.trim() || "#0F172A" : theme.accentTextDark?.trim() || "#FFFFFF",
49
+ isLight,
50
+ mode: theme.mode
51
+ };
52
+ }
53
+
54
+ // packages/core/src/infrastructure/adapters/HttpWalletService.ts
55
+ var HttpWalletService = class {
56
+ constructor(baseUrl, token) {
57
+ this.baseUrl = baseUrl;
58
+ this.token = token;
59
+ }
60
+ async getWallet() {
61
+ try {
62
+ const response = await fetch(`${this.baseUrl}/api/wallet`, {
63
+ method: "GET",
64
+ headers: {
65
+ "Accept": "application/json",
66
+ "Authorization": `Bearer ${this.token}`
67
+ }
68
+ });
69
+ if (!response.ok) {
70
+ console.warn(`[FrugaSDK] Failed to fetch wallet: ${response.status}. Returning zeroed wallet.`);
71
+ return this.getEmptyWallet();
72
+ }
73
+ const data = await response.json();
74
+ return {
75
+ available: data.available ?? 0,
76
+ pending: data.pending ?? 0,
77
+ totalBalance: (data.available || 0) + (data.pending || 0),
78
+ lifetimeEarned: data.lifetimeEarned ?? 0,
79
+ lifetimeRedeemed: data.lifetimeRedeemed ?? 0,
80
+ updatedAt: data.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
81
+ requiredActions: data.requiredActions ?? null
82
+ };
83
+ } catch (error) {
84
+ console.warn(`[FrugaSDK] Network error fetching wallet. Returning zeroed wallet.`, error);
85
+ return this.getEmptyWallet();
86
+ }
87
+ }
88
+ getEmptyWallet() {
89
+ return {
90
+ available: 0,
91
+ pending: 0,
92
+ totalBalance: 0,
93
+ lifetimeEarned: 0,
94
+ lifetimeRedeemed: 0,
95
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
96
+ requiredActions: null
97
+ };
98
+ }
99
+ };
100
+
101
+ // packages/core/src/infrastructure/adapters/BaseHttpService.ts
102
+ var BaseHttpService = class {
103
+ constructor(baseUrl, token) {
104
+ this.baseUrl = baseUrl;
105
+ this.token = token;
106
+ }
107
+ async fetchQuery(path, queryParams = {}) {
108
+ const url = new URL(`${this.baseUrl}${path}`);
109
+ Object.entries(queryParams).forEach(([key, value]) => {
110
+ if (value !== void 0 && value !== null) {
111
+ if (Array.isArray(value)) {
112
+ value.forEach((item) => url.searchParams.append(key, String(item)));
113
+ } else {
114
+ url.searchParams.append(key, String(value));
115
+ }
116
+ }
117
+ });
118
+ return fetch(url.toString(), {
119
+ method: "GET",
120
+ headers: {
121
+ "Accept": "application/json",
122
+ "Authorization": `Bearer ${this.token}`
123
+ }
124
+ });
125
+ }
126
+ };
127
+
128
+ // packages/core/src/infrastructure/adapters/HttpActivityService.ts
129
+ var HttpActivityService = class extends BaseHttpService {
130
+ constructor(baseUrl, token) {
131
+ super(baseUrl, token);
132
+ }
133
+ async getActivity(pageNumber, pageSize) {
134
+ try {
135
+ const response = await this.fetchQuery("/api/activity", { pageNumber, pageSize });
136
+ if (!response.ok) {
137
+ console.warn(`[FrugaSDK] Failed to fetch activity: ${response.status}. Returning empty activity.`);
138
+ return this.getEmptyActivity();
139
+ }
140
+ const data = await response.json();
141
+ return {
142
+ ...data,
143
+ transactions: data.transactions || []
144
+ };
145
+ } catch (error) {
146
+ console.warn(`[FrugaSDK] Network error fetching activity. Returning empty activity.`, error);
147
+ return this.getEmptyActivity();
148
+ }
149
+ }
150
+ getEmptyActivity() {
151
+ return {
152
+ actorType: "",
153
+ partnerId: 0,
154
+ transactions: []
155
+ };
156
+ }
157
+ };
158
+
159
+ // packages/core/src/infrastructure/adapters/HttpOffersService.ts
160
+ var HttpOffersService = class extends BaseHttpService {
161
+ constructor(baseUrl, token) {
162
+ super(baseUrl, token);
163
+ }
164
+ async getCategories() {
165
+ try {
166
+ const response = await this.fetchQuery("/api/offers/categories");
167
+ if (!response.ok) {
168
+ console.warn(`[FrugaSDK] Failed to fetch offer categories: ${response.status}. Returning empty list.`);
169
+ return [];
170
+ }
171
+ const data = await response.json();
172
+ const categories = data.categories || [];
173
+ return categories;
174
+ } catch (error) {
175
+ console.warn(`[FrugaSDK] Network error fetching offer categories. Returning empty list.`, error);
176
+ return [];
177
+ }
178
+ }
179
+ async getOffers(query) {
180
+ try {
181
+ const response = await this.fetchQuery("/api/offers", {
182
+ pageNumber: query.pageNumber ?? 0,
183
+ pageSize: query.pageSize ?? 10,
184
+ type: query.type,
185
+ query: query.query,
186
+ featured: query.featured,
187
+ categories: query.categories
188
+ });
189
+ if (!response.ok) {
190
+ console.warn(`[FrugaSDK] Failed to fetch offers: ${response.status}. Returning empty response.`);
191
+ return this.getEmptyPaginatedResponse();
192
+ }
193
+ const data = await response.json();
194
+ return {
195
+ ...data,
196
+ content: data.content || []
197
+ };
198
+ } catch (error) {
199
+ console.warn(`[FrugaSDK] Network error fetching offers. Returning empty response.`, error);
200
+ return this.getEmptyPaginatedResponse();
201
+ }
202
+ }
203
+ getEmptyPaginatedResponse() {
204
+ return {
205
+ content: [],
206
+ totalElements: 0,
207
+ totalPages: 0,
208
+ number: 0,
209
+ size: 0,
210
+ numberOfElements: 0,
211
+ numberOfPages: 0
212
+ };
213
+ }
214
+ };
215
+
216
+ // packages/core/src/infrastructure/adapters/HttpPayoutService.ts
217
+ var HttpPayoutService = class {
218
+ constructor(baseUrl, token) {
219
+ this.baseUrl = baseUrl;
220
+ this.token = token;
221
+ }
222
+ async listMethods() {
223
+ try {
224
+ const response = await fetch(`${this.baseUrl}/api/payout-methods`, {
225
+ method: "GET",
226
+ headers: {
227
+ "Accept": "application/json",
228
+ "Authorization": `Bearer ${this.token}`
229
+ }
230
+ });
231
+ if (!response.ok) {
232
+ throw new Error(`Failed to list payout methods: ${response.status}`);
233
+ }
234
+ const data = await response.json();
235
+ return data.items || [];
236
+ } catch (error) {
237
+ console.error("[FrugaSDK] Error listing payout methods:", error);
238
+ return [];
239
+ }
240
+ }
241
+ async addBankMethod(payload) {
242
+ const response = await fetch(`${this.baseUrl}/api/payout-methods/bank`, {
243
+ method: "POST",
244
+ headers: {
245
+ "Accept": "application/json",
246
+ "Content-Type": "application/json",
247
+ "Authorization": `Bearer ${this.token}`
248
+ },
249
+ body: JSON.stringify(payload)
250
+ });
251
+ if (!response.ok) {
252
+ throw new Error(`Failed to add bank payout method: ${response.status}`);
253
+ }
254
+ return response.json();
255
+ }
256
+ async deleteMethod(id) {
257
+ const response = await fetch(`${this.baseUrl}/api/payout-methods/${id}`, {
258
+ method: "DELETE",
259
+ headers: {
260
+ "Accept": "application/json",
261
+ "Authorization": `Bearer ${this.token}`
262
+ }
263
+ });
264
+ if (!response.ok) {
265
+ throw new Error(`Failed to delete payout method: ${response.status}`);
266
+ }
267
+ }
268
+ };
269
+
270
+ // packages/core/src/presentation/components/index.tsx
271
+ import { jsx } from "react/jsx-runtime";
272
+ var WidgetButton = ({
273
+ children,
274
+ variant = "primary",
275
+ style,
276
+ ...props
277
+ }) => {
278
+ const baseStyle = {
279
+ padding: "10px 20px",
280
+ borderRadius: "8px",
281
+ border: "none",
282
+ cursor: "pointer",
283
+ fontWeight: 600,
284
+ transition: "opacity 0.2s",
285
+ backgroundColor: variant === "primary" ? "#007bff" : "#6c757d",
286
+ color: "white",
287
+ ...style
288
+ };
289
+ return /* @__PURE__ */ jsx("button", { style: baseStyle, ...props, children });
290
+ };
291
+ var WidgetHeader = ({ title }) => /* @__PURE__ */ jsx("header", { style: {
292
+ padding: "16px",
293
+ borderBottom: "1px solid #eee",
294
+ display: "flex",
295
+ alignItems: "center",
296
+ justifyContent: "space-between"
297
+ }, children: /* @__PURE__ */ jsx("h3", { style: { margin: 0, fontSize: "1.1rem" }, children: title }) });
298
+
299
+ // packages/core/src/presentation/components/WidgetContainer.tsx
300
+ import { useState, useEffect as useEffect2, useRef as useRef2 } from "react";
301
+
302
+ // packages/core/src/presentation/components/ui/LauncherIcon.tsx
303
+ import { Sparkles, CreditCard, MoveUpRight, Gift } from "lucide-react";
304
+ import { jsx as jsx2 } from "react/jsx-runtime";
305
+ function LauncherIcon({ theme }) {
306
+ if (theme.launcherIcon === "none") return null;
307
+ if (theme.launcherIcon === "sparkles") return /* @__PURE__ */ jsx2(Sparkles, { size: 20 });
308
+ if (theme.launcherIcon === "credit") return /* @__PURE__ */ jsx2(CreditCard, { size: 20 });
309
+ if (theme.launcherIcon === "arrow") return /* @__PURE__ */ jsx2(MoveUpRight, { size: 20 });
310
+ return /* @__PURE__ */ jsx2(Gift, { size: 20 });
311
+ }
312
+
313
+ // packages/core/src/presentation/hooks/useActivityDetector.ts
314
+ import { useEffect, useRef } from "react";
315
+
316
+ // node_modules/.pnpm/zustand@5.0.12_@types+react@19.2.14_react@19.2.4/node_modules/zustand/esm/vanilla.mjs
317
+ var createStoreImpl = (createState) => {
318
+ let state;
319
+ const listeners = /* @__PURE__ */ new Set();
320
+ const setState = (partial, replace) => {
321
+ const nextState = typeof partial === "function" ? partial(state) : partial;
322
+ if (!Object.is(nextState, state)) {
323
+ const previousState = state;
324
+ state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
325
+ listeners.forEach((listener) => listener(state, previousState));
326
+ }
327
+ };
328
+ const getState = () => state;
329
+ const getInitialState = () => initialState;
330
+ const subscribe = (listener) => {
331
+ listeners.add(listener);
332
+ return () => listeners.delete(listener);
333
+ };
334
+ const api = { setState, getState, getInitialState, subscribe };
335
+ const initialState = state = createState(setState, getState, api);
336
+ return api;
337
+ };
338
+ var createStore = ((createState) => createState ? createStoreImpl(createState) : createStoreImpl);
339
+
340
+ // node_modules/.pnpm/zustand@5.0.12_@types+react@19.2.14_react@19.2.4/node_modules/zustand/esm/react.mjs
341
+ import React from "react";
342
+ var identity = (arg) => arg;
343
+ function useStore(api, selector = identity) {
344
+ const slice = React.useSyncExternalStore(
345
+ api.subscribe,
346
+ React.useCallback(() => selector(api.getState()), [api, selector]),
347
+ React.useCallback(() => selector(api.getInitialState()), [api, selector])
348
+ );
349
+ React.useDebugValue(slice);
350
+ return slice;
351
+ }
352
+ var createImpl = (createState) => {
353
+ const api = createStore(createState);
354
+ const useBoundStore = (selector) => useStore(api, selector);
355
+ Object.assign(useBoundStore, api);
356
+ return useBoundStore;
357
+ };
358
+ var create = ((createState) => createState ? createImpl(createState) : createImpl);
359
+
360
+ // packages/core/src/presentation/store/useWidgetStore.ts
361
+ var useWidgetStore = create((set) => ({
362
+ isOpen: false,
363
+ isFocused: true,
364
+ // Default to true as the widget starts in some state
365
+ isIdle: false,
366
+ available: 0,
367
+ pending: 0,
368
+ mode: "light",
369
+ token: null,
370
+ partnerPayout: null,
371
+ isAddingPayoutFromAlert: false,
372
+ showPayoutSuccess: false,
373
+ toggle: () => set((state) => ({ isOpen: !state.isOpen })),
374
+ setOpen: (open) => set({ isOpen: open }),
375
+ setFocused: (focused) => set({ isFocused: focused }),
376
+ setIdle: (idle) => set({ isIdle: idle }),
377
+ setBalances: (available, pending) => set({ available, pending }),
378
+ setMode: (mode) => set({ mode }),
379
+ setToken: (token) => set({ token }),
380
+ setPartnerPayout: (payout) => set({ partnerPayout: payout }),
381
+ setIsAddingPayoutFromAlert: (val) => set({ isAddingPayoutFromAlert: val }),
382
+ setShowPayoutSuccess: (val) => set({ showPayoutSuccess: val })
383
+ }));
384
+
385
+ // packages/core/src/presentation/constants/WidgetConstants.ts
386
+ var IDLE_TIMEOUT_MS = 60 * 1e3;
387
+
388
+ // packages/core/src/presentation/hooks/useActivityDetector.ts
389
+ function useActivityDetector() {
390
+ const { setFocused, setIdle } = useWidgetStore();
391
+ const idleTimerRef = useRef(null);
392
+ useEffect(() => {
393
+ const handleFocus = () => setFocused(true);
394
+ const handleBlur = () => setFocused(false);
395
+ const resetIdleTimer = () => {
396
+ setIdle(false);
397
+ if (idleTimerRef.current) {
398
+ clearTimeout(idleTimerRef.current);
399
+ }
400
+ idleTimerRef.current = setTimeout(() => {
401
+ setIdle(true);
402
+ }, IDLE_TIMEOUT_MS);
403
+ };
404
+ const handleInteraction = () => {
405
+ resetIdleTimer();
406
+ };
407
+ resetIdleTimer();
408
+ setFocused(document.hasFocus());
409
+ window.addEventListener("focus", handleFocus);
410
+ window.addEventListener("blur", handleBlur);
411
+ const activityEvents = ["mousemove", "keydown", "scroll", "touchstart", "mousedown"];
412
+ activityEvents.forEach((event) => {
413
+ window.addEventListener(event, handleInteraction, event === "scroll" ? true : void 0);
414
+ });
415
+ return () => {
416
+ window.removeEventListener("focus", handleFocus);
417
+ window.removeEventListener("blur", handleBlur);
418
+ activityEvents.forEach((event) => {
419
+ window.removeEventListener(event, handleInteraction, event === "scroll" ? true : void 0);
420
+ });
421
+ if (idleTimerRef.current) {
422
+ clearTimeout(idleTimerRef.current);
423
+ }
424
+ };
425
+ }, [setFocused, setIdle]);
426
+ }
427
+
428
+ // packages/core/src/presentation/components/WidgetContainer.tsx
429
+ import { X } from "lucide-react";
430
+ import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
431
+ var WidgetContainer = ({
432
+ children,
433
+ theme: themeProp,
434
+ hostHeight: initialHostHeight = 800,
435
+ isMobile: initialIsMobile = false
436
+ }) => {
437
+ useActivityDetector();
438
+ const { isOpen, toggle, mode: storeMode } = useWidgetStore();
439
+ const theme = themeProp || DEFAULT_THEME;
440
+ const [hostHeight, setHostHeight] = useState(initialHostHeight);
441
+ const [isMobile, setIsMobile] = useState(initialIsMobile);
442
+ const effectiveTheme = theme.allowUserThemeControl ? { ...theme, mode: storeMode } : { ...theme, mode: theme.mode || "light" };
443
+ const resolved = getResolvedTheme(effectiveTheme);
444
+ const containerRef = useRef2(null);
445
+ const buttonRef = useRef2(null);
446
+ useEffect2(() => {
447
+ const handler = (event) => {
448
+ if (event.data?.type === "FRUGA_HOST_RESIZE") {
449
+ setHostHeight(event.data.hostHeight);
450
+ setIsMobile(event.data.isHostMobile);
451
+ }
452
+ if (event.data?.type === "FRUGA_TOKEN_UPDATE") {
453
+ const { setToken } = useWidgetStore.getState();
454
+ setToken(event.data.token);
455
+ }
456
+ if (event.data?.type === "FRUGA_GET_BALANCE") {
457
+ const { available, pending } = useWidgetStore.getState();
458
+ console.log("FRUGA_GET_BALANCE", available, pending);
459
+ window.parent.postMessage({
460
+ type: "FRUGA_BALANCE_RESPONSE",
461
+ requestId: event.data.requestId,
462
+ available,
463
+ pending
464
+ }, "*");
465
+ }
466
+ };
467
+ window.addEventListener("message", handler);
468
+ return () => window.removeEventListener("message", handler);
469
+ }, []);
470
+ useEffect2(() => {
471
+ if (!containerRef.current) return;
472
+ const sendResize = () => {
473
+ if (!containerRef.current) return;
474
+ let expectedWidth = isOpen ? 450 : 100;
475
+ if (!isOpen && buttonRef.current) {
476
+ expectedWidth = buttonRef.current.offsetWidth + 40;
477
+ }
478
+ let expectedHeight = isOpen ? Math.max(200, hostHeight - 100) : 100;
479
+ if (isMobile && isOpen) {
480
+ expectedWidth = window.innerWidth;
481
+ expectedHeight = hostHeight;
482
+ }
483
+ window.parent.postMessage({
484
+ type: "FRUGA_RESIZE",
485
+ height: expectedHeight,
486
+ width: expectedWidth,
487
+ isFullscreen: isMobile && isOpen
488
+ }, "*");
489
+ };
490
+ const observer = new ResizeObserver(() => sendResize());
491
+ observer.observe(containerRef.current);
492
+ sendResize();
493
+ return () => observer.disconnect();
494
+ }, [isOpen, hostHeight, isMobile]);
495
+ return /* @__PURE__ */ jsxs(
496
+ "div",
497
+ {
498
+ ref: containerRef,
499
+ className: resolved.mode === "dark" ? "dark" : "",
500
+ style: {
501
+ fontFamily: 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
502
+ color: resolved.text,
503
+ display: "flex",
504
+ flexDirection: "column",
505
+ alignItems: "flex-end",
506
+ justifyContent: "flex-end",
507
+ padding: isMobile && isOpen ? "0" : "20px",
508
+ boxSizing: "border-box",
509
+ height: "100%",
510
+ margin: 0,
511
+ overscrollBehavior: "contain"
512
+ },
513
+ children: [
514
+ isOpen && /* @__PURE__ */ jsx3("div", { style: {
515
+ width: isMobile ? "100vw" : "410px",
516
+ minWidth: isMobile ? "100vw" : "410px",
517
+ flexShrink: 0,
518
+ // Account for FAB (60px) + padding (20px) + gap (20px) = 100px
519
+ // height: isMobile ? '100vh' : `${Math.max(200, hostHeight - 200)}px`,
520
+ // Update the height of the widget container
521
+ maxHeight: isMobile ? "100vh" : `${Math.max(100, hostHeight - 100)}px`,
522
+ height: "100%",
523
+ marginBottom: isMobile ? "0" : "20px",
524
+ backgroundColor: resolved.accent,
525
+ borderRadius: isMobile ? "0" : "16px",
526
+ boxShadow: isMobile ? "none" : "0 20px 40px -10px rgba(0, 0, 0, 0.15), 0 0 1px rgba(0,0,0,0.1)",
527
+ display: "flex",
528
+ flexDirection: "column",
529
+ overflow: "hidden",
530
+ zIndex: 9998
531
+ }, children: /* @__PURE__ */ jsx3(
532
+ "div",
533
+ {
534
+ className: "fruga-scrollbar",
535
+ style: {
536
+ flex: 1,
537
+ minHeight: 0,
538
+ backgroundColor: resolved.isLight ? "#f9f9f9" : "#111",
539
+ display: "flex",
540
+ flexDirection: "column",
541
+ boxShadow: isMobile ? "none" : "0 20px 40px -10px rgba(0, 0, 0, 0.15), 0 0 1px rgba(0,0,0,0.1)"
542
+ },
543
+ children
544
+ }
545
+ ) }),
546
+ /* @__PURE__ */ jsxs(
547
+ "button",
548
+ {
549
+ ref: buttonRef,
550
+ onClick: toggle,
551
+ style: {
552
+ backgroundColor: resolved.accent,
553
+ color: resolved.accentText,
554
+ border: "none",
555
+ cursor: "pointer",
556
+ boxShadow: "0 12px 30px -5px rgba(0, 0, 0, 0.1), 0 0 1px rgba(0,0,0, 0.1)",
557
+ display: isOpen ? "none" : "flex",
558
+ alignItems: "center",
559
+ justifyContent: "center",
560
+ zIndex: 9999,
561
+ flexShrink: 0,
562
+ borderRadius: "20px",
563
+ padding: "10px 16px",
564
+ transition: "transform 0.3s ease-in-out"
565
+ },
566
+ className: "flex h-15 items-center gap-2 rounded-xl px-4 active:scale-[0.99]",
567
+ "aria-label": isOpen ? "Close widget" : "Open widget",
568
+ children: [
569
+ theme.launcherLabel && !isOpen ? /* @__PURE__ */ jsxs(Fragment, { children: [
570
+ theme.launcherLabel !== "none" && /* @__PURE__ */ jsx3("div", { className: "text-sm font-semibold", children: theme.launcherLabel }),
571
+ /* @__PURE__ */ jsx3("div", { className: "flex h-6 w-5 shrink-0 items-center justify-center", children: /* @__PURE__ */ jsx3(LauncherIcon, { theme: resolved }) })
572
+ ] }) : null,
573
+ isOpen ? /* @__PURE__ */ jsx3(X, {}) : null
574
+ ]
575
+ }
576
+ )
577
+ ]
578
+ }
579
+ );
580
+ };
581
+
582
+ // packages/core/src/presentation/context/WidgetContext.tsx
583
+ import { Fragment as Fragment2, jsx as jsx4 } from "react/jsx-runtime";
584
+ var WidgetProvider = ({ children }) => {
585
+ return /* @__PURE__ */ jsx4(Fragment2, { children });
586
+ };
587
+ var useWidget = () => {
588
+ const { isOpen, toggle, setOpen } = useWidgetStore();
589
+ return { isOpen, toggle, setOpen };
590
+ };
591
+
592
+ // packages/core/src/presentation/hooks/useDebounce.ts
593
+ import { useEffect as useEffect3, useState as useState2 } from "react";
594
+ var useDebounce = (value, delay) => {
595
+ const [debouncedValue, setDebouncedValue] = useState2(value);
596
+ useEffect3(() => {
597
+ const handleTimeout = setTimeout(() => {
598
+ setDebouncedValue(value);
599
+ }, delay);
600
+ return () => {
601
+ clearTimeout(handleTimeout);
602
+ };
603
+ }, [value, delay]);
604
+ return debouncedValue;
605
+ };
606
+
607
+ // packages/core/src/infrastructure/storage/StorageService.ts
608
+ var STORAGE_KEYS = {
609
+ BOOTSTRAP_DATA: "fruga_bootstrap",
610
+ BOOTSTRAP_CONFIG: "fruga_active_config"
611
+ };
612
+ var StorageService = class {
613
+ static set(key, value) {
614
+ if (typeof window === "undefined") return;
615
+ try {
616
+ window.localStorage.setItem(key, JSON.stringify(value));
617
+ } catch (error) {
618
+ console.warn(`[FrugaStorage] Failed to save key "${key}":`, error);
619
+ }
620
+ }
621
+ static get(key) {
622
+ if (typeof window === "undefined") return null;
623
+ try {
624
+ const item = window.localStorage.getItem(key);
625
+ return item ? JSON.parse(item) : null;
626
+ } catch (error) {
627
+ console.warn(`[FrugaStorage] Failed to load key "${key}":`, error);
628
+ return null;
629
+ }
630
+ }
631
+ static remove(key) {
632
+ if (typeof window === "undefined") return;
633
+ window.localStorage.removeItem(key);
634
+ }
635
+ static clear() {
636
+ if (typeof window === "undefined") return;
637
+ window.localStorage.clear();
638
+ }
639
+ };
640
+
641
+ // packages/core/src/infrastructure/config.ts
642
+ var DEFAULT_API_URL = typeof process !== "undefined" && process.env.VITE_FRUGA_API_URL || "https://api-dev.fruga.co.uk";
643
+ var getBaseUrl = (override) => {
644
+ if (override) return override;
645
+ return DEFAULT_API_URL;
646
+ };
647
+
648
+ // packages/core/src/infrastructure/helpers/date.ts
649
+ var formatDate = (dateStr) => {
650
+ if (!dateStr) return "";
651
+ try {
652
+ const normalized = dateStr.includes("/") && !dateStr.includes("-") ? dateStr.split(" ")[0].split("/").reverse().join("-") : dateStr;
653
+ const date = new Date(normalized);
654
+ if (isNaN(date.getTime())) return dateStr;
655
+ const day = date.getDate().toString().padStart(2, "0");
656
+ const month = (date.getMonth() + 1).toString().padStart(2, "0");
657
+ const year = date.getFullYear();
658
+ return `${day}-${month}-${year}`;
659
+ } catch (e) {
660
+ return dateStr;
661
+ }
662
+ };
663
+ var normalizeDate = (dateStr) => {
664
+ try {
665
+ const normalized = dateStr.includes("/") && !dateStr.includes("-") ? dateStr.split(" ")[0].split("/").reverse().join("-") : dateStr;
666
+ const date = new Date(normalized);
667
+ return isNaN(date.getTime()) ? null : date;
668
+ } catch {
669
+ return null;
670
+ }
671
+ };
672
+ var isWithinDays = (dateStr, daysLimit = 30) => {
673
+ if (!dateStr) return false;
674
+ const date = normalizeDate(dateStr);
675
+ if (!date) return false;
676
+ const now = /* @__PURE__ */ new Date();
677
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
678
+ const targetDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
679
+ const diffTime = targetDate.getTime() - today.getTime();
680
+ const diffDays = Math.ceil(diffTime / (1e3 * 60 * 60 * 24));
681
+ return diffDays >= 0 && diffDays <= daysLimit;
682
+ };
683
+ var getDaysRemaining = (dateStr) => {
684
+ if (!dateStr) return null;
685
+ const date = normalizeDate(dateStr);
686
+ if (!date) return null;
687
+ const now = /* @__PURE__ */ new Date();
688
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
689
+ const targetDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
690
+ const diffTime = targetDate.getTime() - today.getTime();
691
+ const diffDays = Math.ceil(diffTime / (1e3 * 60 * 60 * 24));
692
+ return diffDays >= 0 ? diffDays : 0;
693
+ };
694
+
695
+ // packages/core/src/infrastructure/helpers/string.ts
696
+ var truncateText = (str, maxLength = 40) => {
697
+ if (!str) return "";
698
+ if (str.length <= maxLength) return str;
699
+ return str.slice(0, maxLength) + "...";
700
+ };
701
+ export {
702
+ DEFAULT_API_URL,
703
+ DEFAULT_THEME,
704
+ DEFAULT_THEME as DefaultTheme,
705
+ HttpActivityService,
706
+ HttpOffersService,
707
+ HttpPayoutService,
708
+ HttpWalletService,
709
+ STORAGE_KEYS,
710
+ StorageService,
711
+ WidgetButton,
712
+ WidgetContainer,
713
+ WidgetHeader,
714
+ WidgetProvider,
715
+ formatDate,
716
+ getBaseUrl,
717
+ getDaysRemaining,
718
+ getResolvedTheme,
719
+ isWithinDays,
720
+ truncateText,
721
+ useDebounce,
722
+ useWidget,
723
+ useWidgetStore
724
+ };
725
+ //# sourceMappingURL=index.mjs.map