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