@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.
- package/LICENSE.md +675 -0
- package/README.md +1 -0
- package/dist/core/CustomEventSource.d.ts +42 -0
- package/dist/core/OfferStore.d.ts +55 -0
- package/dist/core/OfferwallClient.d.ts +63 -0
- package/dist/core/SSEConnection.d.ts +30 -0
- package/dist/core/TokenManager.d.ts +33 -0
- package/dist/events/EventEmitter.d.ts +13 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.esm.js +1541 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +1551 -0
- package/dist/index.js.map +1 -0
- package/dist/offerwall-sdk.umd.js +1557 -0
- package/dist/offerwall-sdk.umd.js.map +1 -0
- package/dist/types/connection.d.ts +20 -0
- package/dist/types/events.d.ts +42 -0
- package/dist/types/hooks.d.ts +21 -0
- package/dist/types/index.d.ts +23 -0
- package/dist/types/offer.d.ts +142 -0
- package/dist/types/player.d.ts +185 -0
- package/dist/types/reward.d.ts +22 -0
- package/dist/utils/Logger.d.ts +18 -0
- package/dist/utils/assets.d.ts +9 -0
- package/dist/utils/conditions.d.ts +34 -0
- package/package.json +76 -0
|
@@ -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
|