@fruga/sdk 1.0.4 → 1.3.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 CHANGED
@@ -5,6 +5,7 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
9
  var __export = (target, all) => {
9
10
  for (var name in all)
10
11
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -26,6 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
27
  mod
27
28
  ));
28
29
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
29
31
 
30
32
  // packages/core/src/index.ts
31
33
  var src_exports = {};
@@ -33,23 +35,38 @@ __export(src_exports, {
33
35
  DEFAULT_API_URL: () => DEFAULT_API_URL,
34
36
  DEFAULT_THEME: () => DEFAULT_THEME,
35
37
  DefaultTheme: () => DEFAULT_THEME,
38
+ HOST_MOBILE_BREAKPOINT: () => HOST_MOBILE_BREAKPOINT,
36
39
  HttpActivityService: () => HttpActivityService,
40
+ HttpError: () => HttpError,
37
41
  HttpOffersService: () => HttpOffersService,
38
42
  HttpPayoutService: () => HttpPayoutService,
39
43
  HttpWalletService: () => HttpWalletService,
40
- STORAGE_KEYS: () => STORAGE_KEYS,
44
+ LEGACY_BOOTSTRAP_STORAGE_KEY: () => LEGACY_BOOTSTRAP_STORAGE_KEY,
45
+ MessageTransportContext: () => MessageTransportContext,
46
+ MessageTransportProvider: () => MessageTransportProvider,
47
+ PROTOCOL_VERSION: () => PROTOCOL_VERSION,
48
+ PostMessageTransport: () => PostMessageTransport,
49
+ SDK_VERSION: () => SDK_VERSION,
41
50
  StorageService: () => StorageService,
42
- WidgetButton: () => WidgetButton,
51
+ UnauthorizedError: () => UnauthorizedError,
43
52
  WidgetContainer: () => WidgetContainer,
44
- WidgetHeader: () => WidgetHeader,
45
53
  WidgetProvider: () => WidgetProvider,
54
+ createHostTransport: () => createHostTransport,
46
55
  formatDate: () => formatDate,
47
56
  getBaseUrl: () => getBaseUrl,
48
57
  getDaysRemaining: () => getDaysRemaining,
49
58
  getResolvedTheme: () => getResolvedTheme,
59
+ isFrugaMessage: () => isFrugaMessage,
60
+ isFrugaMessageType: () => isFrugaMessageType,
61
+ isTestMode: () => isTestMode,
50
62
  isWithinDays: () => isWithinDays,
63
+ protocolMajor: () => protocolMajor,
64
+ resolvePayoutMode: () => resolvePayoutMode,
65
+ storageKey: () => storageKey,
51
66
  truncateText: () => truncateText,
52
67
  useDebounce: () => useDebounce,
68
+ useFrugaMessage: () => useFrugaMessage,
69
+ useMessageTransport: () => useMessageTransport,
53
70
  useWidget: () => useWidget,
54
71
  useWidgetStore: () => useWidgetStore
55
72
  });
@@ -108,50 +125,51 @@ function getResolvedTheme(theme) {
108
125
  };
109
126
  }
110
127
 
111
- // packages/core/src/infrastructure/adapters/HttpWalletService.ts
112
- var HttpWalletService = class {
113
- constructor(baseUrl, token) {
114
- this.baseUrl = baseUrl;
115
- this.token = token;
128
+ // packages/core/src/domain/entities/Messages.ts
129
+ var PROTOCOL_VERSION = "1.0";
130
+ var MESSAGE_TYPES = [
131
+ "NEED_TOKEN",
132
+ "FRUGA_TOKEN_UPDATE",
133
+ "FRUGA_GET_BALANCE",
134
+ "FRUGA_BALANCE_RESPONSE",
135
+ "FRUGA_RESIZE",
136
+ "FRUGA_HOST_RESIZE",
137
+ "FRUGA_READY",
138
+ "FRUGA_INIT"
139
+ ];
140
+ var isFrugaMessageType = (value) => typeof value === "string" && MESSAGE_TYPES.includes(value);
141
+ var NEED_TOKEN_REASONS = ["ttl", "unauthorized"];
142
+ var isFrugaMessage = (data) => {
143
+ if (typeof data !== "object" || data === null) return false;
144
+ const candidate = data;
145
+ if (!isFrugaMessageType(candidate.type)) return false;
146
+ if (candidate.version !== void 0 && typeof candidate.version !== "string") return false;
147
+ if (candidate.type === "NEED_TOKEN") {
148
+ const { reason } = data;
149
+ return typeof reason === "string" && NEED_TOKEN_REASONS.includes(reason);
116
150
  }
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
- }
151
+ return true;
152
+ };
153
+ var protocolMajor = (v) => Number.parseInt(v.split(".")[0] ?? "", 10);
154
+
155
+ // packages/core/src/domain/entities/Constants.ts
156
+ var HOST_MOBILE_BREAKPOINT = 768;
157
+
158
+ // packages/core/src/domain/entities/index.ts
159
+ var resolvePayoutMode = (value) => value === "PARTNER" || value === "USER" ? value : null;
160
+
161
+ // packages/core/src/domain/errors/HttpError.ts
162
+ var HttpError = class extends Error {
163
+ constructor(status, message) {
164
+ super(message);
165
+ this.status = status;
166
+ this.name = "HttpError";
144
167
  }
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
- };
168
+ };
169
+ var UnauthorizedError = class extends HttpError {
170
+ constructor(message = "Unauthorized") {
171
+ super(401, message);
172
+ this.name = "UnauthorizedError";
155
173
  }
156
174
  };
157
175
 
@@ -181,6 +199,41 @@ var BaseHttpService = class {
181
199
  });
182
200
  }
183
201
  };
202
+ var assertOk = (response, context) => {
203
+ if (response.ok) return;
204
+ if (response.status === 401) {
205
+ throw new UnauthorizedError(`${context}: unauthorized`);
206
+ }
207
+ throw new HttpError(response.status, `${context}: ${response.status}`);
208
+ };
209
+
210
+ // packages/core/src/infrastructure/adapters/HttpWalletService.ts
211
+ var HttpWalletService = class {
212
+ constructor(baseUrl, token) {
213
+ this.baseUrl = baseUrl;
214
+ this.token = token;
215
+ }
216
+ async getWallet() {
217
+ const response = await fetch(`${this.baseUrl}/api/wallet`, {
218
+ method: "GET",
219
+ headers: {
220
+ "Accept": "application/json",
221
+ "Authorization": `Bearer ${this.token}`
222
+ }
223
+ });
224
+ assertOk(response, "Failed to fetch wallet");
225
+ const data = await response.json();
226
+ return {
227
+ available: data.available ?? 0,
228
+ pending: data.pending ?? 0,
229
+ totalBalance: (data.available || 0) + (data.pending || 0),
230
+ lifetimeEarned: data.lifetimeEarned ?? 0,
231
+ lifetimeRedeemed: data.lifetimeRedeemed ?? 0,
232
+ updatedAt: data.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
233
+ requiredActions: data.requiredActions ?? null
234
+ };
235
+ }
236
+ };
184
237
 
185
238
  // packages/core/src/infrastructure/adapters/HttpActivityService.ts
186
239
  var HttpActivityService = class extends BaseHttpService {
@@ -188,27 +241,12 @@ var HttpActivityService = class extends BaseHttpService {
188
241
  super(baseUrl, token);
189
242
  }
190
243
  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() {
244
+ const response = await this.fetchQuery("/api/activity", { pageNumber, pageSize });
245
+ assertOk(response, "Failed to fetch activity");
246
+ const data = await response.json();
208
247
  return {
209
- actorType: "",
210
- partnerId: 0,
211
- transactions: []
248
+ ...data,
249
+ transactions: data.transactions || []
212
250
  };
213
251
  }
214
252
  };
@@ -219,53 +257,25 @@ var HttpOffersService = class extends BaseHttpService {
219
257
  super(baseUrl, token);
220
258
  }
221
259
  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
- }
260
+ const response = await this.fetchQuery("/api/offers/categories");
261
+ assertOk(response, "Failed to fetch offer categories");
262
+ const data = await response.json();
263
+ return data.categories || [];
235
264
  }
236
265
  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() {
266
+ const response = await this.fetchQuery("/api/offers", {
267
+ pageNumber: query.pageNumber ?? 0,
268
+ pageSize: query.pageSize ?? 10,
269
+ type: query.type,
270
+ query: query.query,
271
+ featured: query.featured,
272
+ categories: query.categories
273
+ });
274
+ assertOk(response, "Failed to fetch offers");
275
+ const data = await response.json();
261
276
  return {
262
- content: [],
263
- totalElements: 0,
264
- totalPages: 0,
265
- number: 0,
266
- size: 0,
267
- numberOfElements: 0,
268
- numberOfPages: 0
277
+ ...data,
278
+ content: data.content || []
269
279
  };
270
280
  }
271
281
  };
@@ -277,23 +287,16 @@ var HttpPayoutService = class {
277
287
  this.token = token;
278
288
  }
279
289
  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
+ const response = await fetch(`${this.baseUrl}/api/payout-methods`, {
291
+ method: "GET",
292
+ headers: {
293
+ "Accept": "application/json",
294
+ "Authorization": `Bearer ${this.token}`
290
295
  }
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
- }
296
+ });
297
+ assertOk(response, "Failed to list payout methods");
298
+ const data = await response.json();
299
+ return data.items || [];
297
300
  }
298
301
  async addBankMethod(payload) {
299
302
  const response = await fetch(`${this.baseUrl}/api/payout-methods/bank`, {
@@ -305,9 +308,7 @@ var HttpPayoutService = class {
305
308
  },
306
309
  body: JSON.stringify(payload)
307
310
  });
308
- if (!response.ok) {
309
- throw new Error(`Failed to add bank payout method: ${response.status}`);
310
- }
311
+ assertOk(response, "Failed to add bank payout method");
311
312
  return response.json();
312
313
  }
313
314
  async deleteMethod(id) {
@@ -318,53 +319,124 @@ var HttpPayoutService = class {
318
319
  "Authorization": `Bearer ${this.token}`
319
320
  }
320
321
  });
321
- if (!response.ok) {
322
- throw new Error(`Failed to delete payout method: ${response.status}`);
322
+ assertOk(response, "Failed to delete payout method");
323
+ }
324
+ };
325
+
326
+ // packages/core/src/infrastructure/adapters/PostMessageTransport.ts
327
+ var WILDCARD_ORIGIN = "*";
328
+ var PostMessageTransport = class {
329
+ constructor(options) {
330
+ __publicField(this, "target");
331
+ __publicField(this, "acceptSource");
332
+ __publicField(this, "listenOn");
333
+ __publicField(this, "handlers", /* @__PURE__ */ new Set());
334
+ __publicField(this, "pinOriginFrom");
335
+ __publicField(this, "targetOrigin");
336
+ __publicField(this, "acceptOrigin");
337
+ __publicField(this, "pinned", null);
338
+ __publicField(this, "disposed", false);
339
+ __publicField(this, "onMessage", (event) => {
340
+ if (this.disposed) return;
341
+ if (this.acceptSource && event.source !== this.acceptSource) return;
342
+ if (this.acceptOrigin && event.origin !== this.acceptOrigin) return;
343
+ if (!isFrugaMessage(event.data)) return;
344
+ if (!this.admitWhileUnpinned(event.data.type, event.origin)) return;
345
+ const message = typeof event.data.version === "string" && event.data.version.length > 0 ? event.data : { ...event.data, version: PROTOCOL_VERSION };
346
+ for (const handler of [...this.handlers]) {
347
+ handler(message);
348
+ }
349
+ });
350
+ const listenOn = options.listenOn ?? (typeof window !== "undefined" ? window : void 0);
351
+ if (!listenOn) {
352
+ throw new Error("PostMessageTransport: no window to listen on; pass options.listenOn");
353
+ }
354
+ this.target = options.target;
355
+ this.targetOrigin = options.targetOrigin;
356
+ this.acceptSource = options.acceptSource;
357
+ this.acceptOrigin = options.acceptOrigin;
358
+ this.pinOriginFrom = options.pinOriginFrom;
359
+ this.listenOn = listenOn;
360
+ this.listenOn.addEventListener("message", this.onMessage);
361
+ }
362
+ /**
363
+ * Origin the transport has locked onto, `'*'` when it pinned to the wildcard
364
+ * after an opaque-origin handshake, or `null` while still unpinned.
365
+ */
366
+ get pinnedOrigin() {
367
+ return this.pinned;
368
+ }
369
+ /** Lock both the outbound and inbound origin once the peer origin is known. */
370
+ pinOrigin(origin) {
371
+ this.targetOrigin = origin;
372
+ this.acceptOrigin = origin;
373
+ this.pinned = origin;
374
+ }
375
+ send(message) {
376
+ if (this.disposed) return;
377
+ const payload = typeof message.version === "string" && message.version.length > 0 ? message : { ...message, version: PROTOCOL_VERSION };
378
+ this.target.postMessage(payload, this.targetOrigin);
379
+ }
380
+ subscribe(handler) {
381
+ if (this.disposed) {
382
+ throw new Error("PostMessageTransport: cannot subscribe after dispose()");
383
+ }
384
+ this.handlers.add(handler);
385
+ return () => {
386
+ this.handlers.delete(handler);
387
+ };
388
+ }
389
+ dispose() {
390
+ if (this.disposed) return;
391
+ this.disposed = true;
392
+ this.handlers.clear();
393
+ this.listenOn.removeEventListener("message", this.onMessage);
394
+ }
395
+ /**
396
+ * Gate applied only while `pinOriginFrom` is set and the transport is still
397
+ * unpinned: nothing but the pinning message type gets through, and the first
398
+ * one that does completes the handshake for everything after it.
399
+ */
400
+ admitWhileUnpinned(type, origin) {
401
+ if (!this.pinOriginFrom || this.pinned !== null) return true;
402
+ if (type !== this.pinOriginFrom) return false;
403
+ if (origin && origin !== "null") {
404
+ this.pinOrigin(origin);
405
+ return true;
323
406
  }
407
+ this.pinned = WILDCARD_ORIGIN;
408
+ return true;
324
409
  }
325
410
  };
326
411
 
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 });
412
+ // packages/core/src/infrastructure/adapters/createHostTransport.ts
413
+ var createHostTransport = (options) => {
414
+ const win = options?.listenOn ?? (typeof window !== "undefined" ? window : void 0);
415
+ if (!win) {
416
+ throw new Error("createHostTransport: no window available; pass options.listenOn");
417
+ }
418
+ const framed = win.parent !== win;
419
+ return new PostMessageTransport({
420
+ target: win.parent,
421
+ targetOrigin: "*",
422
+ pinOriginFrom: "FRUGA_INIT",
423
+ listenOn: win,
424
+ ...framed ? { acceptSource: win.parent } : {}
425
+ });
347
426
  };
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
427
 
356
428
  // packages/core/src/presentation/components/WidgetContainer.tsx
357
- var import_react3 = require("react");
429
+ var import_react5 = require("react");
358
430
 
359
431
  // packages/core/src/presentation/components/ui/LauncherIcon.tsx
360
432
  var import_lucide_react = require("lucide-react");
361
- var import_jsx_runtime2 = require("react/jsx-runtime");
433
+ var import_jsx_runtime = require("react/jsx-runtime");
362
434
  function LauncherIcon({ theme }) {
363
435
  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 });
436
+ if (theme.launcherIcon === "sparkles") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Sparkles, { size: 20 });
437
+ if (theme.launcherIcon === "credit") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.CreditCard, { size: 20 });
438
+ if (theme.launcherIcon === "arrow") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.MoveUpRight, { size: 20 });
439
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Gift, { size: 20 });
368
440
  }
369
441
 
370
442
  // packages/core/src/presentation/hooks/useActivityDetector.ts
@@ -424,6 +496,8 @@ var useWidgetStore = create((set) => ({
424
496
  pending: 0,
425
497
  mode: "light",
426
498
  token: null,
499
+ tokenEpoch: 0,
500
+ authStatus: "pending",
427
501
  partnerPayout: null,
428
502
  isAddingPayoutFromAlert: false,
429
503
  showPayoutSuccess: false,
@@ -433,8 +507,12 @@ var useWidgetStore = create((set) => ({
433
507
  setIdle: (idle) => set({ isIdle: idle }),
434
508
  setBalances: (available, pending) => set({ available, pending }),
435
509
  setMode: (mode) => set({ mode }),
436
- setToken: (token) => set({ token }),
437
- setPartnerPayout: (payout) => set({ partnerPayout: payout }),
510
+ setToken: (token) => set((state) => ({
511
+ token,
512
+ tokenEpoch: state.token === token ? state.tokenEpoch : state.tokenEpoch + 1,
513
+ authStatus: token ? "authenticated" : "pending"
514
+ })),
515
+ setPartnerPayout: (payout) => set({ partnerPayout: resolvePayoutMode(payout) }),
438
516
  setIsAddingPayoutFromAlert: (val) => set({ isAddingPayoutFromAlert: val }),
439
517
  setShowPayoutSuccess: (val) => set({ showPayoutSuccess: val })
440
518
  }));
@@ -482,6 +560,42 @@ function useActivityDetector() {
482
560
  }, [setFocused, setIdle]);
483
561
  }
484
562
 
563
+ // packages/core/src/presentation/hooks/useMessageTransport.ts
564
+ var import_react4 = require("react");
565
+
566
+ // packages/core/src/presentation/context/MessageTransportContext.tsx
567
+ var import_react3 = require("react");
568
+ var import_jsx_runtime2 = require("react/jsx-runtime");
569
+ var MessageTransportContext = (0, import_react3.createContext)(null);
570
+ var MessageTransportProvider = ({
571
+ transport,
572
+ children
573
+ }) => {
574
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MessageTransportContext.Provider, { value: transport, children });
575
+ };
576
+
577
+ // packages/core/src/presentation/hooks/useMessageTransport.ts
578
+ var useMessageTransport = () => {
579
+ const transport = (0, import_react4.useContext)(MessageTransportContext);
580
+ if (!transport) {
581
+ throw new Error(
582
+ "useMessageTransport: no transport in context. Wrap the widget tree in <MessageTransportProvider transport={...}>."
583
+ );
584
+ }
585
+ return transport;
586
+ };
587
+ var useFrugaMessage = (type, handler) => {
588
+ const transport = useMessageTransport();
589
+ const handlerRef = (0, import_react4.useRef)(handler);
590
+ handlerRef.current = handler;
591
+ (0, import_react4.useEffect)(() => {
592
+ return transport.subscribe((message) => {
593
+ if (message.type !== type) return;
594
+ handlerRef.current(message);
595
+ });
596
+ }, [transport, type]);
597
+ };
598
+
485
599
  // packages/core/src/presentation/components/WidgetContainer.tsx
486
600
  var import_lucide_react2 = require("lucide-react");
487
601
  var import_jsx_runtime3 = require("react/jsx-runtime");
@@ -494,37 +608,47 @@ var WidgetContainer = ({
494
608
  useActivityDetector();
495
609
  const { isOpen, toggle, mode: storeMode } = useWidgetStore();
496
610
  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" };
611
+ const [hostHeight, setHostHeight] = (0, import_react5.useState)(initialHostHeight);
612
+ const [isMobile, setIsMobile] = (0, import_react5.useState)(initialIsMobile);
613
+ const [hostThemeMode, setHostThemeMode] = (0, import_react5.useState)(void 0);
614
+ const effectiveTheme = theme.allowUserThemeControl ? { ...theme, mode: storeMode } : { ...theme, mode: hostThemeMode ?? theme.mode ?? "light" };
500
615
  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)(() => {
616
+ const containerRef = (0, import_react5.useRef)(null);
617
+ const buttonRef = (0, import_react5.useRef)(null);
618
+ const transport = useMessageTransport();
619
+ useFrugaMessage("FRUGA_HOST_RESIZE", (message) => {
620
+ setHostHeight(message.hostHeight);
621
+ setIsMobile(message.isHostMobile);
622
+ });
623
+ useFrugaMessage("FRUGA_TOKEN_UPDATE", (message) => {
624
+ useWidgetStore.getState().setToken(message.token);
625
+ });
626
+ useFrugaMessage("FRUGA_GET_BALANCE", (message) => {
627
+ const { available, pending } = useWidgetStore.getState();
628
+ transport.send({
629
+ type: "FRUGA_BALANCE_RESPONSE",
630
+ version: PROTOCOL_VERSION,
631
+ requestId: message.requestId,
632
+ available,
633
+ pending
634
+ });
635
+ });
636
+ useFrugaMessage("FRUGA_INIT", (message) => {
637
+ setHostHeight(message.host.height);
638
+ setIsMobile(message.host.isMobile);
639
+ if (message.token) {
640
+ useWidgetStore.getState().setToken(message.token);
641
+ }
642
+ if (message.overrides?.panelOpen) {
643
+ useWidgetStore.getState().setOpen(true);
644
+ }
645
+ const overrideThemeMode = message.overrides?.theme;
646
+ if (overrideThemeMode) {
647
+ setHostThemeMode(overrideThemeMode);
648
+ useWidgetStore.getState().setMode(overrideThemeMode);
649
+ }
650
+ });
651
+ (0, import_react5.useEffect)(() => {
528
652
  if (!containerRef.current) return;
529
653
  const sendResize = () => {
530
654
  if (!containerRef.current) return;
@@ -537,18 +661,19 @@ var WidgetContainer = ({
537
661
  expectedWidth = window.innerWidth;
538
662
  expectedHeight = hostHeight;
539
663
  }
540
- window.parent.postMessage({
664
+ transport.send({
541
665
  type: "FRUGA_RESIZE",
666
+ version: PROTOCOL_VERSION,
542
667
  height: expectedHeight,
543
668
  width: expectedWidth,
544
669
  isFullscreen: isMobile && isOpen
545
- }, "*");
670
+ });
546
671
  };
547
672
  const observer = new ResizeObserver(() => sendResize());
548
673
  observer.observe(containerRef.current);
549
674
  sendResize();
550
675
  return () => observer.disconnect();
551
- }, [isOpen, hostHeight, isMobile]);
676
+ }, [isOpen, hostHeight, isMobile, transport]);
552
677
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
553
678
  "div",
554
679
  {
@@ -647,10 +772,10 @@ var useWidget = () => {
647
772
  };
648
773
 
649
774
  // packages/core/src/presentation/hooks/useDebounce.ts
650
- var import_react4 = require("react");
775
+ var import_react6 = require("react");
651
776
  var useDebounce = (value, delay) => {
652
- const [debouncedValue, setDebouncedValue] = (0, import_react4.useState)(value);
653
- (0, import_react4.useEffect)(() => {
777
+ const [debouncedValue, setDebouncedValue] = (0, import_react6.useState)(value);
778
+ (0, import_react6.useEffect)(() => {
654
779
  const handleTimeout = setTimeout(() => {
655
780
  setDebouncedValue(value);
656
781
  }, delay);
@@ -662,10 +787,8 @@ var useDebounce = (value, delay) => {
662
787
  };
663
788
 
664
789
  // packages/core/src/infrastructure/storage/StorageService.ts
665
- var STORAGE_KEYS = {
666
- BOOTSTRAP_DATA: "fruga_bootstrap",
667
- BOOTSTRAP_CONFIG: "fruga_active_config"
668
- };
790
+ var LEGACY_BOOTSTRAP_STORAGE_KEY = "fruga_bootstrap";
791
+ var storageKey = (partnerKey, name) => `fruga:${partnerKey}:${name}`;
669
792
  var StorageService = class {
670
793
  static set(key, value) {
671
794
  if (typeof window === "undefined") return;
@@ -687,20 +810,22 @@ var StorageService = class {
687
810
  }
688
811
  static remove(key) {
689
812
  if (typeof window === "undefined") return;
690
- window.localStorage.removeItem(key);
691
- }
692
- static clear() {
693
- if (typeof window === "undefined") return;
694
- window.localStorage.clear();
813
+ try {
814
+ window.localStorage.removeItem(key);
815
+ } catch (error) {
816
+ console.warn(`[FrugaStorage] Failed to remove key "${key}":`, error);
817
+ }
695
818
  }
696
819
  };
697
820
 
698
821
  // 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";
822
+ var DEFAULT_API_URL = "https://api.fruga.co.uk";
700
823
  var getBaseUrl = (override) => {
701
824
  if (override) return override;
702
- return DEFAULT_API_URL;
825
+ return "" ? "" : DEFAULT_API_URL;
703
826
  };
827
+ var isTestMode = () => false;
828
+ var SDK_VERSION = "1.3.0" ? "1.3.0" : "0.0.0-dev";
704
829
 
705
830
  // packages/core/src/infrastructure/helpers/date.ts
706
831
  var formatDate = (dateStr) => {
@@ -713,7 +838,7 @@ var formatDate = (dateStr) => {
713
838
  const month = (date.getMonth() + 1).toString().padStart(2, "0");
714
839
  const year = date.getFullYear();
715
840
  return `${day}-${month}-${year}`;
716
- } catch (e) {
841
+ } catch {
717
842
  return dateStr;
718
843
  }
719
844
  };
@@ -760,23 +885,38 @@ var truncateText = (str, maxLength = 40) => {
760
885
  DEFAULT_API_URL,
761
886
  DEFAULT_THEME,
762
887
  DefaultTheme,
888
+ HOST_MOBILE_BREAKPOINT,
763
889
  HttpActivityService,
890
+ HttpError,
764
891
  HttpOffersService,
765
892
  HttpPayoutService,
766
893
  HttpWalletService,
767
- STORAGE_KEYS,
894
+ LEGACY_BOOTSTRAP_STORAGE_KEY,
895
+ MessageTransportContext,
896
+ MessageTransportProvider,
897
+ PROTOCOL_VERSION,
898
+ PostMessageTransport,
899
+ SDK_VERSION,
768
900
  StorageService,
769
- WidgetButton,
901
+ UnauthorizedError,
770
902
  WidgetContainer,
771
- WidgetHeader,
772
903
  WidgetProvider,
904
+ createHostTransport,
773
905
  formatDate,
774
906
  getBaseUrl,
775
907
  getDaysRemaining,
776
908
  getResolvedTheme,
909
+ isFrugaMessage,
910
+ isFrugaMessageType,
911
+ isTestMode,
777
912
  isWithinDays,
913
+ protocolMajor,
914
+ resolvePayoutMode,
915
+ storageKey,
778
916
  truncateText,
779
917
  useDebounce,
918
+ useFrugaMessage,
919
+ useMessageTransport,
780
920
  useWidget,
781
921
  useWidgetStore
782
922
  });