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/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
  /**
@@ -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
- 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
+ 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
- else if (storage === StorageType.Memory) {
678
- storageRef.current = new MemoryStorage();
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
- throw new Error(`Unsupported storage type: ${storage}`);
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