react-achievements 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +186 -1
- package/dist/index.d.ts +209 -56
- package/dist/index.js +681 -14
- package/dist/index.js.map +1 -1
- package/dist/types/core/errors/AchievementErrors.d.ts +7 -3
- package/dist/types/core/storage/AsyncStorageAdapter.d.ts +48 -0
- package/dist/types/core/storage/IndexedDBStorage.d.ts +29 -0
- package/dist/types/core/storage/OfflineQueueStorage.d.ts +42 -0
- package/dist/types/core/storage/RestApiStorage.d.ts +20 -0
- package/dist/types/core/types.d.ts +13 -2
- package/dist/types/index.d.ts +6 -1
- package/dist/types/providers/AchievementProvider.d.ts +4 -2
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -8,10 +8,18 @@ import 'react-toastify/dist/ReactToastify.css';
|
|
|
8
8
|
const isDate = (value) => {
|
|
9
9
|
return value instanceof Date;
|
|
10
10
|
};
|
|
11
|
+
// Type guard to detect async storage
|
|
12
|
+
function isAsyncStorage(storage) {
|
|
13
|
+
// Check if methods return Promises
|
|
14
|
+
const testResult = storage.getMetrics();
|
|
15
|
+
return testResult && typeof testResult.then === 'function';
|
|
16
|
+
}
|
|
11
17
|
var StorageType;
|
|
12
18
|
(function (StorageType) {
|
|
13
19
|
StorageType["Local"] = "local";
|
|
14
20
|
StorageType["Memory"] = "memory";
|
|
21
|
+
StorageType["IndexedDB"] = "indexeddb";
|
|
22
|
+
StorageType["RestAPI"] = "restapi"; // Asynchronous REST API storage
|
|
15
23
|
})(StorageType || (StorageType = {}));
|
|
16
24
|
|
|
17
25
|
/**
|
|
@@ -70,13 +78,15 @@ class ConfigurationError extends AchievementError {
|
|
|
70
78
|
}
|
|
71
79
|
}
|
|
72
80
|
/**
|
|
73
|
-
* Error thrown when sync operations fail
|
|
81
|
+
* Error thrown when network sync operations fail
|
|
74
82
|
*/
|
|
75
83
|
class SyncError extends AchievementError {
|
|
76
|
-
constructor(message,
|
|
77
|
-
super(message, 'SYNC_ERROR', true,
|
|
78
|
-
|
|
84
|
+
constructor(message, details) {
|
|
85
|
+
super(message, 'SYNC_ERROR', true, // recoverable (can retry)
|
|
86
|
+
'Check your network connection and try again. If the problem persists, achievements will sync when connection is restored.');
|
|
79
87
|
this.name = 'SyncError';
|
|
88
|
+
this.statusCode = details === null || details === void 0 ? void 0 : details.statusCode;
|
|
89
|
+
this.timeout = details === null || details === void 0 ? void 0 : details.timeout;
|
|
80
90
|
}
|
|
81
91
|
}
|
|
82
92
|
/**
|
|
@@ -216,6 +226,641 @@ class MemoryStorage {
|
|
|
216
226
|
}
|
|
217
227
|
}
|
|
218
228
|
|
|
229
|
+
/******************************************************************************
|
|
230
|
+
Copyright (c) Microsoft Corporation.
|
|
231
|
+
|
|
232
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
233
|
+
purpose with or without fee is hereby granted.
|
|
234
|
+
|
|
235
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
236
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
237
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
238
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
239
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
240
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
241
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
242
|
+
***************************************************************************** */
|
|
243
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
function __awaiter(thisArg, _arguments, P, generator) {
|
|
247
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
248
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
249
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
250
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
251
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
252
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
257
|
+
var e = new Error(message);
|
|
258
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
class AsyncStorageAdapter {
|
|
262
|
+
constructor(asyncStorage, options) {
|
|
263
|
+
this.pendingWrites = [];
|
|
264
|
+
this.asyncStorage = asyncStorage;
|
|
265
|
+
this.onError = options === null || options === void 0 ? void 0 : options.onError;
|
|
266
|
+
this.cache = {
|
|
267
|
+
metrics: {},
|
|
268
|
+
unlocked: [],
|
|
269
|
+
loaded: false
|
|
270
|
+
};
|
|
271
|
+
// Eagerly load data from async storage (non-blocking)
|
|
272
|
+
this.initializeCache();
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Initialize cache by loading from async storage
|
|
276
|
+
* This happens in the background during construction
|
|
277
|
+
*/
|
|
278
|
+
initializeCache() {
|
|
279
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
280
|
+
try {
|
|
281
|
+
const [metrics, unlocked] = yield Promise.all([
|
|
282
|
+
this.asyncStorage.getMetrics(),
|
|
283
|
+
this.asyncStorage.getUnlockedAchievements()
|
|
284
|
+
]);
|
|
285
|
+
this.cache.metrics = metrics;
|
|
286
|
+
this.cache.unlocked = unlocked;
|
|
287
|
+
this.cache.loaded = true;
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
// Handle initialization errors
|
|
291
|
+
console.error('Failed to initialize async storage:', error);
|
|
292
|
+
if (this.onError) {
|
|
293
|
+
const storageError = error instanceof AchievementError
|
|
294
|
+
? error
|
|
295
|
+
: new StorageError('Failed to initialize storage', error);
|
|
296
|
+
this.onError(storageError);
|
|
297
|
+
}
|
|
298
|
+
// Set to empty state on error
|
|
299
|
+
this.cache.loaded = true; // Mark as loaded even on error to prevent blocking
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Wait for cache to be loaded (used internally)
|
|
305
|
+
* Returns immediately if already loaded, otherwise waits
|
|
306
|
+
*/
|
|
307
|
+
ensureCacheLoaded() {
|
|
308
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
309
|
+
while (!this.cache.loaded) {
|
|
310
|
+
yield new Promise(resolve => setTimeout(resolve, 10));
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* SYNC READ: Returns cached metrics immediately
|
|
316
|
+
* Cache is loaded eagerly during construction
|
|
317
|
+
*/
|
|
318
|
+
getMetrics() {
|
|
319
|
+
return this.cache.metrics;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* SYNC WRITE: Updates cache immediately, writes to storage in background
|
|
323
|
+
* Uses optimistic updates - assumes write will succeed
|
|
324
|
+
*/
|
|
325
|
+
setMetrics(metrics) {
|
|
326
|
+
// Update cache immediately (optimistic update)
|
|
327
|
+
this.cache.metrics = metrics;
|
|
328
|
+
// Write to async storage in background
|
|
329
|
+
const writePromise = this.asyncStorage.setMetrics(metrics).catch(error => {
|
|
330
|
+
console.error('Failed to write metrics to async storage:', error);
|
|
331
|
+
if (this.onError) {
|
|
332
|
+
const storageError = error instanceof AchievementError
|
|
333
|
+
? error
|
|
334
|
+
: new StorageError('Failed to write metrics', error);
|
|
335
|
+
this.onError(storageError);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
// Track pending write for cleanup/testing
|
|
339
|
+
this.pendingWrites.push(writePromise);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* SYNC READ: Returns cached unlocked achievements immediately
|
|
343
|
+
*/
|
|
344
|
+
getUnlockedAchievements() {
|
|
345
|
+
return this.cache.unlocked;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* SYNC WRITE: Updates cache immediately, writes to storage in background
|
|
349
|
+
*/
|
|
350
|
+
setUnlockedAchievements(achievements) {
|
|
351
|
+
// Update cache immediately (optimistic update)
|
|
352
|
+
this.cache.unlocked = achievements;
|
|
353
|
+
// Write to async storage in background
|
|
354
|
+
const writePromise = this.asyncStorage.setUnlockedAchievements(achievements).catch(error => {
|
|
355
|
+
console.error('Failed to write unlocked achievements to async storage:', error);
|
|
356
|
+
if (this.onError) {
|
|
357
|
+
const storageError = error instanceof AchievementError
|
|
358
|
+
? error
|
|
359
|
+
: new StorageError('Failed to write achievements', error);
|
|
360
|
+
this.onError(storageError);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
// Track pending write
|
|
364
|
+
this.pendingWrites.push(writePromise);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* SYNC CLEAR: Clears cache immediately, clears storage in background
|
|
368
|
+
*/
|
|
369
|
+
clear() {
|
|
370
|
+
// Clear cache immediately
|
|
371
|
+
this.cache.metrics = {};
|
|
372
|
+
this.cache.unlocked = [];
|
|
373
|
+
// Clear async storage in background
|
|
374
|
+
const clearPromise = this.asyncStorage.clear().catch(error => {
|
|
375
|
+
console.error('Failed to clear async storage:', error);
|
|
376
|
+
if (this.onError) {
|
|
377
|
+
const storageError = error instanceof AchievementError
|
|
378
|
+
? error
|
|
379
|
+
: new StorageError('Failed to clear storage', error);
|
|
380
|
+
this.onError(storageError);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
// Track pending write
|
|
384
|
+
this.pendingWrites.push(clearPromise);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Wait for all pending writes to complete (useful for testing/cleanup)
|
|
388
|
+
* NOT part of AchievementStorage interface - utility method
|
|
389
|
+
*/
|
|
390
|
+
flush() {
|
|
391
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
392
|
+
yield Promise.all(this.pendingWrites);
|
|
393
|
+
this.pendingWrites = [];
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
class IndexedDBStorage {
|
|
399
|
+
constructor(dbName = 'react-achievements') {
|
|
400
|
+
this.storeName = 'achievements';
|
|
401
|
+
this.db = null;
|
|
402
|
+
this.dbName = dbName;
|
|
403
|
+
this.initPromise = this.initDB();
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Initialize IndexedDB database and object store
|
|
407
|
+
*/
|
|
408
|
+
initDB() {
|
|
409
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
410
|
+
return new Promise((resolve, reject) => {
|
|
411
|
+
const request = indexedDB.open(this.dbName, 1);
|
|
412
|
+
request.onerror = () => {
|
|
413
|
+
reject(new StorageError('Failed to open IndexedDB'));
|
|
414
|
+
};
|
|
415
|
+
request.onsuccess = () => {
|
|
416
|
+
this.db = request.result;
|
|
417
|
+
resolve();
|
|
418
|
+
};
|
|
419
|
+
request.onupgradeneeded = (event) => {
|
|
420
|
+
const db = event.target.result;
|
|
421
|
+
// Create object store if it doesn't exist
|
|
422
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
423
|
+
db.createObjectStore(this.storeName);
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Generic get operation from IndexedDB
|
|
431
|
+
*/
|
|
432
|
+
get(key) {
|
|
433
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
434
|
+
yield this.initPromise;
|
|
435
|
+
if (!this.db)
|
|
436
|
+
throw new StorageError('Database not initialized');
|
|
437
|
+
return new Promise((resolve, reject) => {
|
|
438
|
+
const transaction = this.db.transaction([this.storeName], 'readonly');
|
|
439
|
+
const store = transaction.objectStore(this.storeName);
|
|
440
|
+
const request = store.get(key);
|
|
441
|
+
request.onsuccess = () => {
|
|
442
|
+
resolve(request.result || null);
|
|
443
|
+
};
|
|
444
|
+
request.onerror = () => {
|
|
445
|
+
reject(new StorageError(`Failed to read from IndexedDB: ${key}`));
|
|
446
|
+
};
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Generic set operation to IndexedDB
|
|
452
|
+
*/
|
|
453
|
+
set(key, value) {
|
|
454
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
455
|
+
yield this.initPromise;
|
|
456
|
+
if (!this.db)
|
|
457
|
+
throw new StorageError('Database not initialized');
|
|
458
|
+
return new Promise((resolve, reject) => {
|
|
459
|
+
const transaction = this.db.transaction([this.storeName], 'readwrite');
|
|
460
|
+
const store = transaction.objectStore(this.storeName);
|
|
461
|
+
const request = store.put(value, key);
|
|
462
|
+
request.onsuccess = () => {
|
|
463
|
+
resolve();
|
|
464
|
+
};
|
|
465
|
+
request.onerror = () => {
|
|
466
|
+
reject(new StorageError(`Failed to write to IndexedDB: ${key}`));
|
|
467
|
+
};
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Delete operation from IndexedDB
|
|
473
|
+
*/
|
|
474
|
+
delete(key) {
|
|
475
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
476
|
+
yield this.initPromise;
|
|
477
|
+
if (!this.db)
|
|
478
|
+
throw new StorageError('Database not initialized');
|
|
479
|
+
return new Promise((resolve, reject) => {
|
|
480
|
+
const transaction = this.db.transaction([this.storeName], 'readwrite');
|
|
481
|
+
const store = transaction.objectStore(this.storeName);
|
|
482
|
+
const request = store.delete(key);
|
|
483
|
+
request.onsuccess = () => {
|
|
484
|
+
resolve();
|
|
485
|
+
};
|
|
486
|
+
request.onerror = () => {
|
|
487
|
+
reject(new StorageError(`Failed to delete from IndexedDB: ${key}`));
|
|
488
|
+
};
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
getMetrics() {
|
|
493
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
494
|
+
const metrics = yield this.get('metrics');
|
|
495
|
+
return metrics || {};
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
setMetrics(metrics) {
|
|
499
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
500
|
+
yield this.set('metrics', metrics);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
getUnlockedAchievements() {
|
|
504
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
505
|
+
const unlocked = yield this.get('unlocked');
|
|
506
|
+
return unlocked || [];
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
setUnlockedAchievements(achievements) {
|
|
510
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
511
|
+
yield this.set('unlocked', achievements);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
clear() {
|
|
515
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
516
|
+
yield Promise.all([
|
|
517
|
+
this.delete('metrics'),
|
|
518
|
+
this.delete('unlocked')
|
|
519
|
+
]);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
class RestApiStorage {
|
|
525
|
+
constructor(config) {
|
|
526
|
+
this.config = Object.assign({ timeout: 10000, headers: {} }, config);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Generic fetch wrapper with timeout and error handling
|
|
530
|
+
*/
|
|
531
|
+
fetchWithTimeout(url, options) {
|
|
532
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
533
|
+
const controller = new AbortController();
|
|
534
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
535
|
+
try {
|
|
536
|
+
const response = yield fetch(url, Object.assign(Object.assign({}, options), { headers: Object.assign(Object.assign({ 'Content-Type': 'application/json' }, this.config.headers), options.headers), signal: controller.signal }));
|
|
537
|
+
clearTimeout(timeoutId);
|
|
538
|
+
if (!response.ok) {
|
|
539
|
+
throw new SyncError(`HTTP ${response.status}: ${response.statusText}`, { statusCode: response.status });
|
|
540
|
+
}
|
|
541
|
+
return response;
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
clearTimeout(timeoutId);
|
|
545
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
546
|
+
throw new SyncError('Request timeout', { timeout: this.config.timeout });
|
|
547
|
+
}
|
|
548
|
+
throw error;
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
getMetrics() {
|
|
553
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
554
|
+
var _a;
|
|
555
|
+
try {
|
|
556
|
+
const url = `${this.config.baseUrl}/users/${this.config.userId}/achievements/metrics`;
|
|
557
|
+
const response = yield this.fetchWithTimeout(url, { method: 'GET' });
|
|
558
|
+
const data = yield response.json();
|
|
559
|
+
return data.metrics || {};
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
// Re-throw SyncError and other AchievementErrors (but not StorageError)
|
|
563
|
+
// Multiple checks for Jest compatibility
|
|
564
|
+
const err = error;
|
|
565
|
+
if (((_a = err === null || err === void 0 ? void 0 : err.constructor) === null || _a === void 0 ? void 0 : _a.name) === 'SyncError' || (err === null || err === void 0 ? void 0 : err.name) === 'SyncError') {
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
568
|
+
// Also check instanceof for normal cases
|
|
569
|
+
if (error instanceof AchievementError && !(error instanceof StorageError)) {
|
|
570
|
+
throw error;
|
|
571
|
+
}
|
|
572
|
+
throw new StorageError('Failed to fetch metrics from API', error);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
setMetrics(metrics) {
|
|
577
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
578
|
+
var _a;
|
|
579
|
+
try {
|
|
580
|
+
const url = `${this.config.baseUrl}/users/${this.config.userId}/achievements/metrics`;
|
|
581
|
+
yield this.fetchWithTimeout(url, {
|
|
582
|
+
method: 'PUT',
|
|
583
|
+
body: JSON.stringify({ metrics })
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
const err = error;
|
|
588
|
+
if (((_a = err === null || err === void 0 ? void 0 : err.constructor) === null || _a === void 0 ? void 0 : _a.name) === 'SyncError' || (err === null || err === void 0 ? void 0 : err.name) === 'SyncError')
|
|
589
|
+
throw error;
|
|
590
|
+
if (error instanceof AchievementError && !(error instanceof StorageError))
|
|
591
|
+
throw error;
|
|
592
|
+
throw new StorageError('Failed to save metrics to API', error);
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
getUnlockedAchievements() {
|
|
597
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
598
|
+
var _a;
|
|
599
|
+
try {
|
|
600
|
+
const url = `${this.config.baseUrl}/users/${this.config.userId}/achievements/unlocked`;
|
|
601
|
+
const response = yield this.fetchWithTimeout(url, { method: 'GET' });
|
|
602
|
+
const data = yield response.json();
|
|
603
|
+
return data.unlocked || [];
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
const err = error;
|
|
607
|
+
if (((_a = err === null || err === void 0 ? void 0 : err.constructor) === null || _a === void 0 ? void 0 : _a.name) === 'SyncError' || (err === null || err === void 0 ? void 0 : err.name) === 'SyncError')
|
|
608
|
+
throw error;
|
|
609
|
+
if (error instanceof AchievementError && !(error instanceof StorageError))
|
|
610
|
+
throw error;
|
|
611
|
+
throw new StorageError('Failed to fetch unlocked achievements from API', error);
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
setUnlockedAchievements(achievements) {
|
|
616
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
617
|
+
var _a;
|
|
618
|
+
try {
|
|
619
|
+
const url = `${this.config.baseUrl}/users/${this.config.userId}/achievements/unlocked`;
|
|
620
|
+
yield this.fetchWithTimeout(url, {
|
|
621
|
+
method: 'PUT',
|
|
622
|
+
body: JSON.stringify({ unlocked: achievements })
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
const err = error;
|
|
627
|
+
if (((_a = err === null || err === void 0 ? void 0 : err.constructor) === null || _a === void 0 ? void 0 : _a.name) === 'SyncError' || (err === null || err === void 0 ? void 0 : err.name) === 'SyncError')
|
|
628
|
+
throw error;
|
|
629
|
+
if (error instanceof AchievementError && !(error instanceof StorageError))
|
|
630
|
+
throw error;
|
|
631
|
+
throw new StorageError('Failed to save unlocked achievements to API', error);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
clear() {
|
|
636
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
637
|
+
var _a;
|
|
638
|
+
try {
|
|
639
|
+
const url = `${this.config.baseUrl}/users/${this.config.userId}/achievements`;
|
|
640
|
+
yield this.fetchWithTimeout(url, { method: 'DELETE' });
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
const err = error;
|
|
644
|
+
if (((_a = err === null || err === void 0 ? void 0 : err.constructor) === null || _a === void 0 ? void 0 : _a.name) === 'SyncError' || (err === null || err === void 0 ? void 0 : err.name) === 'SyncError')
|
|
645
|
+
throw error;
|
|
646
|
+
if (error instanceof AchievementError && !(error instanceof StorageError))
|
|
647
|
+
throw error;
|
|
648
|
+
throw new StorageError('Failed to clear achievements via API', error);
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
class OfflineQueueStorage {
|
|
655
|
+
constructor(innerStorage) {
|
|
656
|
+
this.queue = [];
|
|
657
|
+
this.isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
|
|
658
|
+
this.isSyncing = false;
|
|
659
|
+
this.queueStorageKey = 'achievements_offline_queue';
|
|
660
|
+
this.handleOnline = () => {
|
|
661
|
+
this.isOnline = true;
|
|
662
|
+
console.log('[OfflineQueue] Back online, processing queue...');
|
|
663
|
+
this.processQueue();
|
|
664
|
+
};
|
|
665
|
+
this.handleOffline = () => {
|
|
666
|
+
this.isOnline = false;
|
|
667
|
+
console.log('[OfflineQueue] Offline mode activated');
|
|
668
|
+
};
|
|
669
|
+
this.innerStorage = innerStorage;
|
|
670
|
+
// Load queued operations from localStorage
|
|
671
|
+
this.loadQueue();
|
|
672
|
+
// Listen for online/offline events (only in browser environment)
|
|
673
|
+
if (typeof window !== 'undefined') {
|
|
674
|
+
window.addEventListener('online', this.handleOnline);
|
|
675
|
+
window.addEventListener('offline', this.handleOffline);
|
|
676
|
+
}
|
|
677
|
+
// Process queue if already online
|
|
678
|
+
if (this.isOnline) {
|
|
679
|
+
this.processQueue();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
loadQueue() {
|
|
683
|
+
try {
|
|
684
|
+
if (typeof localStorage !== 'undefined') {
|
|
685
|
+
const queueData = localStorage.getItem(this.queueStorageKey);
|
|
686
|
+
if (queueData) {
|
|
687
|
+
this.queue = JSON.parse(queueData);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
console.error('Failed to load offline queue:', error);
|
|
693
|
+
this.queue = [];
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
saveQueue() {
|
|
697
|
+
try {
|
|
698
|
+
if (typeof localStorage !== 'undefined') {
|
|
699
|
+
localStorage.setItem(this.queueStorageKey, JSON.stringify(this.queue));
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
console.error('Failed to save offline queue:', error);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
processQueue() {
|
|
707
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
708
|
+
if (this.isSyncing || this.queue.length === 0 || !this.isOnline) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
this.isSyncing = true;
|
|
712
|
+
try {
|
|
713
|
+
// Process operations in order
|
|
714
|
+
while (this.queue.length > 0 && this.isOnline) {
|
|
715
|
+
const operation = this.queue[0];
|
|
716
|
+
try {
|
|
717
|
+
switch (operation.type) {
|
|
718
|
+
case 'setMetrics':
|
|
719
|
+
yield this.innerStorage.setMetrics(operation.data);
|
|
720
|
+
break;
|
|
721
|
+
case 'setUnlockedAchievements':
|
|
722
|
+
yield this.innerStorage.setUnlockedAchievements(operation.data);
|
|
723
|
+
break;
|
|
724
|
+
case 'clear':
|
|
725
|
+
yield this.innerStorage.clear();
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
// Operation succeeded, remove from queue
|
|
729
|
+
this.queue.shift();
|
|
730
|
+
this.saveQueue();
|
|
731
|
+
}
|
|
732
|
+
catch (error) {
|
|
733
|
+
console.error('Failed to sync queued operation:', error);
|
|
734
|
+
// Stop processing on error, will retry later
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
finally {
|
|
740
|
+
this.isSyncing = false;
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
queueOperation(type, data) {
|
|
745
|
+
const operation = {
|
|
746
|
+
id: `${Date.now()}_${Math.random()}`,
|
|
747
|
+
type,
|
|
748
|
+
data,
|
|
749
|
+
timestamp: Date.now()
|
|
750
|
+
};
|
|
751
|
+
this.queue.push(operation);
|
|
752
|
+
this.saveQueue();
|
|
753
|
+
// Try to process queue if online
|
|
754
|
+
if (this.isOnline) {
|
|
755
|
+
this.processQueue();
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
getMetrics() {
|
|
759
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
760
|
+
// Reads always try to hit the server first
|
|
761
|
+
try {
|
|
762
|
+
return yield this.innerStorage.getMetrics();
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
if (!this.isOnline) {
|
|
766
|
+
throw new StorageError('Cannot read metrics while offline');
|
|
767
|
+
}
|
|
768
|
+
throw error;
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
setMetrics(metrics) {
|
|
773
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
774
|
+
if (this.isOnline) {
|
|
775
|
+
try {
|
|
776
|
+
yield this.innerStorage.setMetrics(metrics);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
catch (error) {
|
|
780
|
+
// Failed while online, queue it
|
|
781
|
+
console.warn('Failed to set metrics, queuing for later:', error);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
// Queue operation if offline or if online operation failed
|
|
785
|
+
this.queueOperation('setMetrics', metrics);
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
getUnlockedAchievements() {
|
|
789
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
790
|
+
// Reads always try to hit the server first
|
|
791
|
+
try {
|
|
792
|
+
return yield this.innerStorage.getUnlockedAchievements();
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
if (!this.isOnline) {
|
|
796
|
+
throw new StorageError('Cannot read achievements while offline');
|
|
797
|
+
}
|
|
798
|
+
throw error;
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
setUnlockedAchievements(achievements) {
|
|
803
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
804
|
+
if (this.isOnline) {
|
|
805
|
+
try {
|
|
806
|
+
yield this.innerStorage.setUnlockedAchievements(achievements);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
catch (error) {
|
|
810
|
+
// Failed while online, queue it
|
|
811
|
+
console.warn('Failed to set unlocked achievements, queuing for later:', error);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// Queue operation if offline or if online operation failed
|
|
815
|
+
this.queueOperation('setUnlockedAchievements', achievements);
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
clear() {
|
|
819
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
820
|
+
if (this.isOnline) {
|
|
821
|
+
try {
|
|
822
|
+
yield this.innerStorage.clear();
|
|
823
|
+
// Also clear the queue
|
|
824
|
+
this.queue = [];
|
|
825
|
+
this.saveQueue();
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
catch (error) {
|
|
829
|
+
console.warn('Failed to clear, queuing for later:', error);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
// Queue operation if offline or if online operation failed
|
|
833
|
+
this.queueOperation('clear');
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Manually trigger queue processing (useful for testing)
|
|
838
|
+
*/
|
|
839
|
+
sync() {
|
|
840
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
841
|
+
yield this.processQueue();
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Get current queue status (useful for debugging)
|
|
846
|
+
*/
|
|
847
|
+
getQueueStatus() {
|
|
848
|
+
return {
|
|
849
|
+
pending: this.queue.length,
|
|
850
|
+
operations: [...this.queue]
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Cleanup listeners (call on unmount)
|
|
855
|
+
*/
|
|
856
|
+
destroy() {
|
|
857
|
+
if (typeof window !== 'undefined') {
|
|
858
|
+
window.removeEventListener('online', this.handleOnline);
|
|
859
|
+
window.removeEventListener('offline', this.handleOffline);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
219
864
|
const getPositionStyles = (position) => {
|
|
220
865
|
const base = {
|
|
221
866
|
position: 'fixed',
|
|
@@ -655,7 +1300,7 @@ function preserveMetrics(current, imported) {
|
|
|
655
1300
|
}
|
|
656
1301
|
|
|
657
1302
|
const AchievementContext = createContext(undefined);
|
|
658
|
-
const AchievementProvider = ({ achievements: achievementsConfig, storage = StorageType.Local, children, icons = {}, onError, }) => {
|
|
1303
|
+
const AchievementProvider = ({ achievements: achievementsConfig, storage = StorageType.Local, children, icons = {}, onError, restApiConfig, }) => {
|
|
659
1304
|
// Normalize the configuration to the complex format
|
|
660
1305
|
const achievements = normalizeAchievements(achievementsConfig);
|
|
661
1306
|
const [achievementState, setAchievementState] = useState({
|
|
@@ -671,19 +1316,41 @@ const AchievementProvider = ({ achievements: achievementsConfig, storage = Stora
|
|
|
671
1316
|
const [currentAchievement, setCurrentAchievement] = useState(null);
|
|
672
1317
|
if (!storageRef.current) {
|
|
673
1318
|
if (typeof storage === 'string') {
|
|
674
|
-
|
|
675
|
-
|
|
1319
|
+
// StorageType enum
|
|
1320
|
+
switch (storage) {
|
|
1321
|
+
case StorageType.Local:
|
|
1322
|
+
storageRef.current = new LocalStorage('achievements');
|
|
1323
|
+
break;
|
|
1324
|
+
case StorageType.Memory:
|
|
1325
|
+
storageRef.current = new MemoryStorage();
|
|
1326
|
+
break;
|
|
1327
|
+
case StorageType.IndexedDB:
|
|
1328
|
+
// Wrap async storage with adapter
|
|
1329
|
+
const indexedDB = new IndexedDBStorage('react-achievements');
|
|
1330
|
+
storageRef.current = new AsyncStorageAdapter(indexedDB, { onError });
|
|
1331
|
+
break;
|
|
1332
|
+
case StorageType.RestAPI:
|
|
1333
|
+
if (!restApiConfig) {
|
|
1334
|
+
throw new ConfigurationError('restApiConfig is required when using StorageType.RestAPI');
|
|
1335
|
+
}
|
|
1336
|
+
// Wrap async storage with adapter
|
|
1337
|
+
const restApi = new RestApiStorage(restApiConfig);
|
|
1338
|
+
storageRef.current = new AsyncStorageAdapter(restApi, { onError });
|
|
1339
|
+
break;
|
|
1340
|
+
default:
|
|
1341
|
+
throw new ConfigurationError(`Unsupported storage type: ${storage}`);
|
|
676
1342
|
}
|
|
677
|
-
|
|
678
|
-
|
|
1343
|
+
}
|
|
1344
|
+
else {
|
|
1345
|
+
// Custom storage instance
|
|
1346
|
+
// Check if it's async storage and wrap with adapter
|
|
1347
|
+
if (isAsyncStorage(storage)) {
|
|
1348
|
+
storageRef.current = new AsyncStorageAdapter(storage, { onError });
|
|
679
1349
|
}
|
|
680
1350
|
else {
|
|
681
|
-
|
|
1351
|
+
storageRef.current = storage;
|
|
682
1352
|
}
|
|
683
1353
|
}
|
|
684
|
-
else {
|
|
685
|
-
storageRef.current = storage;
|
|
686
|
-
}
|
|
687
1354
|
}
|
|
688
1355
|
const storageImpl = storageRef.current;
|
|
689
1356
|
const getNotifiedAchievementsKey = () => {
|
|
@@ -1344,5 +2011,5 @@ class AchievementBuilder {
|
|
|
1344
2011
|
}
|
|
1345
2012
|
}
|
|
1346
2013
|
|
|
1347
|
-
export { AchievementBuilder, AchievementContext, AchievementError, AchievementProvider, BadgesButton, BadgesModal, ConfettiWrapper, ConfigurationError, ImportValidationError, LocalStorage, MemoryStorage, StorageError, StorageQuotaError, StorageType, SyncError, createConfigHash, defaultAchievementIcons, defaultStyles, exportAchievementData, importAchievementData, isAchievementError, isRecoverableError, isSimpleConfig, normalizeAchievements, useAchievements, useSimpleAchievements };
|
|
2014
|
+
export { AchievementBuilder, AchievementContext, AchievementError, AchievementProvider, AsyncStorageAdapter, BadgesButton, BadgesModal, ConfettiWrapper, ConfigurationError, ImportValidationError, IndexedDBStorage, LocalStorage, MemoryStorage, OfflineQueueStorage, RestApiStorage, StorageError, StorageQuotaError, StorageType, SyncError, createConfigHash, defaultAchievementIcons, defaultStyles, exportAchievementData, importAchievementData, isAchievementError, isAsyncStorage, isRecoverableError, isSimpleConfig, normalizeAchievements, useAchievements, useSimpleAchievements };
|
|
1348
2015
|
//# sourceMappingURL=index.js.map
|