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 CHANGED
@@ -322,6 +322,10 @@ See the [examples directory](./stories/examples) for detailed implementations an
322
322
  - Toast notifications
323
323
  - Confetti animations
324
324
  - TypeScript support
325
+ - **NEW in v3.4.0**: Async storage support (IndexedDB, REST API, Offline Queue)
326
+ - **NEW in v3.4.0**: 50MB+ storage capacity with IndexedDB
327
+ - **NEW in v3.4.0**: Server-side sync with REST API storage
328
+ - **NEW in v3.4.0**: Offline-first capabilities with automatic queue sync
325
329
  - **NEW in v3.3.0**: Comprehensive error handling system
326
330
  - **NEW in v3.3.0**: Data export/import for achievement portability
327
331
  - **NEW in v3.3.0**: Type-safe error classes with recovery guidance
@@ -416,9 +420,185 @@ const achievements = {
416
420
 
417
421
  The library provides a small set of essential fallback icons for system use (error states, loading, etc.). These are automatically used when needed and don't require any configuration.
418
422
 
423
+ ## Async Storage (NEW in v3.4.0)
424
+
425
+ React Achievements now supports async storage backends for modern applications that need large data capacity, server sync, or offline-first capabilities.
426
+
427
+ ### IndexedDB Storage
428
+
429
+ Browser-native storage with 50MB+ capacity (vs localStorage's 5-10MB limit):
430
+
431
+ ```tsx
432
+ import { AchievementProvider, StorageType } from 'react-achievements';
433
+
434
+ const App = () => {
435
+ return (
436
+ <AchievementProvider
437
+ achievements={gameAchievements}
438
+ storage={StorageType.IndexedDB} // Use IndexedDB for large data
439
+ >
440
+ <Game />
441
+ </AchievementProvider>
442
+ );
443
+ };
444
+ ```
445
+
446
+ **Benefits:**
447
+ - ✅ 10x larger capacity than localStorage
448
+ - ✅ Structured data storage
449
+ - ✅ Better performance for large datasets
450
+ - ✅ Non-blocking async operations
451
+
452
+ ### REST API Storage
453
+
454
+ Sync achievements with your backend server:
455
+
456
+ ```tsx
457
+ import { AchievementProvider, StorageType } from 'react-achievements';
458
+
459
+ const App = () => {
460
+ return (
461
+ <AchievementProvider
462
+ achievements={gameAchievements}
463
+ storage={StorageType.RestAPI}
464
+ restApiConfig={{
465
+ baseUrl: 'https://api.example.com',
466
+ userId: getCurrentUserId(),
467
+ headers: {
468
+ 'Authorization': `Bearer ${getAuthToken()}`
469
+ },
470
+ timeout: 10000 // Optional, default 10s
471
+ }}
472
+ >
473
+ <Game />
474
+ </AchievementProvider>
475
+ );
476
+ };
477
+ ```
478
+
479
+ **API Endpoints Expected:**
480
+ ```
481
+ GET /users/:userId/achievements/metrics
482
+ PUT /users/:userId/achievements/metrics
483
+ GET /users/:userId/achievements/unlocked
484
+ PUT /users/:userId/achievements/unlocked
485
+ DELETE /users/:userId/achievements
486
+ ```
487
+
488
+ **Benefits:**
489
+ - ✅ Cross-device synchronization
490
+ - ✅ Server-side backup
491
+ - ✅ User authentication support
492
+ - ✅ Centralized data management
493
+
494
+ ### Offline Queue Storage
495
+
496
+ Offline-first storage with automatic sync when back online:
497
+
498
+ ```tsx
499
+ import {
500
+ AchievementProvider,
501
+ OfflineQueueStorage,
502
+ RestApiStorage
503
+ } from 'react-achievements';
504
+
505
+ // Wrap REST API storage with offline queue
506
+ const restApi = new RestApiStorage({
507
+ baseUrl: 'https://api.example.com',
508
+ userId: 'user123',
509
+ headers: { 'Authorization': 'Bearer token' }
510
+ });
511
+
512
+ const offlineStorage = new OfflineQueueStorage(restApi);
513
+
514
+ const App = () => {
515
+ return (
516
+ <AchievementProvider
517
+ achievements={gameAchievements}
518
+ storage={offlineStorage}
519
+ >
520
+ <Game />
521
+ </AchievementProvider>
522
+ );
523
+ };
524
+ ```
525
+
526
+ **Benefits:**
527
+ - ✅ Works offline - queues operations locally
528
+ - ✅ Automatic sync when connection restored
529
+ - ✅ Persistent queue survives page refreshes
530
+ - ✅ Graceful degradation for poor connectivity
531
+
532
+ ### Custom Async Storage
533
+
534
+ You can create custom async storage by implementing the `AsyncAchievementStorage` interface:
535
+
536
+ ```tsx
537
+ import {
538
+ AsyncAchievementStorage,
539
+ AchievementMetrics,
540
+ AsyncStorageAdapter,
541
+ AchievementProvider
542
+ } from 'react-achievements';
543
+
544
+ class MyCustomAsyncStorage implements AsyncAchievementStorage {
545
+ async getMetrics(): Promise<AchievementMetrics> {
546
+ // Your async implementation (e.g., fetch from database)
547
+ const response = await fetch('/my-api/metrics');
548
+ return response.json();
549
+ }
550
+
551
+ async setMetrics(metrics: AchievementMetrics): Promise<void> {
552
+ await fetch('/my-api/metrics', {
553
+ method: 'PUT',
554
+ body: JSON.stringify(metrics)
555
+ });
556
+ }
557
+
558
+ async getUnlockedAchievements(): Promise<string[]> {
559
+ const response = await fetch('/my-api/unlocked');
560
+ return response.json();
561
+ }
562
+
563
+ async setUnlockedAchievements(achievements: string[]): Promise<void> {
564
+ await fetch('/my-api/unlocked', {
565
+ method: 'PUT',
566
+ body: JSON.stringify(achievements)
567
+ });
568
+ }
569
+
570
+ async clear(): Promise<void> {
571
+ await fetch('/my-api/clear', { method: 'DELETE' });
572
+ }
573
+ }
574
+
575
+ // Wrap with adapter for optimistic updates
576
+ const customStorage = new MyCustomAsyncStorage();
577
+ const adapter = new AsyncStorageAdapter(customStorage, {
578
+ onError: (error) => console.error('Storage error:', error)
579
+ });
580
+
581
+ const App = () => {
582
+ return (
583
+ <AchievementProvider
584
+ achievements={gameAchievements}
585
+ storage={adapter}
586
+ >
587
+ <Game />
588
+ </AchievementProvider>
589
+ );
590
+ };
591
+ ```
592
+
593
+ **How AsyncStorageAdapter Works:**
594
+ - **Optimistic Updates**: Returns cached data immediately (no waiting)
595
+ - **Eager Loading**: Preloads data during initialization
596
+ - **Background Writes**: All writes happen async without blocking UI
597
+ - **Error Handling**: Optional error callback for failed operations
598
+
419
599
  ## Custom Storage
420
600
 
421
- You can implement your own storage solution by implementing the `AchievementStorage` interface:
601
+ You can implement your own synchronous storage solution by implementing the `AchievementStorage` interface:
422
602
 
423
603
  ```tsx
424
604
  import { AchievementStorage, AchievementMetrics, AchievementProvider } from 'react-achievements';
@@ -946,7 +1126,9 @@ const result = importAchievementData(data, {
946
1126
  strategy: 'replace',
947
1127
  achievements
948
1128
  });
1129
+ ```
949
1130
 
1131
+ ```tsx
950
1132
  // Merge: Combine imported and existing data
951
1133
  // - Takes maximum values for metrics
952
1134
  // - Combines unlocked achievements
@@ -954,6 +1136,9 @@ const result = importAchievementData(data, {
954
1136
  strategy: 'merge',
955
1137
  achievements
956
1138
  });
1139
+ ```
1140
+
1141
+ ```tsx
957
1142
 
958
1143
  // Preserve: Only import new achievements, keep existing data
959
1144
  const result = importAchievementData(data, {
package/dist/index.d.ts CHANGED
@@ -51,6 +51,15 @@ interface AchievementStorage {
51
51
  setUnlockedAchievements(achievements: string[]): void;
52
52
  clear(): void;
53
53
  }
54
+ interface AsyncAchievementStorage {
55
+ getMetrics(): Promise<AchievementMetrics>;
56
+ setMetrics(metrics: AchievementMetrics): Promise<void>;
57
+ getUnlockedAchievements(): Promise<string[]>;
58
+ setUnlockedAchievements(achievements: string[]): Promise<void>;
59
+ clear(): Promise<void>;
60
+ }
61
+ type AnyAchievementStorage = AchievementStorage | AsyncAchievementStorage;
62
+ declare function isAsyncStorage(storage: AnyAchievementStorage): storage is AsyncAchievementStorage;
54
63
  interface AchievementContextValue {
55
64
  updateMetrics: (metrics: AchievementMetrics | ((prev: AchievementMetrics) => AchievementMetrics)) => void;
56
65
  unlockedAchievements: string[];
@@ -84,8 +93,10 @@ interface AchievementProviderProps$1 {
84
93
  onAchievementUnlocked?: (achievement: AchievementDetails) => void;
85
94
  }
86
95
  declare enum StorageType {
87
- Local = "local",
88
- Memory = "memory"
96
+ Local = "local",// Synchronous localStorage
97
+ Memory = "memory",// Synchronous in-memory storage
98
+ IndexedDB = "indexeddb",// Asynchronous IndexedDB storage
99
+ RestAPI = "restapi"
89
100
  }
90
101
 
91
102
  declare class LocalStorage implements AchievementStorage {
@@ -115,6 +126,199 @@ declare class MemoryStorage implements AchievementStorage {
115
126
  clear(): void;
116
127
  }
117
128
 
129
+ /**
130
+ * Base error class for all achievement-related errors
131
+ */
132
+ declare class AchievementError extends Error {
133
+ code: string;
134
+ recoverable: boolean;
135
+ remedy?: string | undefined;
136
+ constructor(message: string, code: string, recoverable: boolean, remedy?: string | undefined);
137
+ }
138
+ /**
139
+ * Error thrown when browser storage quota is exceeded
140
+ */
141
+ declare class StorageQuotaError extends AchievementError {
142
+ bytesNeeded: number;
143
+ constructor(bytesNeeded: number);
144
+ }
145
+ /**
146
+ * Error thrown when imported data fails validation
147
+ */
148
+ declare class ImportValidationError extends AchievementError {
149
+ validationErrors: string[];
150
+ constructor(validationErrors: string[]);
151
+ }
152
+ /**
153
+ * Error thrown when storage operations fail
154
+ */
155
+ declare class StorageError extends AchievementError {
156
+ originalError?: Error | undefined;
157
+ constructor(message: string, originalError?: Error | undefined);
158
+ }
159
+ /**
160
+ * Error thrown when configuration is invalid
161
+ */
162
+ declare class ConfigurationError extends AchievementError {
163
+ constructor(message: string);
164
+ }
165
+ /**
166
+ * Error thrown when network sync operations fail
167
+ */
168
+ declare class SyncError extends AchievementError {
169
+ readonly statusCode?: number;
170
+ readonly timeout?: number;
171
+ constructor(message: string, details?: {
172
+ statusCode?: number;
173
+ timeout?: number;
174
+ });
175
+ }
176
+ /**
177
+ * Type guard to check if an error is an AchievementError
178
+ */
179
+ declare function isAchievementError(error: unknown): error is AchievementError;
180
+ /**
181
+ * Type guard to check if an error is recoverable
182
+ */
183
+ declare function isRecoverableError(error: unknown): boolean;
184
+
185
+ declare class AsyncStorageAdapter implements AchievementStorage {
186
+ private asyncStorage;
187
+ private cache;
188
+ private pendingWrites;
189
+ private onError?;
190
+ constructor(asyncStorage: AsyncAchievementStorage, options?: {
191
+ onError?: (error: AchievementError) => void;
192
+ });
193
+ /**
194
+ * Initialize cache by loading from async storage
195
+ * This happens in the background during construction
196
+ */
197
+ private initializeCache;
198
+ /**
199
+ * Wait for cache to be loaded (used internally)
200
+ * Returns immediately if already loaded, otherwise waits
201
+ */
202
+ private ensureCacheLoaded;
203
+ /**
204
+ * SYNC READ: Returns cached metrics immediately
205
+ * Cache is loaded eagerly during construction
206
+ */
207
+ getMetrics(): AchievementMetrics;
208
+ /**
209
+ * SYNC WRITE: Updates cache immediately, writes to storage in background
210
+ * Uses optimistic updates - assumes write will succeed
211
+ */
212
+ setMetrics(metrics: AchievementMetrics): void;
213
+ /**
214
+ * SYNC READ: Returns cached unlocked achievements immediately
215
+ */
216
+ getUnlockedAchievements(): string[];
217
+ /**
218
+ * SYNC WRITE: Updates cache immediately, writes to storage in background
219
+ */
220
+ setUnlockedAchievements(achievements: string[]): void;
221
+ /**
222
+ * SYNC CLEAR: Clears cache immediately, clears storage in background
223
+ */
224
+ clear(): void;
225
+ /**
226
+ * Wait for all pending writes to complete (useful for testing/cleanup)
227
+ * NOT part of AchievementStorage interface - utility method
228
+ */
229
+ flush(): Promise<void>;
230
+ }
231
+
232
+ declare class IndexedDBStorage implements AsyncAchievementStorage {
233
+ private dbName;
234
+ private storeName;
235
+ private db;
236
+ private initPromise;
237
+ constructor(dbName?: string);
238
+ /**
239
+ * Initialize IndexedDB database and object store
240
+ */
241
+ private initDB;
242
+ /**
243
+ * Generic get operation from IndexedDB
244
+ */
245
+ private get;
246
+ /**
247
+ * Generic set operation to IndexedDB
248
+ */
249
+ private set;
250
+ /**
251
+ * Delete operation from IndexedDB
252
+ */
253
+ private delete;
254
+ getMetrics(): Promise<AchievementMetrics>;
255
+ setMetrics(metrics: AchievementMetrics): Promise<void>;
256
+ getUnlockedAchievements(): Promise<string[]>;
257
+ setUnlockedAchievements(achievements: string[]): Promise<void>;
258
+ clear(): Promise<void>;
259
+ }
260
+
261
+ interface RestApiStorageConfig {
262
+ baseUrl: string;
263
+ userId: string;
264
+ headers?: Record<string, string>;
265
+ timeout?: number;
266
+ }
267
+ declare class RestApiStorage implements AsyncAchievementStorage {
268
+ private config;
269
+ constructor(config: RestApiStorageConfig);
270
+ /**
271
+ * Generic fetch wrapper with timeout and error handling
272
+ */
273
+ private fetchWithTimeout;
274
+ getMetrics(): Promise<AchievementMetrics>;
275
+ setMetrics(metrics: AchievementMetrics): Promise<void>;
276
+ getUnlockedAchievements(): Promise<string[]>;
277
+ setUnlockedAchievements(achievements: string[]): Promise<void>;
278
+ clear(): Promise<void>;
279
+ }
280
+
281
+ interface QueuedOperation {
282
+ id: string;
283
+ type: 'setMetrics' | 'setUnlockedAchievements' | 'clear';
284
+ data?: any;
285
+ timestamp: number;
286
+ }
287
+ declare class OfflineQueueStorage implements AsyncAchievementStorage {
288
+ private innerStorage;
289
+ private queue;
290
+ private isOnline;
291
+ private isSyncing;
292
+ private queueStorageKey;
293
+ constructor(innerStorage: AsyncAchievementStorage);
294
+ private loadQueue;
295
+ private saveQueue;
296
+ private handleOnline;
297
+ private handleOffline;
298
+ private processQueue;
299
+ private queueOperation;
300
+ getMetrics(): Promise<AchievementMetrics>;
301
+ setMetrics(metrics: AchievementMetrics): Promise<void>;
302
+ getUnlockedAchievements(): Promise<string[]>;
303
+ setUnlockedAchievements(achievements: string[]): Promise<void>;
304
+ clear(): Promise<void>;
305
+ /**
306
+ * Manually trigger queue processing (useful for testing)
307
+ */
308
+ sync(): Promise<void>;
309
+ /**
310
+ * Get current queue status (useful for debugging)
311
+ */
312
+ getQueueStatus(): {
313
+ pending: number;
314
+ operations: QueuedOperation[];
315
+ };
316
+ /**
317
+ * Cleanup listeners (call on unmount)
318
+ */
319
+ destroy(): void;
320
+ }
321
+
118
322
  interface BadgesButtonProps {
119
323
  onClick: () => void;
120
324
  position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
@@ -187,58 +391,6 @@ interface ImportResult {
187
391
  */
188
392
  declare function importAchievementData(jsonString: string, currentMetrics: AchievementMetrics, currentUnlocked: string[], options?: ImportOptions): ImportResult;
189
393
 
190
- /**
191
- * Base error class for all achievement-related errors
192
- */
193
- declare class AchievementError extends Error {
194
- code: string;
195
- recoverable: boolean;
196
- remedy?: string | undefined;
197
- constructor(message: string, code: string, recoverable: boolean, remedy?: string | undefined);
198
- }
199
- /**
200
- * Error thrown when browser storage quota is exceeded
201
- */
202
- declare class StorageQuotaError extends AchievementError {
203
- bytesNeeded: number;
204
- constructor(bytesNeeded: number);
205
- }
206
- /**
207
- * Error thrown when imported data fails validation
208
- */
209
- declare class ImportValidationError extends AchievementError {
210
- validationErrors: string[];
211
- constructor(validationErrors: string[]);
212
- }
213
- /**
214
- * Error thrown when storage operations fail
215
- */
216
- declare class StorageError extends AchievementError {
217
- originalError?: Error | undefined;
218
- constructor(message: string, originalError?: Error | undefined);
219
- }
220
- /**
221
- * Error thrown when configuration is invalid
222
- */
223
- declare class ConfigurationError extends AchievementError {
224
- constructor(message: string);
225
- }
226
- /**
227
- * Error thrown when sync operations fail (for async storage backends)
228
- */
229
- declare class SyncError extends AchievementError {
230
- originalError?: Error | undefined;
231
- constructor(message: string, originalError?: Error | undefined);
232
- }
233
- /**
234
- * Type guard to check if an error is an AchievementError
235
- */
236
- declare function isAchievementError(error: unknown): error is AchievementError;
237
- /**
238
- * Type guard to check if an error is recoverable
239
- */
240
- declare function isRecoverableError(error: unknown): boolean;
241
-
242
394
  interface AchievementContextType {
243
395
  update: (metrics: Record<string, any>) => void;
244
396
  achievements: {
@@ -256,10 +408,11 @@ interface AchievementContextType {
256
408
  declare const AchievementContext: React$1.Context<AchievementContextType | undefined>;
257
409
  interface AchievementProviderProps {
258
410
  achievements: AchievementConfigurationType;
259
- storage?: AchievementStorage | StorageType;
411
+ storage?: AchievementStorage | AsyncAchievementStorage | StorageType;
260
412
  children: React$1.ReactNode;
261
413
  icons?: Record<string, string>;
262
414
  onError?: (error: AchievementError) => void;
415
+ restApiConfig?: RestApiStorageConfig;
263
416
  }
264
417
  declare const AchievementProvider: React$1.FC<AchievementProviderProps>;
265
418
 
@@ -506,4 +659,4 @@ declare function exportAchievementData(metrics: AchievementMetrics, unlocked: st
506
659
  */
507
660
  declare function createConfigHash(config: any): string;
508
661
 
509
- export { AchievementBuilder, AchievementCondition, AchievementConfiguration, AchievementConfigurationType, AchievementContext, AchievementContextValue, AchievementDetails, AchievementError, AchievementMetricArrayValue, AchievementMetricValue, AchievementMetrics, AchievementProvider, AchievementProviderProps$1 as AchievementProviderProps, AchievementState, AchievementStorage, AwardDetails, BadgesButton, BadgesModal, ConfettiWrapper, ConfigurationError, CustomAchievementDetails, ExportedData, ImportOptions, ImportResult, ImportValidationError, InitialAchievementMetrics, LocalStorage, MemoryStorage, SimpleAchievementConfig, SimpleAchievementDetails, StorageError, StorageQuotaError, StorageType, StylesProps, SyncError, createConfigHash, defaultAchievementIcons, defaultStyles, exportAchievementData, importAchievementData, isAchievementError, isRecoverableError, isSimpleConfig, normalizeAchievements, useAchievements, useSimpleAchievements };
662
+ export { AchievementBuilder, AchievementCondition, AchievementConfiguration, AchievementConfigurationType, AchievementContext, AchievementContextValue, AchievementDetails, AchievementError, AchievementMetricArrayValue, AchievementMetricValue, AchievementMetrics, AchievementProvider, AchievementProviderProps$1 as AchievementProviderProps, AchievementState, AchievementStorage, AnyAchievementStorage, AsyncAchievementStorage, AsyncStorageAdapter, AwardDetails, BadgesButton, BadgesModal, ConfettiWrapper, ConfigurationError, CustomAchievementDetails, ExportedData, ImportOptions, ImportResult, ImportValidationError, IndexedDBStorage, InitialAchievementMetrics, LocalStorage, MemoryStorage, OfflineQueueStorage, RestApiStorage, RestApiStorageConfig, SimpleAchievementConfig, SimpleAchievementDetails, StorageError, StorageQuotaError, StorageType, StylesProps, SyncError, createConfigHash, defaultAchievementIcons, defaultStyles, exportAchievementData, importAchievementData, isAchievementError, isAsyncStorage, isRecoverableError, isSimpleConfig, normalizeAchievements, useAchievements, useSimpleAchievements };