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/README.md +688 -0
- package/dist/index.d.ts +152 -1
- package/dist/index.js +373 -11
- package/dist/index.js.map +1 -1
- package/dist/types/core/errors/AchievementErrors.d.ts +51 -0
- package/dist/types/core/utils/dataExport.d.ts +34 -0
- package/dist/types/core/utils/dataImport.d.ts +50 -0
- package/dist/types/hooks/useSimpleAchievements.d.ts +12 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/providers/AchievementProvider.d.ts +5 -0
- package/package.json +2 -1
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
|
-
|
|
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
|
-
|
|
148
|
+
const jsonString = JSON.stringify(serialized);
|
|
149
|
+
localStorage.setItem(this.storageKey, jsonString);
|
|
71
150
|
}
|
|
72
151
|
catch (error) {
|
|
73
|
-
//
|
|
74
|
-
if (error instanceof
|
|
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
|
-
|
|
78
|
-
|
|
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
|
|
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
|
-
|
|
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
|