react-achievements 3.2.1 → 3.3.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.d.ts CHANGED
@@ -137,6 +137,108 @@ interface ConfettiWrapperProps {
137
137
  }
138
138
  declare const ConfettiWrapper: React$1.FC<ConfettiWrapperProps>;
139
139
 
140
+ /**
141
+ * Options for importing achievement data
142
+ */
143
+ interface ImportOptions {
144
+ /** Strategy for merging imported data with existing data */
145
+ mergeStrategy?: 'replace' | 'merge' | 'preserve';
146
+ /** Whether to validate the imported data */
147
+ validate?: boolean;
148
+ /** Optional config hash to validate against */
149
+ expectedConfigHash?: string;
150
+ }
151
+ /**
152
+ * Result of an import operation
153
+ */
154
+ interface ImportResult {
155
+ success: boolean;
156
+ imported: {
157
+ metrics: number;
158
+ achievements: number;
159
+ };
160
+ errors?: string[];
161
+ warnings?: string[];
162
+ }
163
+ /**
164
+ * Imports achievement data from a JSON string
165
+ *
166
+ * @param jsonString - JSON string containing exported achievement data
167
+ * @param currentMetrics - Current metrics state
168
+ * @param currentUnlocked - Current unlocked achievements
169
+ * @param options - Import options
170
+ * @returns Import result with success status and any errors
171
+ *
172
+ * @example
173
+ * ```typescript
174
+ * const result = importAchievementData(
175
+ * jsonString,
176
+ * currentMetrics,
177
+ * currentUnlocked,
178
+ * { mergeStrategy: 'merge', validate: true }
179
+ * );
180
+ *
181
+ * if (result.success) {
182
+ * console.log(`Imported ${result.imported.achievements} achievements`);
183
+ * } else {
184
+ * console.error('Import failed:', result.errors);
185
+ * }
186
+ * ```
187
+ */
188
+ declare function importAchievementData(jsonString: string, currentMetrics: AchievementMetrics, currentUnlocked: string[], options?: ImportOptions): ImportResult;
189
+
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
+
140
242
  interface AchievementContextType {
141
243
  update: (metrics: Record<string, any>) => void;
142
244
  achievements: {
@@ -148,6 +250,8 @@ interface AchievementContextType {
148
250
  metrics: AchievementMetrics;
149
251
  unlocked: string[];
150
252
  };
253
+ exportData: () => string;
254
+ importData: (jsonString: string, options?: ImportOptions) => ImportResult;
151
255
  }
152
256
  declare const AchievementContext: React$1.Context<AchievementContextType | undefined>;
153
257
  interface AchievementProviderProps {
@@ -155,6 +259,7 @@ interface AchievementProviderProps {
155
259
  storage?: AchievementStorage | StorageType;
156
260
  children: React$1.ReactNode;
157
261
  icons?: Record<string, string>;
262
+ onError?: (error: AchievementError) => void;
158
263
  }
159
264
  declare const AchievementProvider: React$1.FC<AchievementProviderProps>;
160
265
 
@@ -205,6 +310,18 @@ declare const useSimpleAchievements: () => {
205
310
  metrics: AchievementMetrics;
206
311
  unlocked: string[];
207
312
  };
313
+ /**
314
+ * Export achievement data to JSON string
315
+ * @returns JSON string containing all achievement data
316
+ */
317
+ exportData: () => string;
318
+ /**
319
+ * Import achievement data from JSON string
320
+ * @param jsonString - JSON string containing exported achievement data
321
+ * @param options - Import options (merge strategy, validation)
322
+ * @returns Import result with success status and any errors
323
+ */
324
+ importData: (jsonString: string, options?: ImportOptions) => ImportResult;
208
325
  };
209
326
 
210
327
  declare const defaultStyles: Required<StylesProps>;
@@ -355,4 +472,38 @@ declare class AchievementBuilder {
355
472
  static combine(achievements: (SimpleAchievementConfig | Achievement)[]): SimpleAchievementConfig;
356
473
  }
357
474
 
358
- export { AchievementBuilder, AchievementCondition, AchievementConfiguration, AchievementConfigurationType, AchievementContext, AchievementContextValue, AchievementDetails, AchievementMetricArrayValue, AchievementMetricValue, AchievementMetrics, AchievementProvider, AchievementProviderProps$1 as AchievementProviderProps, AchievementState, AchievementStorage, AwardDetails, BadgesButton, BadgesModal, ConfettiWrapper, CustomAchievementDetails, InitialAchievementMetrics, LocalStorage, MemoryStorage, SimpleAchievementConfig, SimpleAchievementDetails, StorageType, StylesProps, defaultAchievementIcons, defaultStyles, isSimpleConfig, normalizeAchievements, useAchievements, useSimpleAchievements };
475
+ /**
476
+ * Structure of exported achievement data
477
+ */
478
+ interface ExportedData {
479
+ version: string;
480
+ timestamp: number;
481
+ metrics: AchievementMetrics;
482
+ unlockedAchievements: string[];
483
+ configHash?: string;
484
+ }
485
+ /**
486
+ * Exports achievement data to a JSON string
487
+ *
488
+ * @param metrics - Current achievement metrics
489
+ * @param unlocked - Array of unlocked achievement IDs
490
+ * @param configHash - Optional hash of achievement configuration for validation
491
+ * @returns JSON string containing all achievement data
492
+ *
493
+ * @example
494
+ * ```typescript
495
+ * const json = exportAchievementData(metrics, ['score_100', 'level_5']);
496
+ * // Save json to file or send to server
497
+ * ```
498
+ */
499
+ declare function exportAchievementData(metrics: AchievementMetrics, unlocked: string[], configHash?: string): string;
500
+ /**
501
+ * Creates a simple hash of the achievement configuration
502
+ * Used to validate that imported data matches the current configuration
503
+ *
504
+ * @param config - Achievement configuration object
505
+ * @returns Simple hash string
506
+ */
507
+ declare function createConfigHash(config: any): string;
508
+
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 };
package/dist/index.js CHANGED
@@ -14,6 +14,84 @@ var StorageType;
14
14
  StorageType["Memory"] = "memory";
15
15
  })(StorageType || (StorageType = {}));
16
16
 
17
+ /**
18
+ * Base error class for all achievement-related errors
19
+ */
20
+ class AchievementError extends Error {
21
+ constructor(message, code, recoverable, remedy) {
22
+ super(message);
23
+ this.code = code;
24
+ this.recoverable = recoverable;
25
+ this.remedy = remedy;
26
+ this.name = 'AchievementError';
27
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
28
+ if (Error.captureStackTrace) {
29
+ Error.captureStackTrace(this, AchievementError);
30
+ }
31
+ }
32
+ }
33
+ /**
34
+ * Error thrown when browser storage quota is exceeded
35
+ */
36
+ class StorageQuotaError extends AchievementError {
37
+ constructor(bytesNeeded) {
38
+ super('Browser storage quota exceeded. Achievement data could not be saved.', 'STORAGE_QUOTA_EXCEEDED', true, 'Clear browser storage, reduce the number of achievements, or use an external database backend. You can export your current data using exportData() before clearing storage.');
39
+ this.bytesNeeded = bytesNeeded;
40
+ this.name = 'StorageQuotaError';
41
+ }
42
+ }
43
+ /**
44
+ * Error thrown when imported data fails validation
45
+ */
46
+ class ImportValidationError extends AchievementError {
47
+ constructor(validationErrors) {
48
+ super(`Imported data failed validation: ${validationErrors.join(', ')}`, 'IMPORT_VALIDATION_ERROR', true, 'Check that the imported data was exported from a compatible version and matches your current achievement configuration.');
49
+ this.validationErrors = validationErrors;
50
+ this.name = 'ImportValidationError';
51
+ }
52
+ }
53
+ /**
54
+ * Error thrown when storage operations fail
55
+ */
56
+ class StorageError extends AchievementError {
57
+ constructor(message, originalError) {
58
+ super(message, 'STORAGE_ERROR', true, 'Check browser storage permissions and available space. If using custom storage, verify the implementation is correct.');
59
+ this.originalError = originalError;
60
+ this.name = 'StorageError';
61
+ }
62
+ }
63
+ /**
64
+ * Error thrown when configuration is invalid
65
+ */
66
+ class ConfigurationError extends AchievementError {
67
+ constructor(message) {
68
+ super(message, 'CONFIGURATION_ERROR', false, 'Review your achievement configuration and ensure it follows the correct format.');
69
+ this.name = 'ConfigurationError';
70
+ }
71
+ }
72
+ /**
73
+ * Error thrown when sync operations fail (for async storage backends)
74
+ */
75
+ 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;
79
+ this.name = 'SyncError';
80
+ }
81
+ }
82
+ /**
83
+ * Type guard to check if an error is an AchievementError
84
+ */
85
+ function isAchievementError(error) {
86
+ return error instanceof AchievementError;
87
+ }
88
+ /**
89
+ * Type guard to check if an error is recoverable
90
+ */
91
+ function isRecoverableError(error) {
92
+ return isAchievementError(error) && error.recoverable;
93
+ }
94
+
17
95
  class LocalStorage {
18
96
  constructor(storageKey) {
19
97
  this.storageKey = storageKey;
@@ -67,17 +145,33 @@ class LocalStorage {
67
145
  metrics: this.serializeMetrics(data.metrics),
68
146
  unlockedAchievements: data.unlockedAchievements
69
147
  };
70
- localStorage.setItem(this.storageKey, JSON.stringify(serialized));
148
+ const jsonString = JSON.stringify(serialized);
149
+ localStorage.setItem(this.storageKey, jsonString);
71
150
  }
72
151
  catch (error) {
73
- // Silently fail on quota exceeded errors
74
- if (error instanceof Error &&
152
+ // Throw proper error instead of silently failing
153
+ if (error instanceof DOMException &&
75
154
  (error.name === 'QuotaExceededError' ||
76
- error.name === 'NS_ERROR_DOM_QUOTA_REACHED' ||
77
- error.message.includes('QuotaExceeded'))) {
78
- return;
155
+ error.name === 'NS_ERROR_DOM_QUOTA_REACHED')) {
156
+ const serialized = {
157
+ metrics: this.serializeMetrics(data.metrics),
158
+ unlockedAchievements: data.unlockedAchievements
159
+ };
160
+ const bytesNeeded = JSON.stringify(serialized).length;
161
+ throw new StorageQuotaError(bytesNeeded);
162
+ }
163
+ if (error instanceof Error) {
164
+ if (error.message && error.message.includes('QuotaExceeded')) {
165
+ const serialized = {
166
+ metrics: this.serializeMetrics(data.metrics),
167
+ unlockedAchievements: data.unlockedAchievements
168
+ };
169
+ const bytesNeeded = JSON.stringify(serialized).length;
170
+ throw new StorageQuotaError(bytesNeeded);
171
+ }
172
+ throw new StorageError(`Failed to save achievement data: ${error.message}`, error);
79
173
  }
80
- throw error;
174
+ throw new StorageError('Failed to save achievement data');
81
175
  }
82
176
  }
83
177
  getMetrics() {
@@ -349,8 +443,219 @@ function normalizeAchievements(config) {
349
443
  return normalized;
350
444
  }
351
445
 
446
+ /**
447
+ * Exports achievement data to a JSON string
448
+ *
449
+ * @param metrics - Current achievement metrics
450
+ * @param unlocked - Array of unlocked achievement IDs
451
+ * @param configHash - Optional hash of achievement configuration for validation
452
+ * @returns JSON string containing all achievement data
453
+ *
454
+ * @example
455
+ * ```typescript
456
+ * const json = exportAchievementData(metrics, ['score_100', 'level_5']);
457
+ * // Save json to file or send to server
458
+ * ```
459
+ */
460
+ function exportAchievementData(metrics, unlocked, configHash) {
461
+ const data = Object.assign({ version: '3.3.0', timestamp: Date.now(), metrics, unlockedAchievements: unlocked }, (configHash && { configHash }));
462
+ return JSON.stringify(data, null, 2);
463
+ }
464
+ /**
465
+ * Creates a simple hash of the achievement configuration
466
+ * Used to validate that imported data matches the current configuration
467
+ *
468
+ * @param config - Achievement configuration object
469
+ * @returns Simple hash string
470
+ */
471
+ function createConfigHash(config) {
472
+ // Simple hash based on stringified config
473
+ // In production, you might want to use a more robust hashing algorithm
474
+ const str = JSON.stringify(config);
475
+ let hash = 0;
476
+ for (let i = 0; i < str.length; i++) {
477
+ const char = str.charCodeAt(i);
478
+ hash = ((hash << 5) - hash) + char;
479
+ hash = hash & hash; // Convert to 32bit integer
480
+ }
481
+ return hash.toString(36);
482
+ }
483
+
484
+ /**
485
+ * Imports achievement data from a JSON string
486
+ *
487
+ * @param jsonString - JSON string containing exported achievement data
488
+ * @param currentMetrics - Current metrics state
489
+ * @param currentUnlocked - Current unlocked achievements
490
+ * @param options - Import options
491
+ * @returns Import result with success status and any errors
492
+ *
493
+ * @example
494
+ * ```typescript
495
+ * const result = importAchievementData(
496
+ * jsonString,
497
+ * currentMetrics,
498
+ * currentUnlocked,
499
+ * { mergeStrategy: 'merge', validate: true }
500
+ * );
501
+ *
502
+ * if (result.success) {
503
+ * console.log(`Imported ${result.imported.achievements} achievements`);
504
+ * } else {
505
+ * console.error('Import failed:', result.errors);
506
+ * }
507
+ * ```
508
+ */
509
+ function importAchievementData(jsonString, currentMetrics, currentUnlocked, options = {}) {
510
+ const { mergeStrategy = 'replace', validate = true, expectedConfigHash } = options;
511
+ const warnings = [];
512
+ // Parse JSON
513
+ let data;
514
+ try {
515
+ data = JSON.parse(jsonString);
516
+ }
517
+ catch (error) {
518
+ return {
519
+ success: false,
520
+ imported: { metrics: 0, achievements: 0 },
521
+ errors: ['Invalid JSON format']
522
+ };
523
+ }
524
+ // Validate structure
525
+ if (validate) {
526
+ const validationErrors = validateExportedData(data, expectedConfigHash);
527
+ if (validationErrors.length > 0) {
528
+ return {
529
+ success: false,
530
+ imported: { metrics: 0, achievements: 0 },
531
+ errors: validationErrors
532
+ };
533
+ }
534
+ }
535
+ // Version compatibility check
536
+ if (data.version && data.version !== '3.3.0') {
537
+ warnings.push(`Data exported from version ${data.version}, current version is 3.3.0`);
538
+ }
539
+ // Merge metrics based on strategy
540
+ let mergedMetrics;
541
+ let mergedUnlocked;
542
+ switch (mergeStrategy) {
543
+ case 'replace':
544
+ // Replace all existing data
545
+ mergedMetrics = data.metrics;
546
+ mergedUnlocked = data.unlockedAchievements;
547
+ break;
548
+ case 'merge':
549
+ // Union of both datasets, keeping higher metric values
550
+ mergedMetrics = mergeMetrics(currentMetrics, data.metrics);
551
+ mergedUnlocked = Array.from(new Set([...currentUnlocked, ...data.unlockedAchievements]));
552
+ break;
553
+ case 'preserve':
554
+ // Keep existing values, only add new ones
555
+ mergedMetrics = preserveMetrics(currentMetrics, data.metrics);
556
+ mergedUnlocked = Array.from(new Set([...currentUnlocked, ...data.unlockedAchievements]));
557
+ break;
558
+ default:
559
+ return {
560
+ success: false,
561
+ imported: { metrics: 0, achievements: 0 },
562
+ errors: [`Invalid merge strategy: ${mergeStrategy}`]
563
+ };
564
+ }
565
+ return Object.assign(Object.assign({ success: true, imported: {
566
+ metrics: Object.keys(mergedMetrics).length,
567
+ achievements: mergedUnlocked.length
568
+ } }, (warnings.length > 0 && { warnings })), { mergedMetrics,
569
+ mergedUnlocked });
570
+ }
571
+ /**
572
+ * Validates the structure and content of exported data
573
+ */
574
+ function validateExportedData(data, expectedConfigHash) {
575
+ const errors = [];
576
+ // Check required fields
577
+ if (!data.version) {
578
+ errors.push('Missing version field');
579
+ }
580
+ if (!data.timestamp) {
581
+ errors.push('Missing timestamp field');
582
+ }
583
+ if (!data.metrics || typeof data.metrics !== 'object') {
584
+ errors.push('Missing or invalid metrics field');
585
+ }
586
+ if (!Array.isArray(data.unlockedAchievements)) {
587
+ errors.push('Missing or invalid unlockedAchievements field');
588
+ }
589
+ // Validate config hash if provided
590
+ if (expectedConfigHash && data.configHash && data.configHash !== expectedConfigHash) {
591
+ errors.push('Configuration mismatch: imported data may not be compatible with current achievement configuration');
592
+ }
593
+ // Validate metrics structure
594
+ if (data.metrics && typeof data.metrics === 'object') {
595
+ for (const [key, value] of Object.entries(data.metrics)) {
596
+ if (!Array.isArray(value)) {
597
+ errors.push(`Invalid metric format for "${key}": expected array, got ${typeof value}`);
598
+ }
599
+ }
600
+ }
601
+ // Validate achievement IDs are strings
602
+ if (Array.isArray(data.unlockedAchievements)) {
603
+ const invalidIds = data.unlockedAchievements.filter((id) => typeof id !== 'string');
604
+ if (invalidIds.length > 0) {
605
+ errors.push('All achievement IDs must be strings');
606
+ }
607
+ }
608
+ return errors;
609
+ }
610
+ /**
611
+ * Merges two metrics objects, keeping higher values for overlapping keys
612
+ */
613
+ function mergeMetrics(current, imported) {
614
+ const merged = Object.assign({}, current);
615
+ for (const [key, importedValues] of Object.entries(imported)) {
616
+ if (!merged[key]) {
617
+ // New metric, add it
618
+ merged[key] = importedValues;
619
+ }
620
+ else {
621
+ // Existing metric, merge values
622
+ merged[key] = mergeMetricValues(merged[key], importedValues);
623
+ }
624
+ }
625
+ return merged;
626
+ }
627
+ /**
628
+ * Merges two metric value arrays, keeping higher numeric values
629
+ */
630
+ function mergeMetricValues(current, imported) {
631
+ // For simplicity, we'll use the imported values if they're "higher"
632
+ // This works for numeric values; for other types, we prefer imported
633
+ const currentValue = current[0];
634
+ const importedValue = imported[0];
635
+ // If both are numbers, keep the higher one
636
+ if (typeof currentValue === 'number' && typeof importedValue === 'number') {
637
+ return currentValue >= importedValue ? current : imported;
638
+ }
639
+ // For non-numeric values, prefer imported (assume it's newer)
640
+ return imported;
641
+ }
642
+ /**
643
+ * Preserves existing metrics, only adding new ones from imported data
644
+ */
645
+ function preserveMetrics(current, imported) {
646
+ const preserved = Object.assign({}, current);
647
+ for (const [key, value] of Object.entries(imported)) {
648
+ if (!preserved[key]) {
649
+ // Only add if it doesn't exist
650
+ preserved[key] = value;
651
+ }
652
+ // If it exists, keep current value (preserve strategy)
653
+ }
654
+ return preserved;
655
+ }
656
+
352
657
  const AchievementContext = createContext(undefined);
353
- const AchievementProvider = ({ achievements: achievementsConfig, storage = StorageType.Local, children, icons = {}, }) => {
658
+ const AchievementProvider = ({ achievements: achievementsConfig, storage = StorageType.Local, children, icons = {}, onError, }) => {
354
659
  // Normalize the configuration to the complex format
355
660
  const achievements = normalizeAchievements(achievementsConfig);
356
661
  const [achievementState, setAchievementState] = useState({
@@ -520,7 +825,22 @@ const AchievementProvider = ({ achievements: achievementsConfig, storage = Stora
520
825
  });
521
826
  setMetrics(updatedMetrics);
522
827
  const storageMetrics = Object.entries(updatedMetrics).reduce((acc, [key, value]) => (Object.assign(Object.assign({}, acc), { [key]: Array.isArray(value) ? value : [value] })), {});
523
- storageImpl.setMetrics(storageMetrics);
828
+ try {
829
+ storageImpl.setMetrics(storageMetrics);
830
+ }
831
+ catch (error) {
832
+ if (error instanceof AchievementError) {
833
+ if (onError) {
834
+ onError(error);
835
+ }
836
+ else {
837
+ console.error('Achievement storage error:', error.message, error.remedy);
838
+ }
839
+ }
840
+ else {
841
+ console.error('Unexpected error saving metrics:', error);
842
+ }
843
+ }
524
844
  };
525
845
  const reset = () => {
526
846
  var _a, _b;
@@ -547,11 +867,41 @@ const AchievementProvider = ({ achievements: achievementsConfig, storage = Stora
547
867
  unlocked: achievementState.unlocked,
548
868
  };
549
869
  };
870
+ const exportData = () => {
871
+ const state = getState();
872
+ const configHash = createConfigHash(achievementsConfig);
873
+ return exportAchievementData(state.metrics, state.unlocked, configHash);
874
+ };
875
+ const importData = (jsonString, options) => {
876
+ const state = getState();
877
+ const configHash = createConfigHash(achievementsConfig);
878
+ const result = importAchievementData(jsonString, state.metrics, state.unlocked, Object.assign(Object.assign({}, options), { expectedConfigHash: configHash }));
879
+ if (result.success && 'mergedMetrics' in result && 'mergedUnlocked' in result) {
880
+ // Apply the imported data
881
+ const mergedResult = result;
882
+ // Update metrics state
883
+ const metricsFromArrayFormat = Object.entries(mergedResult.mergedMetrics).reduce((acc, [key, value]) => (Object.assign(Object.assign({}, acc), { [key]: Array.isArray(value) ? value[0] : value })), {});
884
+ setMetrics(metricsFromArrayFormat);
885
+ // Update unlocked achievements state
886
+ setAchievementState(prev => (Object.assign(Object.assign({}, prev), { unlocked: mergedResult.mergedUnlocked })));
887
+ // Persist to storage
888
+ storageImpl.setMetrics(mergedResult.mergedMetrics);
889
+ storageImpl.setUnlockedAchievements(mergedResult.mergedUnlocked);
890
+ // Update seen achievements to prevent duplicate notifications
891
+ mergedResult.mergedUnlocked.forEach(id => {
892
+ seenAchievementsRef.current.add(id);
893
+ });
894
+ saveNotifiedAchievements(seenAchievementsRef.current);
895
+ }
896
+ return result;
897
+ };
550
898
  return (React.createElement(AchievementContext.Provider, { value: {
551
899
  update,
552
900
  achievements: achievementState,
553
901
  reset,
554
902
  getState,
903
+ exportData,
904
+ importData,
555
905
  } },
556
906
  children,
557
907
  React.createElement(ToastContainer, { position: "top-right", autoClose: 5000, hideProgressBar: false, newestOnTop: true, closeOnClick: true, rtl: false, pauseOnFocusLoss: true, draggable: true, pauseOnHover: true, theme: "light" }),
@@ -571,7 +921,7 @@ const useAchievements = () => {
571
921
  * Provides an easier API for common use cases while maintaining access to advanced features.
572
922
  */
573
923
  const useSimpleAchievements = () => {
574
- const { update, achievements, reset, getState } = useAchievements();
924
+ const { update, achievements, reset, getState, exportData, importData } = useAchievements();
575
925
  return {
576
926
  /**
577
927
  * Track a metric value for achievements
@@ -616,6 +966,18 @@ const useSimpleAchievements = () => {
616
966
  * Get current state (advanced usage)
617
967
  */
618
968
  getState,
969
+ /**
970
+ * Export achievement data to JSON string
971
+ * @returns JSON string containing all achievement data
972
+ */
973
+ exportData,
974
+ /**
975
+ * Import achievement data from JSON string
976
+ * @param jsonString - JSON string containing exported achievement data
977
+ * @param options - Import options (merge strategy, validation)
978
+ * @returns Import result with success status and any errors
979
+ */
980
+ importData,
619
981
  };
620
982
  };
621
983
 
@@ -982,5 +1344,5 @@ class AchievementBuilder {
982
1344
  }
983
1345
  }
984
1346
 
985
- export { AchievementBuilder, AchievementContext, AchievementProvider, BadgesButton, BadgesModal, ConfettiWrapper, LocalStorage, MemoryStorage, StorageType, defaultAchievementIcons, defaultStyles, isSimpleConfig, normalizeAchievements, useAchievements, useSimpleAchievements };
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 };
986
1348
  //# sourceMappingURL=index.js.map