react-achievements 3.3.0 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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 (for async storage backends)
81
+ * Error thrown when network sync operations fail
74
82
  */
75
83
  class SyncError extends AchievementError {
76
- constructor(message, originalError) {
77
- super(message, 'SYNC_ERROR', true, 'Check your network connection and backend server status. The operation will be retried automatically.');
78
- this.originalError = originalError;
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
  /**
@@ -135,7 +145,7 @@ class LocalStorage {
135
145
  unlockedAchievements: parsed.unlockedAchievements || []
136
146
  };
137
147
  }
138
- catch (e) {
148
+ catch (_a) {
139
149
  return { metrics: {}, unlockedAchievements: [] };
140
150
  }
141
151
  }
@@ -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',
@@ -387,10 +1032,10 @@ function normalizeAchievements(config) {
387
1032
  if (hasCustomCondition(achievement)) {
388
1033
  // Custom condition function
389
1034
  return {
390
- isConditionMet: (value, state) => {
1035
+ isConditionMet: (_value, _state) => {
391
1036
  // Convert internal metrics format (arrays) to simple format for custom conditions
392
1037
  const simpleMetrics = {};
393
- Object.entries(state.metrics).forEach(([key, val]) => {
1038
+ Object.entries(_state.metrics).forEach(([key, val]) => {
394
1039
  simpleMetrics[key] = Array.isArray(val) ? val[0] : val;
395
1040
  });
396
1041
  return achievement.condition(simpleMetrics);
@@ -453,7 +1098,7 @@ function normalizeAchievements(config) {
453
1098
  *
454
1099
  * @example
455
1100
  * ```typescript
456
- * const json = exportAchievementData(metrics, ['score_100', 'level_5']);
1101
+ * const json = exportAchievementData(_metrics, ['score_100', 'level_5']);
457
1102
  * // Save json to file or send to server
458
1103
  * ```
459
1104
  */
@@ -514,7 +1159,7 @@ function importAchievementData(jsonString, currentMetrics, currentUnlocked, opti
514
1159
  try {
515
1160
  data = JSON.parse(jsonString);
516
1161
  }
517
- catch (error) {
1162
+ catch (_a) {
518
1163
  return {
519
1164
  success: false,
520
1165
  imported: { metrics: 0, achievements: 0 },
@@ -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({
@@ -668,22 +1313,46 @@ const AchievementProvider = ({ achievements: achievementsConfig, storage = Stora
668
1313
  const storageRef = useRef(null);
669
1314
  const metricsUpdatedRef = useRef(false);
670
1315
  const [showConfetti, setShowConfetti] = useState(false);
671
- const [currentAchievement, setCurrentAchievement] = useState(null);
1316
+ const [_currentAchievement, setCurrentAchievement] = useState(null);
672
1317
  if (!storageRef.current) {
673
1318
  if (typeof storage === 'string') {
674
- if (storage === StorageType.Local) {
675
- storageRef.current = new LocalStorage('achievements');
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
+ }
1333
+ case StorageType.RestAPI: {
1334
+ if (!restApiConfig) {
1335
+ throw new ConfigurationError('restApiConfig is required when using StorageType.RestAPI');
1336
+ }
1337
+ // Wrap async storage with adapter
1338
+ const restApi = new RestApiStorage(restApiConfig);
1339
+ storageRef.current = new AsyncStorageAdapter(restApi, { onError });
1340
+ break;
1341
+ }
1342
+ default:
1343
+ throw new ConfigurationError(`Unsupported storage type: ${storage}`);
676
1344
  }
677
- else if (storage === StorageType.Memory) {
678
- storageRef.current = new MemoryStorage();
1345
+ }
1346
+ else {
1347
+ // Custom storage instance
1348
+ // Check if it's async storage and wrap with adapter
1349
+ if (isAsyncStorage(storage)) {
1350
+ storageRef.current = new AsyncStorageAdapter(storage, { onError });
679
1351
  }
680
1352
  else {
681
- throw new Error(`Unsupported storage type: ${storage}`);
1353
+ storageRef.current = storage;
682
1354
  }
683
1355
  }
684
- else {
685
- storageRef.current = storage;
686
- }
687
1356
  }
688
1357
  const storageImpl = storageRef.current;
689
1358
  const getNotifiedAchievementsKey = () => {
@@ -746,7 +1415,7 @@ const AchievementProvider = ({ achievements: achievementsConfig, storage = Stora
746
1415
  if (Object.keys(metrics).length === 0 || !metricsUpdatedRef.current)
747
1416
  return;
748
1417
  metricsUpdatedRef.current = false;
749
- let newlyUnlockedAchievements = [];
1418
+ const newlyUnlockedAchievements = [];
750
1419
  let achievementToShow = null;
751
1420
  Object.entries(achievements).forEach(([metricName, metricAchievements]) => {
752
1421
  metricAchievements.forEach((achievement) => {
@@ -1344,5 +2013,5 @@ class AchievementBuilder {
1344
2013
  }
1345
2014
  }
1346
2015
 
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 };
2016
+ 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
2017
  //# sourceMappingURL=index.js.map