@pixels-online/pixels-client-js-sdk 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1557 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.OfferwallSDK = {}));
5
+ })(this, (function (exports) { 'use strict';
6
+
7
+ class Logger {
8
+ constructor(config, prefix) {
9
+ this.config = config;
10
+ this.prefix = prefix;
11
+ }
12
+ log(...args) {
13
+ if (this.config.debug) {
14
+ console.log(`[${this.prefix}]`, ...args);
15
+ }
16
+ }
17
+ error(...args) {
18
+ console.error(`[${this.prefix}]`, ...args);
19
+ }
20
+ warn(...args) {
21
+ if (this.config.debug) {
22
+ console.warn(`[${this.prefix}]`, ...args);
23
+ }
24
+ }
25
+ info(...args) {
26
+ if (this.config.debug) {
27
+ console.info(`[${this.prefix}]`, ...args);
28
+ }
29
+ }
30
+ /**
31
+ * Create a child logger with an additional prefix
32
+ */
33
+ child(subPrefix) {
34
+ return new Logger(this.config, `${this.prefix}:${subPrefix}`);
35
+ }
36
+ }
37
+ /**
38
+ * Create a logger instance for a component
39
+ */
40
+ function createLogger(config, prefix) {
41
+ return new Logger(config, prefix);
42
+ }
43
+
44
+ class EventEmitter {
45
+ constructor(config) {
46
+ this.events = new Map();
47
+ this.logger = createLogger(config, 'EventEmitter');
48
+ }
49
+ on(event, handler) {
50
+ if (!this.events.has(event)) {
51
+ this.events.set(event, new Set());
52
+ }
53
+ this.events.get(event).add(handler);
54
+ this.logger.log(`Handler registered for event: ${event}`);
55
+ }
56
+ off(event, handler) {
57
+ const handlers = this.events.get(event);
58
+ if (handlers) {
59
+ handlers.delete(handler);
60
+ if (handlers.size === 0) {
61
+ this.events.delete(event);
62
+ }
63
+ }
64
+ this.logger.log(`Handler removed for event: ${event}`);
65
+ }
66
+ emit(event, data) {
67
+ const handlers = this.events.get(event);
68
+ if (handlers) {
69
+ handlers.forEach(handler => {
70
+ try {
71
+ handler(data);
72
+ }
73
+ catch (error) {
74
+ this.logger.error(`Error in handler for event ${event}:`, error);
75
+ }
76
+ });
77
+ }
78
+ this.logger.log(`Event emitted: ${event}`, data);
79
+ }
80
+ once(event, handler) {
81
+ const wrappedHandler = (data) => {
82
+ handler(data);
83
+ this.off(event, wrappedHandler);
84
+ };
85
+ this.on(event, wrappedHandler);
86
+ }
87
+ removeAllListeners(event) {
88
+ if (event) {
89
+ this.events.delete(event);
90
+ }
91
+ else {
92
+ this.events.clear();
93
+ }
94
+ this.logger.log(`Listeners removed for: ${event || 'all events'}`);
95
+ }
96
+ listenerCount(event) {
97
+ const handlers = this.events.get(event);
98
+ return handlers ? handlers.size : 0;
99
+ }
100
+ }
101
+
102
+ exports.OfferEvent = void 0;
103
+ (function (OfferEvent) {
104
+ OfferEvent["CONNECTED"] = "connected";
105
+ OfferEvent["DISCONNECTED"] = "disconnected";
106
+ OfferEvent["CONNECTION_ERROR"] = "connection_error";
107
+ OfferEvent["CONNECTION_STATE_CHANGED"] = "connection_state_changed";
108
+ OfferEvent["OFFER_SURFACED"] = "offer_surfaced";
109
+ OfferEvent["OFFER_CLAIMED"] = "offer_claimed";
110
+ OfferEvent["ERROR"] = "error";
111
+ OfferEvent["REFRESH"] = "refresh";
112
+ /* placeholder events for future use
113
+ OFFER_ADDED = 'offer_added',
114
+ OFFER_UPDATED = 'offer_updated',
115
+ SNAPSHOT_UPDATED = 'snapshot_updated',
116
+ */
117
+ })(exports.OfferEvent || (exports.OfferEvent = {}));
118
+
119
+ /**
120
+ * Connection states for SSE connection
121
+ */
122
+ exports.ConnectionState = void 0;
123
+ (function (ConnectionState) {
124
+ ConnectionState["DISCONNECTED"] = "disconnected";
125
+ ConnectionState["CONNECTING"] = "connecting";
126
+ ConnectionState["CONNECTED"] = "connected";
127
+ ConnectionState["RECONNECTING"] = "reconnecting";
128
+ ConnectionState["ERROR"] = "error";
129
+ })(exports.ConnectionState || (exports.ConnectionState = {}));
130
+
131
+ /**
132
+ * Custom EventSource implementation that supports headers
133
+ * This is needed because the native EventSource API doesn't support custom headers
134
+ */
135
+ var ReadyState;
136
+ (function (ReadyState) {
137
+ ReadyState[ReadyState["CONNECTING"] = 0] = "CONNECTING";
138
+ ReadyState[ReadyState["OPEN"] = 1] = "OPEN";
139
+ ReadyState[ReadyState["CLOSED"] = 2] = "CLOSED";
140
+ })(ReadyState || (ReadyState = {}));
141
+ class CustomEventSource {
142
+ constructor(url, init) {
143
+ this.readyState = ReadyState.CONNECTING;
144
+ this.eventListeners = new Map();
145
+ this.onopen = null;
146
+ this.onerror = null;
147
+ this.onmessage = null;
148
+ this.reader = null;
149
+ this.decoder = new TextDecoder();
150
+ this.eventBuffer = '';
151
+ this.abortController = null;
152
+ this.lastEventId = '';
153
+ this.reconnectTime = 3000;
154
+ this.onRetryUpdate = null;
155
+ this.url = url;
156
+ this.headers = {
157
+ 'Accept': 'text/event-stream',
158
+ 'Cache-Control': 'no-cache',
159
+ ...(init?.headers || {})
160
+ };
161
+ this.connect();
162
+ }
163
+ async connect() {
164
+ try {
165
+ this.readyState = ReadyState.CONNECTING;
166
+ this.abortController = new AbortController();
167
+ const response = await fetch(this.url, {
168
+ method: 'GET',
169
+ headers: this.headers,
170
+ credentials: 'same-origin',
171
+ signal: this.abortController.signal,
172
+ });
173
+ if (!response.ok) {
174
+ // Check for JWT errors
175
+ if (response.status === 400 || response.status === 401) {
176
+ try {
177
+ const json = await response.json();
178
+ if (json.message === 'jwt-expired' || json.message === 'jwt-invalid') {
179
+ const error = new Error(json.message);
180
+ error.isAuthError = true;
181
+ throw error;
182
+ }
183
+ }
184
+ catch (e) {
185
+ // If not JSON or no message, throw original error
186
+ }
187
+ }
188
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
189
+ }
190
+ if (!response.body) {
191
+ throw new Error('Response body is null');
192
+ }
193
+ this.readyState = ReadyState.OPEN;
194
+ // Dispatch open event
195
+ if (this.onopen) {
196
+ this.onopen(new Event('open'));
197
+ }
198
+ this.reader = response.body.getReader();
199
+ await this.readStream();
200
+ }
201
+ catch (error) {
202
+ this.handleError(error);
203
+ }
204
+ }
205
+ async readStream() {
206
+ if (!this.reader)
207
+ return;
208
+ try {
209
+ while (true) {
210
+ const { done, value } = await this.reader.read();
211
+ if (done) {
212
+ this.handleError(new Error('Stream ended'));
213
+ break;
214
+ }
215
+ const chunk = this.decoder.decode(value, { stream: true });
216
+ this.eventBuffer += chunk;
217
+ this.processBuffer();
218
+ }
219
+ }
220
+ catch (error) {
221
+ this.handleError(error);
222
+ }
223
+ }
224
+ processBuffer() {
225
+ const lines = this.eventBuffer.split('\n');
226
+ this.eventBuffer = lines.pop() || '';
227
+ let eventType = 'message';
228
+ let eventData = '';
229
+ let eventId = '';
230
+ for (const line of lines) {
231
+ if (line === '') {
232
+ // Empty line signals end of event
233
+ if (eventData) {
234
+ this.dispatchEvent(eventType, eventData.trim(), eventId);
235
+ eventType = 'message';
236
+ eventData = '';
237
+ eventId = '';
238
+ }
239
+ }
240
+ else if (line.startsWith(':')) {
241
+ // Comment, ignore
242
+ continue;
243
+ }
244
+ else if (line.startsWith('event:')) {
245
+ eventType = line.slice(6).trim();
246
+ }
247
+ else if (line.startsWith('data:')) {
248
+ eventData += line.slice(5).trim() + '\n';
249
+ }
250
+ else if (line.startsWith('id:')) {
251
+ eventId = line.slice(3).trim();
252
+ this.lastEventId = eventId;
253
+ }
254
+ else if (line.startsWith('retry:')) {
255
+ const retry = parseInt(line.slice(6).trim(), 10);
256
+ if (!isNaN(retry)) {
257
+ this.reconnectTime = retry;
258
+ // Notify SSEConnection about the server's suggested retry time
259
+ if (this.onRetryUpdate) {
260
+ this.onRetryUpdate(retry);
261
+ }
262
+ }
263
+ }
264
+ }
265
+ }
266
+ dispatchEvent(type, data, id) {
267
+ const event = new MessageEvent(type, {
268
+ data: data.endsWith('\n') ? data.slice(0, -1) : data,
269
+ lastEventId: id || this.lastEventId,
270
+ origin: new URL(this.url).origin,
271
+ });
272
+ // Dispatch to specific event listeners
273
+ const listeners = this.eventListeners.get(type);
274
+ if (listeners) {
275
+ listeners.forEach(listener => listener(event));
276
+ }
277
+ // Dispatch to general message handler for 'message' events
278
+ if (type === 'message' && this.onmessage) {
279
+ this.onmessage(event);
280
+ }
281
+ }
282
+ handleError(error) {
283
+ this.readyState = ReadyState.CLOSED;
284
+ if (this.reader) {
285
+ this.reader.cancel();
286
+ this.reader = null;
287
+ }
288
+ // Dispatch error event with details
289
+ if (this.onerror) {
290
+ const errorEvent = new Event('error');
291
+ errorEvent.detail = error;
292
+ errorEvent.message = error?.message;
293
+ this.onerror(errorEvent);
294
+ }
295
+ // Reconnection is handled by SSEConnection, not here
296
+ }
297
+ addEventListener(type, listener) {
298
+ if (!this.eventListeners.has(type)) {
299
+ this.eventListeners.set(type, new Set());
300
+ }
301
+ this.eventListeners.get(type).add(listener);
302
+ }
303
+ removeEventListener(type, listener) {
304
+ const listeners = this.eventListeners.get(type);
305
+ if (listeners) {
306
+ listeners.delete(listener);
307
+ }
308
+ }
309
+ close() {
310
+ this.readyState = ReadyState.CLOSED;
311
+ if (this.abortController) {
312
+ this.abortController.abort();
313
+ this.abortController = null;
314
+ }
315
+ if (this.reader) {
316
+ this.reader.cancel();
317
+ this.reader = null;
318
+ }
319
+ this.eventListeners.clear();
320
+ this.onopen = null;
321
+ this.onerror = null;
322
+ this.onmessage = null;
323
+ }
324
+ get CONNECTING() {
325
+ return ReadyState.CONNECTING;
326
+ }
327
+ get OPEN() {
328
+ return ReadyState.OPEN;
329
+ }
330
+ get CLOSED() {
331
+ return ReadyState.CLOSED;
332
+ }
333
+ getReadyState() {
334
+ return this.readyState;
335
+ }
336
+ getReconnectTime() {
337
+ return this.reconnectTime;
338
+ }
339
+ }
340
+
341
+ class SSEConnection {
342
+ constructor(config, eventEmitter, tokenManager) {
343
+ this.config = config;
344
+ this.eventEmitter = eventEmitter;
345
+ this.tokenManager = tokenManager;
346
+ this.eventSource = null;
347
+ this.reconnectAttempts = 0;
348
+ this.reconnectTimeout = null;
349
+ this.isConnecting = false;
350
+ this.connectionState = exports.ConnectionState.DISCONNECTED;
351
+ this.serverSuggestedRetryTime = null;
352
+ this.logger = createLogger(config, 'SSEConnection');
353
+ }
354
+ /**
355
+ * Get current connection state
356
+ */
357
+ getConnectionState() {
358
+ return this.connectionState;
359
+ }
360
+ /**
361
+ * Set connection state and emit change event
362
+ */
363
+ setConnectionState(newState, error) {
364
+ const previousState = this.connectionState;
365
+ if (previousState === newState)
366
+ return;
367
+ this.connectionState = newState;
368
+ this.eventEmitter.emit(exports.OfferEvent.CONNECTION_STATE_CHANGED, {
369
+ state: newState,
370
+ previousState,
371
+ error,
372
+ attempt: this.reconnectAttempts,
373
+ maxAttempts: this.config.maxReconnectAttempts
374
+ });
375
+ }
376
+ connect() {
377
+ return new Promise(async (resolve, reject) => {
378
+ if (this.eventSource) {
379
+ this.logger.log('Already connected');
380
+ resolve();
381
+ return;
382
+ }
383
+ if (this.isConnecting) {
384
+ this.logger.log('Connection already in progress');
385
+ resolve();
386
+ return;
387
+ }
388
+ this.isConnecting = true;
389
+ this.setConnectionState(exports.ConnectionState.CONNECTING);
390
+ this.logger.log('Connecting to SSE endpoint...');
391
+ try {
392
+ // Create SSE URL
393
+ const url = new URL(this.config.endpoint + '/sse/connect');
394
+ const token = await this.tokenManager.getTokenForConnection();
395
+ // Use CustomEventSource with Authorization Bearer header
396
+ this.eventSource = new CustomEventSource(url.toString(), {
397
+ headers: {
398
+ 'Authorization': `Bearer ${token}`,
399
+ }
400
+ });
401
+ // Listen for server-suggested retry time updates
402
+ this.eventSource.onRetryUpdate = (retryTime) => {
403
+ this.serverSuggestedRetryTime = retryTime;
404
+ this.logger.log(`Server suggested retry time: ${retryTime}ms`);
405
+ };
406
+ this.eventSource.onopen = () => {
407
+ this.logger.log('SSE connection opened');
408
+ this.isConnecting = false;
409
+ this.reconnectAttempts = 0;
410
+ this.setConnectionState(exports.ConnectionState.CONNECTED);
411
+ this.eventEmitter.emit(exports.OfferEvent.CONNECTED, { timestamp: new Date() });
412
+ resolve();
413
+ };
414
+ this.eventSource.onerror = (error) => {
415
+ this.logger.log('SSE connection error', error);
416
+ this.isConnecting = false;
417
+ // Check if it's an auth error
418
+ if (error?.detail?.isAuthError || error?.message === 'jwt-expired' || error?.message === 'jwt-invalid') {
419
+ this.tokenManager.clearToken();
420
+ this.setConnectionState(exports.ConnectionState.DISCONNECTED);
421
+ // Try to reconnect with fresh token if reconnect is enabled
422
+ if (this.config.reconnect && this.reconnectAttempts < (this.config.maxReconnectAttempts || 5)) {
423
+ this.logger.log('JWT invalid/expired, attempting reconnect with fresh token');
424
+ this.handleReconnect();
425
+ resolve(); // Resolve instead of reject to allow reconnection
426
+ return;
427
+ }
428
+ else {
429
+ reject(new Error('JWT token invalid or expired'));
430
+ return;
431
+ }
432
+ }
433
+ const errorMsg = this.eventSource?.getReadyState() === ReadyState.CLOSED ?
434
+ 'Connection closed' : 'Connection error';
435
+ const connectionError = new Error(errorMsg);
436
+ this.setConnectionState(exports.ConnectionState.ERROR, connectionError);
437
+ if (this.eventSource?.getReadyState() === ReadyState.CLOSED) {
438
+ this.eventEmitter.emit(exports.OfferEvent.CONNECTION_ERROR, {
439
+ error: connectionError,
440
+ timestamp: new Date()
441
+ });
442
+ this.handleReconnect();
443
+ }
444
+ else {
445
+ this.eventEmitter.emit(exports.OfferEvent.CONNECTION_ERROR, {
446
+ error: connectionError,
447
+ timestamp: new Date()
448
+ });
449
+ reject(connectionError);
450
+ }
451
+ };
452
+ this.eventSource.addEventListener(exports.OfferEvent.OFFER_SURFACED, (event) => {
453
+ this.handleMessage(exports.OfferEvent.OFFER_SURFACED, event.data);
454
+ });
455
+ /*this.eventSource.addEventListener(OfferEvent.INIT, (event) => {
456
+ this.handleMessage(OfferEvent.INIT, event.data);
457
+ });
458
+
459
+ this.eventSource.addEventListener(OfferEvent.OFFER_ADDED, (event) => {
460
+ this.handleMessage(OfferEvent.OFFER_ADDED, event.data);
461
+ });
462
+
463
+
464
+ this.eventSource.addEventListener(OfferEvent.OFFER_UPDATED, (event) => {
465
+ this.handleMessage(OfferEvent.OFFER_UPDATED, event.data);
466
+ });
467
+
468
+ this.eventSource.addEventListener(OfferEvent.SNAPSHOT_UPDATED, (event) => {
469
+ this.handleMessage(OfferEvent.SNAPSHOT_UPDATED, event.data);
470
+ });
471
+ */
472
+ }
473
+ catch (error) {
474
+ this.isConnecting = false;
475
+ this.logger.log('Failed to create EventSource', error);
476
+ reject(error);
477
+ }
478
+ });
479
+ }
480
+ async handleMessage(type, data) {
481
+ try {
482
+ const parsed = JSON.parse(data);
483
+ this.logger.log(`Handling ${type} message:`, parsed);
484
+ switch (type) {
485
+ /*
486
+ case OfferEvent.INIT:
487
+ if (!parsed.offers || !Array.isArray(parsed.offers))
488
+ throw new Error('INIT message missing offers array');
489
+
490
+ if (!parsed.playerSnapshot)
491
+ throw new Error('INIT message missing playerSnapshot');
492
+
493
+ this.eventEmitter.emit(OfferEvent.INIT, {
494
+ offers: parsed.offers as IClientOffer[],
495
+ playerSnapshot: parsed.playerSnapshot as IClientPlayerSnapshot
496
+ });
497
+
498
+ break;
499
+ case OfferEvent.OFFER_ADDED:
500
+ if (!parsed.offer)
501
+ throw new Error('OFFER_ADDED message missing offer');
502
+ this.eventEmitter.emit(OfferEvent.OFFER_ADDED, {
503
+ offer: parsed.offer as IClientOffer,
504
+ });
505
+ break;
506
+
507
+ case OfferEvent.OFFER_UPDATED:
508
+ if (!parsed.offer)
509
+ throw new Error('OFFER_UPDATED message missing offer');
510
+
511
+ this.eventEmitter.emit(OfferEvent.OFFER_UPDATED, {
512
+ offer: parsed.offer as IClientOffer,
513
+ });
514
+ break;
515
+ case OfferEvent.SNAPSHOT_UPDATED:
516
+ if (!parsed.playerSnapshot)
517
+ throw new Error('SNAPSHOT_UPDATED message missing playerSnapshot');
518
+ this.eventEmitter.emit(OfferEvent.SNAPSHOT_UPDATED, {
519
+ playerSnapshot: parsed.playerSnapshot as IClientPlayerSnapshot
520
+ });
521
+ break;
522
+ case OfferEvent.OFFER_CLAIMED:
523
+ if (!parsed.offerId)
524
+ throw new Error('OFFER_CLAIMED message missing offerId');
525
+ this.eventEmitter.emit(OfferEvent.OFFER_CLAIMED, {
526
+ instanceId: parsed as string,
527
+ });
528
+ break;
529
+ */
530
+ case exports.OfferEvent.OFFER_SURFACED:
531
+ if (!parsed.offer)
532
+ throw new Error('OFFER_SURFACED message missing offer');
533
+ let surface = true;
534
+ if (this.config.hooks?.onOfferSurfaced) {
535
+ surface = await this.config.hooks.onOfferSurfaced(parsed.offer);
536
+ }
537
+ if (surface) {
538
+ this.logger.log('Offer surfaced hook returned true, popping notification for offer:', parsed.offer);
539
+ this.eventEmitter.emit(exports.OfferEvent.OFFER_SURFACED, {
540
+ offer: parsed.offer,
541
+ });
542
+ }
543
+ break;
544
+ default:
545
+ this.logger.log('Unknown message type:', type);
546
+ }
547
+ }
548
+ catch (error) {
549
+ this.logger.log('Error parsing SSE message:', error);
550
+ this.eventEmitter.emit(exports.OfferEvent.ERROR, {
551
+ error: error,
552
+ context: 'sse_message_parse'
553
+ });
554
+ }
555
+ }
556
+ handleReconnect() {
557
+ if (!this.config.reconnect) {
558
+ this.disconnect();
559
+ return;
560
+ }
561
+ if (this.reconnectAttempts >= (this.config.maxReconnectAttempts || 5)) {
562
+ this.logger.log('Max reconnection attempts reached');
563
+ this.setConnectionState(exports.ConnectionState.DISCONNECTED);
564
+ this.disconnect();
565
+ this.eventEmitter.emit(exports.OfferEvent.DISCONNECTED, {
566
+ reason: 'max_reconnect_attempts',
567
+ timestamp: new Date()
568
+ });
569
+ return;
570
+ }
571
+ this.reconnectAttempts++;
572
+ // Use server-suggested retry time if available, otherwise use config delay with exponential backoff
573
+ const baseDelay = this.serverSuggestedRetryTime || this.config.reconnectDelay || 1000;
574
+ const backoffDelay = baseDelay * Math.pow(2, this.reconnectAttempts - 1);
575
+ this.setConnectionState(exports.ConnectionState.RECONNECTING);
576
+ this.logger.log(`Reconnecting in ${backoffDelay}ms (attempt ${this.reconnectAttempts}`);
577
+ this.reconnectTimeout = window.setTimeout(() => {
578
+ this.eventSource = null;
579
+ this.connect().catch((error) => {
580
+ this.logger.log('Reconnection failed:', error);
581
+ });
582
+ }, backoffDelay);
583
+ }
584
+ disconnect() {
585
+ if (this.reconnectTimeout) {
586
+ clearTimeout(this.reconnectTimeout);
587
+ this.reconnectTimeout = null;
588
+ }
589
+ if (this.eventSource) {
590
+ this.logger.log('Closing SSE connection');
591
+ this.eventSource.close();
592
+ this.eventSource = null;
593
+ this.setConnectionState(exports.ConnectionState.DISCONNECTED);
594
+ this.eventEmitter.emit(exports.OfferEvent.DISCONNECTED, {
595
+ reason: 'manual',
596
+ timestamp: new Date()
597
+ });
598
+ }
599
+ this.isConnecting = false;
600
+ this.reconnectAttempts = 0;
601
+ }
602
+ isConnected() {
603
+ return this.eventSource?.getReadyState() === ReadyState.OPEN;
604
+ }
605
+ }
606
+
607
+ class OfferStore {
608
+ constructor(config) {
609
+ this.offers = new Map();
610
+ this.playerSnapshot = null;
611
+ this.logger = createLogger(config, 'OfferStore');
612
+ }
613
+ getSnapshot() {
614
+ return this.playerSnapshot;
615
+ }
616
+ setSnapshot(snapshot) {
617
+ this.playerSnapshot = snapshot;
618
+ this.logger.log('Updated player snapshot:', snapshot);
619
+ }
620
+ /**
621
+ * Set all offers (replaces existing)
622
+ */
623
+ setOffers(offers) {
624
+ this.offers.clear();
625
+ offers.forEach(offer => {
626
+ this.offers.set(offer.instanceId, offer);
627
+ });
628
+ this.logger.log(`Set ${offers.length} offers`);
629
+ }
630
+ /**
631
+ * Add or update a single offer
632
+ */
633
+ upsertOffer(offer) {
634
+ const previousOffer = this.offers.get(offer.instanceId);
635
+ this.offers.set(offer.instanceId, offer);
636
+ this.logger.log(`${previousOffer ? 'Updated' : 'Added'} offer:`, offer.instanceId);
637
+ return previousOffer;
638
+ }
639
+ /**
640
+ * Remove an offer
641
+ */
642
+ removeOffer(offerId) {
643
+ const removed = this.offers.delete(offerId);
644
+ if (removed) {
645
+ this.logger.log(`Removed offer:`, offerId);
646
+ }
647
+ return removed;
648
+ }
649
+ /**
650
+ * Get a single offer
651
+ */
652
+ getOffer(offerId) {
653
+ return this.offers.get(offerId);
654
+ }
655
+ /**
656
+ * Get all offers
657
+ */
658
+ getAllOffers() {
659
+ return Array.from(this.offers.values());
660
+ }
661
+ /**
662
+ * Get offers filtered by status
663
+ */
664
+ getOffersByStatus(status) {
665
+ return this.getAllOffers().filter(offer => offer.status === status);
666
+ }
667
+ /**
668
+ * Get active offers (not expired, not claimed)
669
+ */
670
+ getActiveOffers() {
671
+ return this.getAllOffers().filter(offer => {
672
+ if (!offer.status)
673
+ return false; // Must have a status
674
+ return offer.status === 'surfaced' || offer.status === 'viewed' || offer.status === 'claimable';
675
+ });
676
+ }
677
+ /**
678
+ * Get claimable offers
679
+ */
680
+ getClaimableOffers() {
681
+ return this.getOffersByStatus('claimable');
682
+ }
683
+ /**
684
+ * Check if an offer has expired
685
+ */
686
+ isOfferExpired(offerId) {
687
+ const offer = this.getOffer(offerId);
688
+ if (!offer)
689
+ return true;
690
+ // Check status
691
+ if (offer.status === 'expired')
692
+ return true;
693
+ // Check expiry date
694
+ if (offer.expiresAt && new Date(offer.expiresAt) < new Date()) {
695
+ return true;
696
+ }
697
+ return false;
698
+ }
699
+ /**
700
+ * Clear all offers
701
+ */
702
+ clear() {
703
+ this.offers.clear();
704
+ this.logger.log('Cleared all offers');
705
+ }
706
+ /**
707
+ * Get offer count
708
+ */
709
+ get size() {
710
+ return this.offers.size;
711
+ }
712
+ }
713
+
714
+ /**
715
+ * Manages JWT tokens for the offerwall client
716
+ * Handles token storage, validation, and refresh logic
717
+ */
718
+ class TokenManager {
719
+ constructor(config) {
720
+ this.config = config;
721
+ this.currentToken = null;
722
+ this.logger = createLogger(config, 'TokenManager');
723
+ }
724
+ /**
725
+ * Get the current stored token
726
+ */
727
+ getCurrentToken() {
728
+ return this.currentToken;
729
+ }
730
+ /**
731
+ * Clear the stored token
732
+ */
733
+ clearToken() {
734
+ this.currentToken = null;
735
+ }
736
+ /**
737
+ * Get current token or fetch a new one if needed
738
+ */
739
+ async getTokenForConnection() {
740
+ if (!this.currentToken)
741
+ return this.fetchToken();
742
+ if (!this.validateToken(this.currentToken))
743
+ return this.fetchToken();
744
+ return this.currentToken;
745
+ }
746
+ async fetchToken() {
747
+ const token = await this.config.tokenProvider();
748
+ this.currentToken = token;
749
+ return token;
750
+ }
751
+ validateToken(token) {
752
+ try {
753
+ const { exp } = this.parseJWT(token);
754
+ const now = Date.now();
755
+ const expiryTime = exp * 1000;
756
+ var isValid = expiryTime > now + 60000; // valid if more than 1 minute left
757
+ return isValid;
758
+ }
759
+ catch (error) {
760
+ this.logger.log(`Failed to validate token: ${error}`);
761
+ return false;
762
+ }
763
+ }
764
+ parseJWT(token) {
765
+ try {
766
+ const parts = token.split(".");
767
+ if (parts.length !== 3)
768
+ throw new Error("Invalid JWT format");
769
+ const payload = JSON.parse(atob(parts[1]));
770
+ // Validate required claims
771
+ if (!payload.sub || typeof payload.sub !== "string") {
772
+ throw new Error("Invalid sub claim");
773
+ }
774
+ if (!payload.iss || typeof payload.iss !== "string") {
775
+ throw new Error("Invalid iss claim");
776
+ }
777
+ if (!payload.exp || typeof payload.exp !== "number") {
778
+ throw new Error("Invalid exp claim");
779
+ }
780
+ if (!payload.iat || typeof payload.iat !== "number") {
781
+ throw new Error("Invalid iat claim");
782
+ }
783
+ // discard unknown claims
784
+ return {
785
+ sub: payload.sub,
786
+ iss: payload.iss,
787
+ exp: payload.exp,
788
+ iat: payload.iat,
789
+ };
790
+ }
791
+ catch (error) {
792
+ throw new Error(`Failed to parse JWT: ${error}`);
793
+ }
794
+ }
795
+ }
796
+
797
+ class AssetHelper {
798
+ constructor(config) {
799
+ this.config = config;
800
+ }
801
+ resolveReward(reward) {
802
+ // Determine the ID to translate based on reward kind
803
+ let id;
804
+ switch (reward.kind) {
805
+ case 'item':
806
+ id = reward.itemId;
807
+ break;
808
+ case 'coins':
809
+ id = reward.currencyId;
810
+ break;
811
+ case 'exp':
812
+ id = reward.skillId;
813
+ break;
814
+ case 'on_chain':
815
+ id = reward.assetId;
816
+ break;
817
+ case 'trust_points':
818
+ id = 'trust_points';
819
+ break;
820
+ }
821
+ // Fallback to reward's built-in name, we should add 5x popberry etc
822
+ const formattedName = reward.amount > 1 ? `${reward.amount}x ${reward.name}` : reward.name;
823
+ // Try to resolve asset content if ID exists and resolver is set
824
+ if (id && this.config.assetResolver) {
825
+ const content = this.config.assetResolver(reward, id);
826
+ if (content) {
827
+ return {
828
+ name: content.name || formattedName,
829
+ image: content.image || this.config.fallbackRewardImage
830
+ };
831
+ }
832
+ }
833
+ return {
834
+ name: formattedName,
835
+ image: this.config.fallbackRewardImage
836
+ };
837
+ }
838
+ resolveOfferRewards(offer) {
839
+ return offer.rewards.map(reward => ({
840
+ ...reward,
841
+ ...this.resolveReward(reward),
842
+ }));
843
+ }
844
+ }
845
+
846
+ class OfferwallClient {
847
+ constructor(config) {
848
+ this.isInitializing = false;
849
+ this.config = {
850
+ endpoint: config.endpoint || 'https://api.buildon.pixels.xyz',
851
+ autoConnect: config.autoConnect ?? false,
852
+ reconnect: config.reconnect ?? true,
853
+ reconnectDelay: config.reconnectDelay ?? 1000,
854
+ maxReconnectAttempts: config.maxReconnectAttempts ?? 5,
855
+ debug: config.debug ?? false,
856
+ ...config
857
+ };
858
+ this.hooks = this.config.hooks || {};
859
+ this.logger = createLogger(this.config, 'OfferwallClient');
860
+ this.eventEmitter = new EventEmitter(this.config);
861
+ this.offerStore = new OfferStore(this.config);
862
+ this.tokenManager = new TokenManager(this.config);
863
+ this.assetHelper = new AssetHelper(this.config);
864
+ this.sseConnection = new SSEConnection(this.config, this.eventEmitter, this.tokenManager);
865
+ this.setupInternalListeners();
866
+ if (this.config.autoConnect) {
867
+ this.initialize().catch(err => {
868
+ this.logger.error('Auto-initialization failed:', err);
869
+ });
870
+ }
871
+ }
872
+ /**
873
+ * Get the offer store instance
874
+ */
875
+ get store() {
876
+ return this.offerStore;
877
+ }
878
+ /**
879
+ * Get the event emitter instance for event handling
880
+ */
881
+ get events() {
882
+ return this.eventEmitter;
883
+ }
884
+ /**
885
+ * Get asset helper for resolving offer/reward content
886
+ */
887
+ get assets() {
888
+ return this.assetHelper;
889
+ }
890
+ /**
891
+ * Initialize the offerwall client and connect
892
+ */
893
+ async initialize() {
894
+ try {
895
+ if (this.isInitializing) {
896
+ this.logger.log('Initialization already in progress');
897
+ return;
898
+ }
899
+ this.isInitializing = true;
900
+ await this.refreshOffersAndSnapshot();
901
+ await this.connect();
902
+ this.isInitializing = false;
903
+ }
904
+ catch (error) {
905
+ this.handleError(error, 'initialize');
906
+ throw error;
907
+ }
908
+ }
909
+ /**
910
+ * Connect to the offerwall SSE endpoint
911
+ */
912
+ async connect() {
913
+ if (this.sseConnection?.isConnected()) {
914
+ this.logger.log('Already connected');
915
+ return;
916
+ }
917
+ const token = await this.tokenManager.getTokenForConnection();
918
+ if (this.hooks.beforeConnect) {
919
+ await this.hooks.beforeConnect({ jwt: token, endpoint: this.config.endpoint });
920
+ }
921
+ try {
922
+ await this.sseConnection.connect();
923
+ if (this.hooks.afterConnect) {
924
+ await this.hooks.afterConnect();
925
+ }
926
+ }
927
+ catch (error) {
928
+ this.handleError(error, 'connection');
929
+ throw error;
930
+ }
931
+ }
932
+ /**
933
+ * Disconnect from the offerwall
934
+ */
935
+ async disconnect() {
936
+ if (this.hooks.beforeDisconnect) {
937
+ await this.hooks.beforeDisconnect();
938
+ }
939
+ if (this.sseConnection) {
940
+ this.sseConnection.disconnect();
941
+ }
942
+ this.offerStore.clear();
943
+ this.tokenManager.clearToken();
944
+ if (this.hooks.afterDisconnect) {
945
+ await this.hooks.afterDisconnect();
946
+ }
947
+ }
948
+ /**
949
+ * Claim rewards for an offer
950
+ */
951
+ async claimReward(instanceId) {
952
+ const offer = this.offerStore.getOffer(instanceId);
953
+ if (!offer) {
954
+ throw new Error(`Offer ${instanceId} not found`);
955
+ }
956
+ if (offer.status !== 'claimable') {
957
+ throw new Error(`Offer ${instanceId} is not claimable (status: ${offer.status})`);
958
+ }
959
+ if (this.hooks.beforeOfferClaim) {
960
+ const canClaim = await this.hooks.beforeOfferClaim(offer);
961
+ if (!canClaim) {
962
+ this.logger.log('Claim cancelled by beforeOfferClaim hook');
963
+ return;
964
+ }
965
+ }
966
+ try {
967
+ const response = await this.claimOfferAPI(instanceId);
968
+ const updatedOffer = { ...offer, status: 'claimed', };
969
+ this.offerStore.upsertOffer(updatedOffer);
970
+ this.eventEmitter.emit(exports.OfferEvent.OFFER_CLAIMED, {
971
+ instanceId,
972
+ });
973
+ if (this.hooks.afterOfferClaim) {
974
+ await this.hooks.afterOfferClaim(updatedOffer, offer.rewards);
975
+ }
976
+ return response;
977
+ }
978
+ catch (error) {
979
+ this.handleError(error, 'claim');
980
+ throw error;
981
+ }
982
+ }
983
+ /**
984
+ * Check if connected
985
+ */
986
+ isConnected() {
987
+ return this.sseConnection?.isConnected() || false;
988
+ }
989
+ /**
990
+ * Get current connection state
991
+ */
992
+ getConnectionState() {
993
+ return this.sseConnection?.getConnectionState() || exports.ConnectionState.DISCONNECTED;
994
+ }
995
+ setupInternalListeners() {
996
+ /*
997
+ this.eventEmitter.on(OfferEvent.INIT, ({ offers, playerSnapshot }) => {
998
+ this.offerStore.setOffers(offers);
999
+ this.offerStore.setSnapshot(playerSnapshot);
1000
+ this.logger.log(`Loaded ${offers.length} offers`);
1001
+ });
1002
+
1003
+ this.eventEmitter.on(OfferEvent.OFFER_ADDED, ({ offer }) => {
1004
+ this.offerStore.upsertOffer(offer);
1005
+ this.logger.log(`Added offer: ${offer.instanceId}`);
1006
+ });
1007
+
1008
+ this.eventEmitter.on(OfferEvent.OFFER_UPDATED, ({ offer }) => {
1009
+ this.offerStore.upsertOffer(offer);
1010
+ this.logger.log(`Updated offer: ${offer.instanceId}`);
1011
+ });
1012
+
1013
+ this.eventEmitter.on(OfferEvent.SNAPSHOT_UPDATED, ({ playerSnapshot }) => {
1014
+ this.offerStore.setSnapshot(playerSnapshot);
1015
+ this.logger.log(`Updated player snapshot`);
1016
+ });
1017
+
1018
+ this.eventEmitter.on(OfferEvent.ERROR, ({ error, context }) => {
1019
+ this.handleError(error, context);
1020
+ });
1021
+ */
1022
+ this.eventEmitter.on(exports.OfferEvent.OFFER_SURFACED, ({ offer }) => {
1023
+ this.offerStore.upsertOffer(offer);
1024
+ this.logger.log(`Surfaced offer: ${offer.instanceId}`);
1025
+ });
1026
+ }
1027
+ /**
1028
+ * Helper to POST to an endpoint with JWT and handle token expiry/retry logic
1029
+ */
1030
+ async postWithAuth(endpoint, body, retry = false) {
1031
+ const token = await this.tokenManager.getTokenForConnection();
1032
+ const response = await fetch(`${this.config.endpoint}${endpoint}`, {
1033
+ method: 'POST',
1034
+ headers: {
1035
+ 'Authorization': `Bearer ${token}`,
1036
+ 'Content-Type': 'application/json'
1037
+ },
1038
+ body: body ? JSON.stringify(body) : undefined,
1039
+ });
1040
+ if (!response.ok) {
1041
+ if (response.status === 400) {
1042
+ const errorData = await response.json();
1043
+ if ((errorData.message === 'jwt-expired' || errorData.message === 'jwt-invalid') &&
1044
+ !retry) {
1045
+ this.tokenManager.clearToken();
1046
+ return this.postWithAuth(endpoint, body, true);
1047
+ }
1048
+ }
1049
+ throw new Error(`Failed to POST to ${endpoint}: ${response.statusText}`);
1050
+ }
1051
+ return response.json();
1052
+ }
1053
+ async claimOfferAPI(instanceId) {
1054
+ return this.postWithAuth('/client/reward/claim', { instanceId, kind: 'offer' });
1055
+ }
1056
+ async refreshOffersAndSnapshot() {
1057
+ try {
1058
+ const { offers, snapshot } = await this.getOffersAndSnapshot();
1059
+ this.offerStore.setOffers(offers);
1060
+ this.offerStore.setSnapshot(snapshot);
1061
+ this.eventEmitter.emit(exports.OfferEvent.REFRESH, { offers, playerSnapshot: snapshot });
1062
+ this.logger.log('Refreshed offers and player snapshot');
1063
+ }
1064
+ catch (error) {
1065
+ this.handleError(error, 'refreshOffersAndSnapshot');
1066
+ throw error;
1067
+ }
1068
+ }
1069
+ async getOffersAndSnapshot() {
1070
+ const data = await this.postWithAuth('/client/player/campaigns', { viewingCampaigns: true });
1071
+ if (!data.offers || !Array.isArray(data.offers)) {
1072
+ throw new Error('No offers returned from offers endpoint');
1073
+ }
1074
+ if (!data.snapshot) {
1075
+ throw new Error('No player snapshot returned from offers endpoint');
1076
+ }
1077
+ return data;
1078
+ }
1079
+ async getAuthLinkToken() {
1080
+ const data = await this.postWithAuth('/auth/one_time_token/generate');
1081
+ if (!data.token) {
1082
+ throw new Error('No token returned from auth link endpoint');
1083
+ }
1084
+ return data.token;
1085
+ }
1086
+ handleError(error, context) {
1087
+ this.logger.error(`Error in ${context}:`, error);
1088
+ if (this.hooks.onError) {
1089
+ this.hooks.onError(error, context);
1090
+ }
1091
+ }
1092
+ }
1093
+
1094
+ const meetsBaseConditions = ({ conditions, playerSnap, addDetails, }) => {
1095
+ const conditionData = [];
1096
+ let isValid = true;
1097
+ if (conditions?.minDaysInGame) {
1098
+ const isDisqualify = (playerSnap.daysInGame || 0) < conditions.minDaysInGame;
1099
+ if (addDetails) {
1100
+ conditionData.push({
1101
+ isMet: !isDisqualify,
1102
+ kind: 'minDaysInGame',
1103
+ trackerAmount: playerSnap.daysInGame || 0,
1104
+ text: `More than ${conditions.minDaysInGame} Days in Game`,
1105
+ });
1106
+ if (isDisqualify)
1107
+ isValid = false;
1108
+ }
1109
+ else {
1110
+ if (isDisqualify)
1111
+ return { isValid: false };
1112
+ }
1113
+ }
1114
+ if (conditions?.minTrustScore) {
1115
+ const isDisqualify = (playerSnap.trustScore || 0) < conditions.minTrustScore;
1116
+ if (addDetails) {
1117
+ conditionData.push({
1118
+ isMet: !isDisqualify,
1119
+ kind: 'minTrustScore',
1120
+ trackerAmount: playerSnap.trustScore || 0,
1121
+ text: `More than ${conditions.minTrustScore} Rep`,
1122
+ });
1123
+ if (isDisqualify)
1124
+ isValid = false;
1125
+ }
1126
+ else {
1127
+ if (isDisqualify)
1128
+ return { isValid: false };
1129
+ }
1130
+ }
1131
+ if (conditions?.maxTrustScore) {
1132
+ const isDisqualify = (playerSnap.trustScore || 0) > conditions.maxTrustScore;
1133
+ if (addDetails) {
1134
+ conditionData.push({
1135
+ isMet: !isDisqualify,
1136
+ kind: 'maxTrustScore',
1137
+ trackerAmount: playerSnap.trustScore || 0,
1138
+ text: `Less than ${conditions.maxTrustScore} Rep`,
1139
+ });
1140
+ if (isDisqualify)
1141
+ isValid = false;
1142
+ }
1143
+ else {
1144
+ if (isDisqualify)
1145
+ return { isValid: false };
1146
+ }
1147
+ }
1148
+ for (const key in conditions?.achievements) {
1149
+ const a = conditions.achievements[key];
1150
+ const playerAchData = playerSnap.achievements?.[key];
1151
+ if (!playerAchData) {
1152
+ const isDisqualify = true;
1153
+ if (addDetails) {
1154
+ conditionData.push({
1155
+ isMet: isDisqualify,
1156
+ kind: 'achievements',
1157
+ trackerAmount: 0,
1158
+ text: `Have the achievement ${a.name}`,
1159
+ });
1160
+ isValid = false;
1161
+ }
1162
+ else {
1163
+ return { isValid: false };
1164
+ }
1165
+ }
1166
+ if (a.minCount) {
1167
+ const isDisqualify = (playerAchData?.count || 1) < a.minCount;
1168
+ if (addDetails) {
1169
+ conditionData.push({
1170
+ isMet: !isDisqualify,
1171
+ kind: 'achievements',
1172
+ trackerAmount: playerAchData?.count || 0,
1173
+ text: `Have the achievement ${a.name} more than ${a.minCount} times`,
1174
+ });
1175
+ if (isDisqualify)
1176
+ isValid = false;
1177
+ return { isValid: false };
1178
+ }
1179
+ else {
1180
+ if (isDisqualify)
1181
+ return { isValid: false };
1182
+ }
1183
+ }
1184
+ }
1185
+ for (const key in conditions?.currencies) {
1186
+ const c = conditions.currencies[key];
1187
+ const playerCurrencyData = playerSnap.currencies?.[key];
1188
+ if (c.min) {
1189
+ const isDisqualify = (playerCurrencyData?.balance || 0) < c.min;
1190
+ if (addDetails) {
1191
+ conditionData.push({
1192
+ isMet: !isDisqualify,
1193
+ kind: 'currencies',
1194
+ trackerAmount: playerCurrencyData?.balance || 0,
1195
+ text: `Have more than ${c.min} ${c.name}`,
1196
+ });
1197
+ if (isDisqualify)
1198
+ isValid = false;
1199
+ }
1200
+ else {
1201
+ if (isDisqualify)
1202
+ return { isValid: false };
1203
+ }
1204
+ }
1205
+ if (c.max) {
1206
+ const isDisqualify = (playerCurrencyData?.balance || 0) > c.max;
1207
+ if (addDetails) {
1208
+ conditionData.push({
1209
+ isMet: !isDisqualify,
1210
+ kind: 'currencies',
1211
+ trackerAmount: playerCurrencyData?.balance || 0,
1212
+ text: `Have less than ${c.max} ${c.name}`,
1213
+ });
1214
+ if (isDisqualify)
1215
+ isValid = false;
1216
+ }
1217
+ else {
1218
+ if (isDisqualify)
1219
+ return { isValid: false };
1220
+ }
1221
+ }
1222
+ if (c.in) {
1223
+ const isDisqualify = (playerCurrencyData?.in || 0) < c.in;
1224
+ if (addDetails) {
1225
+ conditionData.push({
1226
+ isMet: !isDisqualify,
1227
+ kind: 'currencies',
1228
+ trackerAmount: playerCurrencyData?.in || 0,
1229
+ text: `Deposit at least ${c.in} ${c.name}`,
1230
+ });
1231
+ if (isDisqualify)
1232
+ isValid = false;
1233
+ }
1234
+ else {
1235
+ if (isDisqualify)
1236
+ return { isValid: false };
1237
+ }
1238
+ }
1239
+ if (c.out) {
1240
+ const isDisqualify = (playerCurrencyData?.out || 0) < c.out;
1241
+ if (addDetails) {
1242
+ conditionData.push({
1243
+ isMet: !isDisqualify,
1244
+ kind: 'currencies',
1245
+ trackerAmount: playerCurrencyData?.out || 0,
1246
+ text: `Withdraw at least ${c.out} ${c.name}`,
1247
+ });
1248
+ if (isDisqualify)
1249
+ isValid = false;
1250
+ }
1251
+ else {
1252
+ if (isDisqualify)
1253
+ return { isValid: false };
1254
+ }
1255
+ }
1256
+ }
1257
+ for (const key in conditions?.levels) {
1258
+ const l = conditions.levels[key];
1259
+ const playerLevelData = playerSnap.levels?.[key];
1260
+ if (l.min) {
1261
+ const isDisqualify = (playerLevelData?.level || 0) < l.min;
1262
+ if (addDetails) {
1263
+ conditionData.push({
1264
+ isMet: !isDisqualify,
1265
+ kind: 'levels',
1266
+ trackerAmount: playerLevelData?.level || 0,
1267
+ text: `Be above level ${l.min} ${l.name}`,
1268
+ });
1269
+ if (isDisqualify)
1270
+ isValid = false;
1271
+ }
1272
+ else {
1273
+ if (isDisqualify)
1274
+ return { isValid: false };
1275
+ }
1276
+ }
1277
+ if (l.max) {
1278
+ const isDisqualify = (playerLevelData?.level || 0) > l.max;
1279
+ if (addDetails) {
1280
+ conditionData.push({
1281
+ isMet: !isDisqualify,
1282
+ kind: 'levels',
1283
+ trackerAmount: playerLevelData?.level || 0,
1284
+ text: `Be under level ${l.max} ${l.name}`,
1285
+ });
1286
+ if (isDisqualify)
1287
+ isValid = false;
1288
+ }
1289
+ else {
1290
+ if (isDisqualify)
1291
+ return { isValid: false };
1292
+ }
1293
+ }
1294
+ }
1295
+ if (conditions?.quests) {
1296
+ for (const questId in conditions.quests) {
1297
+ const quest = conditions.quests[questId];
1298
+ const playerQuestData = playerSnap.quests?.[questId];
1299
+ const isDisqualify = playerQuestData
1300
+ ? (playerQuestData?.completions || 0) < (quest.completions || 0)
1301
+ : false;
1302
+ if (addDetails) {
1303
+ conditionData.push({
1304
+ isMet: !isDisqualify,
1305
+ kind: 'quests',
1306
+ trackerAmount: playerQuestData?.completions || 0,
1307
+ text: quest.completions === 1
1308
+ ? `Complete the quest ${quest.name}`
1309
+ : (quest.completions || 0) < 1
1310
+ ? `Start the quest ${quest.name}`
1311
+ : `Complete the quest ${quest.name} ${quest.completions} times`,
1312
+ });
1313
+ if (isDisqualify)
1314
+ isValid = false;
1315
+ }
1316
+ else {
1317
+ if (isDisqualify)
1318
+ return { isValid: false };
1319
+ }
1320
+ }
1321
+ }
1322
+ for (const key in conditions?.memberships) {
1323
+ const m = conditions.memberships[key];
1324
+ const playerMembershipsData = playerSnap.memberships?.[key];
1325
+ if (m.minCount) {
1326
+ const isDisqualify = (playerMembershipsData?.count || 0) < m.minCount;
1327
+ if (addDetails) {
1328
+ conditionData.push({
1329
+ isMet: !isDisqualify,
1330
+ kind: 'memberships',
1331
+ trackerAmount: playerMembershipsData?.count || 0,
1332
+ text: m.minCount > 1
1333
+ ? `Have at least ${m.minCount} ${m.name} memberships`
1334
+ : `Have a ${m.name} membership`,
1335
+ });
1336
+ if (isDisqualify)
1337
+ isValid = false;
1338
+ }
1339
+ else {
1340
+ if (isDisqualify)
1341
+ return { isValid: false };
1342
+ }
1343
+ }
1344
+ if (m.maxCount) {
1345
+ const isDisqualify = (playerMembershipsData?.count || 0) > m.maxCount;
1346
+ if (addDetails) {
1347
+ conditionData.push({
1348
+ isMet: !isDisqualify,
1349
+ kind: 'memberships',
1350
+ trackerAmount: playerMembershipsData?.count || 0,
1351
+ text: `Have less than ${m.maxCount} ${m.name} memberships`,
1352
+ });
1353
+ if (isDisqualify)
1354
+ isValid = false;
1355
+ }
1356
+ else {
1357
+ if (isDisqualify)
1358
+ return { isValid: false };
1359
+ }
1360
+ }
1361
+ const timeOwned = (playerMembershipsData?.expiresAt || 0) - Date.now();
1362
+ if (m.minMs) {
1363
+ const isDisqualify = timeOwned < m.minMs;
1364
+ if (addDetails) {
1365
+ conditionData.push({
1366
+ isMet: !isDisqualify,
1367
+ kind: 'memberships',
1368
+ trackerAmount: Number((timeOwned / (1000 * 60 * 60 * 24)).toFixed(1)),
1369
+ text: `Own ${m.name} membership for at least ${(m.minMs /
1370
+ (1000 * 60 * 60 * 24)).toFixed(1)} days`,
1371
+ });
1372
+ if (isDisqualify)
1373
+ isValid = false;
1374
+ }
1375
+ else {
1376
+ if (isDisqualify)
1377
+ return { isValid: false };
1378
+ }
1379
+ }
1380
+ if (m.maxMs) {
1381
+ const isDisqualify = timeOwned > m.maxMs;
1382
+ if (addDetails) {
1383
+ conditionData.push({
1384
+ isMet: !isDisqualify,
1385
+ kind: 'memberships',
1386
+ trackerAmount: Number((timeOwned / (1000 * 60 * 60 * 24)).toFixed(1)),
1387
+ text: `Own ${m.name} membership for less than ${(m.maxMs /
1388
+ (1000 * 60 * 60 * 24)).toFixed(1)} days`,
1389
+ });
1390
+ if (isDisqualify)
1391
+ isValid = false;
1392
+ }
1393
+ else {
1394
+ if (isDisqualify)
1395
+ return { isValid: false };
1396
+ }
1397
+ }
1398
+ }
1399
+ for (const key in conditions?.stakedTokens) {
1400
+ const s = conditions.stakedTokens[key];
1401
+ const playerStakedData = playerSnap.stakedTokens?.[key];
1402
+ if (s.min) {
1403
+ const isDisqualify = (playerStakedData?.balance || 0) < s.min;
1404
+ if (addDetails) {
1405
+ conditionData.push({
1406
+ isMet: !isDisqualify,
1407
+ kind: 'stakedTokens',
1408
+ trackerAmount: playerStakedData?.balance || 0,
1409
+ text: `Have at least ${s.min} ${s.name} staked`,
1410
+ });
1411
+ if (isDisqualify)
1412
+ isValid = false;
1413
+ }
1414
+ else {
1415
+ if (isDisqualify)
1416
+ return { isValid: false };
1417
+ }
1418
+ }
1419
+ }
1420
+ return { isValid, conditionData: addDetails ? conditionData : undefined };
1421
+ };
1422
+ const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, addDetails = false, }) => {
1423
+ if (completionConditions) {
1424
+ const conditions = completionConditions;
1425
+ const conditionData = [];
1426
+ let isValid = true;
1427
+ if (conditions.buyItem) {
1428
+ const isDisqualify = (completionTrackers?.buyItem || 0) < (conditions.buyItem.amount || 1);
1429
+ if (addDetails) {
1430
+ conditionData.push({
1431
+ isMet: !isDisqualify,
1432
+ kind: 'buyItem',
1433
+ trackerAmount: completionTrackers?.buyItem || 0,
1434
+ text: `Buy ${conditions.buyItem.amount || 1} ${conditions.buyItem.name}`,
1435
+ });
1436
+ if (isDisqualify)
1437
+ isValid = false;
1438
+ }
1439
+ else {
1440
+ if (isDisqualify)
1441
+ return { isValid: false };
1442
+ }
1443
+ }
1444
+ if (conditions.spendCurrency) {
1445
+ const isDisqualify = (completionTrackers?.spendCurrency || 0) < (conditions.spendCurrency.amount || 1);
1446
+ if (addDetails) {
1447
+ conditionData.push({
1448
+ isMet: !isDisqualify,
1449
+ kind: 'spendCurrency',
1450
+ trackerAmount: completionTrackers?.spendCurrency || 0,
1451
+ text: `Spend ${conditions.spendCurrency.amount || 1} ${conditions.spendCurrency.name}`,
1452
+ });
1453
+ if (isDisqualify)
1454
+ isValid = false;
1455
+ }
1456
+ else {
1457
+ if (isDisqualify)
1458
+ return { isValid: false };
1459
+ }
1460
+ }
1461
+ if (conditions.depositCurrency) {
1462
+ const isDisqualify = (completionTrackers?.depositCurrency || 0) <
1463
+ (conditions.depositCurrency.amount || 1);
1464
+ if (addDetails) {
1465
+ conditionData.push({
1466
+ isMet: !isDisqualify,
1467
+ kind: 'depositCurrency',
1468
+ trackerAmount: completionTrackers?.depositCurrency || 0,
1469
+ text: `Deposit ${conditions.depositCurrency.amount || 1} ${conditions.depositCurrency.name}`,
1470
+ });
1471
+ }
1472
+ else {
1473
+ if (isDisqualify)
1474
+ return { isValid: false };
1475
+ }
1476
+ }
1477
+ if (conditions.login) {
1478
+ const isMet = completionTrackers?.login || false;
1479
+ if (addDetails) {
1480
+ conditionData.push({
1481
+ isMet,
1482
+ kind: 'login',
1483
+ trackerAmount: completionTrackers?.login ? 1 : 0,
1484
+ text: `Login to the game`,
1485
+ });
1486
+ isValid = isMet;
1487
+ }
1488
+ else {
1489
+ if (!isMet)
1490
+ return { isValid: false };
1491
+ }
1492
+ }
1493
+ if (conditions.loginStreak) {
1494
+ // player's login streak snapshot right now - their login streak when offer was surfaced = their login streak since the offer was surfaced
1495
+ // if their login streak since the offer was surfaced is less than the required login streak, then they are not yet eligible for the offer
1496
+ const streakSinceOffer = (playerSnap.loginStreak || 0) - (completionTrackers?.currentLoginStreak || 0);
1497
+ const isDisqualify = streakSinceOffer + 1 < conditions.loginStreak;
1498
+ if (addDetails) {
1499
+ conditionData.push({
1500
+ isMet: !isDisqualify,
1501
+ kind: 'loginStreak',
1502
+ trackerAmount: (playerSnap.loginStreak || 0) - (completionTrackers?.currentLoginStreak || 0),
1503
+ text: `Login streak of ${conditions.loginStreak || 0} days`,
1504
+ });
1505
+ if (isDisqualify)
1506
+ isValid = false;
1507
+ }
1508
+ else {
1509
+ if (isDisqualify)
1510
+ return { isValid: false };
1511
+ }
1512
+ }
1513
+ const r = meetsBaseConditions({
1514
+ conditions,
1515
+ playerSnap,
1516
+ addDetails: true,
1517
+ });
1518
+ isValid = r.isValid;
1519
+ conditionData.push(...(r.conditionData || []));
1520
+ return { isValid, conditionData };
1521
+ }
1522
+ return { isValid: true, conditionData: [] };
1523
+ };
1524
+
1525
+ // Taken from buildon_server/src/commons/types/offer.ts and merged into a single interface
1526
+ const PlayerOfferStatuses = [
1527
+ 'inQueue',
1528
+ 'surfaced',
1529
+ 'viewed',
1530
+ 'claimable',
1531
+ 'claimed',
1532
+ 'expired',
1533
+ ];
1534
+
1535
+ // Taken from buildon_server/src/commons/types/reward.ts
1536
+ // Use a const assertion for the array and infer the union type directly
1537
+ const rewardKinds = [
1538
+ 'item',
1539
+ 'coins',
1540
+ 'exp',
1541
+ 'trust_points',
1542
+ /** on-chain rewards require the builder to send funds to a custodial wallet that we use to send to player wallets*/
1543
+ 'on_chain',
1544
+ ];
1545
+
1546
+ exports.AssetHelper = AssetHelper;
1547
+ exports.EventEmitter = EventEmitter;
1548
+ exports.OfferStore = OfferStore;
1549
+ exports.OfferwallClient = OfferwallClient;
1550
+ exports.PlayerOfferStatuses = PlayerOfferStatuses;
1551
+ exports.SSEConnection = SSEConnection;
1552
+ exports.meetsBaseConditions = meetsBaseConditions;
1553
+ exports.meetsCompletionConditions = meetsCompletionConditions;
1554
+ exports.rewardKinds = rewardKinds;
1555
+
1556
+ }));
1557
+ //# sourceMappingURL=offerwall-sdk.umd.js.map