react-achievements 4.4.1 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/web.cjs CHANGED
@@ -47,6 +47,16 @@ function __rest(s, e) {
47
47
  return t;
48
48
  }
49
49
 
50
+ function __awaiter(thisArg, _arguments, P, generator) {
51
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
52
+ return new (P || (P = Promise))(function (resolve, reject) {
53
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
54
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
55
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
56
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
57
+ });
58
+ }
59
+
50
60
  typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
51
61
  var e = new Error(message);
52
62
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
@@ -68,18 +78,74 @@ const AchievementContext = React.createContext(undefined);
68
78
  const getAllAchievementRecord = (achievements) => {
69
79
  return Object.fromEntries(achievements.map((achievement) => [achievement.achievementId, achievement]));
70
80
  };
71
- const AchievementProvider$1 = ({ achievements: achievementsConfig, storage = 'local', children, onError, useBuiltInUI, restApiConfig, engine: externalEngine, eventMapping, icons = {}, }) => {
81
+ const emptySnapshot = () => ({
82
+ metrics: {},
83
+ unlockedIds: [],
84
+ unlockedAchievements: [],
85
+ allAchievements: [],
86
+ unlockedCount: 0,
87
+ totalCount: 0,
88
+ });
89
+ const normalizeAchievementDto = (achievement) => {
90
+ const source = achievement;
91
+ const achievementId = source.id || source.achievementId || '';
92
+ const achievementTitle = source.title || source.achievementTitle || achievementId;
93
+ const achievementDescription = source.description || source.achievementDescription || '';
94
+ const achievementIconKey = source.iconKey || source.icon || source.achievementIconKey;
95
+ return {
96
+ achievementId,
97
+ achievementTitle,
98
+ achievementDescription,
99
+ achievementIconKey,
100
+ confetti: source.confetti,
101
+ isUnlocked: Boolean(source.isUnlocked),
102
+ unlockedAt: source.unlockedAt,
103
+ progress: source.progress,
104
+ metadata: source.metadata,
105
+ };
106
+ };
107
+ const normalizeClientSnapshot = (snapshot) => {
108
+ var _a, _b, _c;
109
+ const allAchievements = (snapshot.achievements || []).map(normalizeAchievementDto);
110
+ const unlockedIds = snapshot.unlockedIds ||
111
+ allAchievements
112
+ .filter((achievement) => achievement.isUnlocked)
113
+ .map((achievement) => achievement.achievementId);
114
+ const unlockedIdSet = new Set(unlockedIds);
115
+ const unlockedAchievements = ((_a = snapshot.unlockedAchievements) === null || _a === void 0 ? void 0 : _a.length)
116
+ ? snapshot.unlockedAchievements.map(normalizeAchievementDto)
117
+ : allAchievements.filter((achievement) => unlockedIdSet.has(achievement.achievementId));
118
+ return {
119
+ metrics: Object.assign({}, (snapshot.metrics || {})),
120
+ unlockedIds,
121
+ unlockedAchievements,
122
+ allAchievements,
123
+ unlockedCount: (_b = snapshot.unlockedCount) !== null && _b !== void 0 ? _b : unlockedAchievements.length,
124
+ totalCount: (_c = snapshot.totalCount) !== null && _c !== void 0 ? _c : allAchievements.length,
125
+ };
126
+ };
127
+ const toError = (unknownError, fallbackMessage) => (unknownError instanceof Error ? unknownError : new Error(fallbackMessage));
128
+ const AchievementProvider$1 = ({ client, achievements: achievementsConfig, storage = 'local', children, onError, useBuiltInUI, restApiConfig, engine: externalEngine, eventMapping, icons = {}, }) => {
72
129
  if (useBuiltInUI !== undefined) {
73
130
  warnDeprecation('`useBuiltInUI` is deprecated and is now a no-op because built-in UI is the default. It will be removed in 5.0.');
74
131
  }
132
+ if (client && (achievementsConfig || externalEngine)) {
133
+ throw new Error('Cannot provide "client" with "achievements" or "engine" props to AchievementProvider.\n\n' +
134
+ 'Use one pattern:\n' +
135
+ '1. Server-backed display: <AchievementProvider client={achievementClient}>\n' +
136
+ '2. Legacy in-browser engine: <AchievementProvider achievements={config}>');
137
+ }
75
138
  if (achievementsConfig && externalEngine) {
76
139
  throw new Error('Cannot provide both "achievements" and "engine" props to AchievementProvider.\n\n' +
77
140
  'Choose one pattern:\n' +
78
141
  '1. Direct metric tracking: <AchievementProvider achievements={config}>\n' +
79
142
  '2. Event-based tracking: <AchievementProvider engine={myEngine}>');
80
143
  }
81
- const isProviderCreatedEngine = Boolean(achievementsConfig);
144
+ const isProviderCreatedEngine = Boolean(achievementsConfig) && !client;
82
145
  const [engine] = React.useState(() => {
146
+ if (client) {
147
+ return undefined;
148
+ }
83
149
  if (externalEngine) {
84
150
  return externalEngine;
85
151
  }
@@ -96,18 +162,63 @@ const AchievementProvider$1 = ({ achievements: achievementsConfig, storage = 'lo
96
162
  eventMapping,
97
163
  });
98
164
  });
99
- const [achievementSnapshot, setAchievementSnapshot] = React.useState(() => engine.getSnapshot());
165
+ const [achievementSnapshot, setAchievementSnapshot] = React.useState(() => engine ? engine.getSnapshot() : emptySnapshot());
166
+ const [recentlyUnlocked, setRecentlyUnlocked] = React.useState([]);
167
+ const [isLoading, setIsLoading] = React.useState(Boolean(client));
168
+ const [error, setError] = React.useState(null);
100
169
  const syncAchievementState = React.useCallback((snapshot) => {
101
- setAchievementSnapshot(snapshot || engine.getSnapshot());
170
+ if (snapshot) {
171
+ setAchievementSnapshot(snapshot);
172
+ return;
173
+ }
174
+ if (engine) {
175
+ setAchievementSnapshot(engine.getSnapshot());
176
+ }
102
177
  }, [engine]);
178
+ const applyClientMutationResult = React.useCallback((result) => {
179
+ setRecentlyUnlocked((result.newlyUnlocked || []).map(normalizeAchievementDto));
180
+ const normalizedSnapshot = normalizeClientSnapshot(result.snapshot);
181
+ setAchievementSnapshot(normalizedSnapshot);
182
+ return result;
183
+ }, []);
184
+ const refresh = React.useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
185
+ if (!client) {
186
+ const snapshot = engine ? engine.getSnapshot() : emptySnapshot();
187
+ setAchievementSnapshot(snapshot);
188
+ return snapshot;
189
+ }
190
+ setIsLoading(true);
191
+ setError(null);
192
+ try {
193
+ const snapshot = normalizeClientSnapshot(yield client.getSnapshot());
194
+ setAchievementSnapshot(snapshot);
195
+ return snapshot;
196
+ }
197
+ catch (unknownError) {
198
+ const nextError = toError(unknownError, 'Failed to load achievements');
199
+ setError(nextError);
200
+ throw nextError;
201
+ }
202
+ finally {
203
+ setIsLoading(false);
204
+ }
205
+ }), [client, engine]);
206
+ const handleClientMutationFailure = React.useCallback((unknownError) => {
207
+ const nextError = toError(unknownError, 'Failed to update achievements');
208
+ setError(nextError);
209
+ throw nextError;
210
+ }, []);
103
211
  React.useEffect(() => {
104
212
  return () => {
105
- if (!externalEngine) {
213
+ if (engine && !externalEngine) {
106
214
  engine.destroy();
107
215
  }
108
216
  };
109
217
  }, [engine, externalEngine]);
110
218
  React.useEffect(() => {
219
+ if (!engine) {
220
+ return;
221
+ }
111
222
  let isMounted = true;
112
223
  const unsubscribeStateChanged = engine.on('state:changed', (event) => {
113
224
  syncAchievementState(event);
@@ -122,29 +233,84 @@ const AchievementProvider$1 = ({ achievements: achievementsConfig, storage = 'lo
122
233
  unsubscribeStateChanged();
123
234
  };
124
235
  }, [engine, syncAchievementState]);
236
+ React.useEffect(() => {
237
+ if (!client) {
238
+ return;
239
+ }
240
+ refresh().catch(() => {
241
+ });
242
+ return () => {
243
+ };
244
+ }, [client, refresh]);
125
245
  const update = (newMetrics) => {
126
- engine.update(newMetrics);
246
+ if (client) {
247
+ setError(null);
248
+ const mutation = client.trackMany
249
+ ? client.trackMany(newMetrics)
250
+ : Promise.all(Object.entries(newMetrics).map(([metric, value]) => client.track(metric, value))).then((results) => results[results.length - 1]);
251
+ return mutation.then((result) => {
252
+ if (result) {
253
+ applyClientMutationResult(result);
254
+ }
255
+ return result;
256
+ }).catch(handleClientMutationFailure);
257
+ }
258
+ return engine === null || engine === void 0 ? void 0 : engine.update(newMetrics);
259
+ };
260
+ const increment = (metric, amount = 1) => {
261
+ if (client) {
262
+ setError(null);
263
+ return client.increment(metric, amount)
264
+ .then(applyClientMutationResult)
265
+ .catch(handleClientMutationFailure);
266
+ }
267
+ return engine === null || engine === void 0 ? void 0 : engine.increment(metric, amount);
268
+ };
269
+ const event = (name, payload) => {
270
+ if (client) {
271
+ setError(null);
272
+ return client.event(name, payload)
273
+ .then(applyClientMutationResult)
274
+ .catch(handleClientMutationFailure);
275
+ }
276
+ engine === null || engine === void 0 ? void 0 : engine.emit(name, payload);
127
277
  };
128
278
  const reset = () => {
129
- engine.reset();
279
+ if (client === null || client === void 0 ? void 0 : client.reset) {
280
+ setError(null);
281
+ return client.reset().then((snapshot) => {
282
+ setRecentlyUnlocked([]);
283
+ setAchievementSnapshot(normalizeClientSnapshot(snapshot));
284
+ }).catch(handleClientMutationFailure);
285
+ }
286
+ if (client) {
287
+ return refresh().then(() => undefined);
288
+ }
289
+ engine === null || engine === void 0 ? void 0 : engine.reset();
130
290
  };
131
291
  const getState = () => {
132
- const snapshot = engine.getSnapshot();
292
+ const snapshot = engine ? engine.getSnapshot() : achievementSnapshot;
133
293
  return {
134
294
  metrics: snapshot.metrics,
135
295
  unlocked: snapshot.unlockedIds,
136
296
  };
137
297
  };
138
298
  const exportData = () => {
139
- return engine.export();
299
+ return engine ? engine.export() : JSON.stringify(achievementSnapshot);
140
300
  };
141
301
  const importData = (jsonString, options) => {
302
+ if (!engine) {
303
+ return {
304
+ success: false,
305
+ errors: ['importData is not supported by remote achievement clients. Import on the server instead.'],
306
+ };
307
+ }
142
308
  const result = engine.import(jsonString, options);
143
309
  syncAchievementState();
144
310
  return result;
145
311
  };
146
312
  const getAllAchievements = () => {
147
- return engine.getSnapshot().allAchievements;
313
+ return engine ? engine.getSnapshot().allAchievements : achievementSnapshot.allAchievements;
148
314
  };
149
315
  const achievements = {
150
316
  unlocked: achievementSnapshot.unlockedIds,
@@ -152,6 +318,9 @@ const AchievementProvider$1 = ({ achievements: achievementsConfig, storage = 'lo
152
318
  };
153
319
  return (React.createElement(AchievementContext.Provider, { value: {
154
320
  update,
321
+ increment,
322
+ event,
323
+ refresh,
155
324
  achievements,
156
325
  snapshot: achievementSnapshot,
157
326
  reset,
@@ -160,27 +329,21 @@ const AchievementProvider$1 = ({ achievements: achievementsConfig, storage = 'lo
160
329
  importData,
161
330
  getAllAchievements,
162
331
  engine,
332
+ client,
163
333
  icons,
334
+ recentlyUnlocked,
335
+ isLoading,
336
+ error,
164
337
  _isLegacyPattern: isProviderCreatedEngine,
165
338
  } }, children));
166
339
  };
167
340
 
168
- /**
169
- * Access the active AchievementEngine instance.
170
- *
171
- * In v4 this works with both provider-created engines (`achievements` prop) and
172
- * injected engines (`engine` prop).
173
- */
174
- const useAchievementEngine = () => {
341
+ const useAchievements = () => {
175
342
  const context = React.useContext(AchievementContext);
176
343
  if (!context) {
177
- throw new Error('useAchievementEngine must be used within an AchievementProvider.\n\n' +
178
- 'Wrap your component tree:\n' +
179
- '<AchievementProvider achievements={achievements}>\n' +
180
- ' <YourComponent />\n' +
181
- '</AchievementProvider>');
344
+ throw new Error('useAchievements must be used within an AchievementProvider');
182
345
  }
183
- return context.engine;
346
+ return context;
184
347
  };
185
348
 
186
349
  /**
@@ -558,8 +721,9 @@ const AchievementUIContext = React.createContext({
558
721
  });
559
722
  const AchievementEffects = ({ icons, ui, achievementConfetti }) => {
560
723
  var _a, _b;
561
- const engine = useAchievementEngine();
562
- const seenAchievementsRef = React.useRef(new Set(engine.getUnlocked()));
724
+ const achievementContext = useAchievements();
725
+ const engine = achievementContext.engine;
726
+ const seenAchievementsRef = React.useRef(new Set(achievementContext.snapshot.unlockedIds));
563
727
  const confettiTimerRef = React.useRef(null);
564
728
  const achievementConfettiRef = React.useRef(achievementConfetti);
565
729
  const [showConfetti, setShowConfetti] = React.useState(false);
@@ -588,42 +752,47 @@ const AchievementEffects = ({ icons, ui, achievementConfetti }) => {
588
752
  setActiveConfettiConfig(null);
589
753
  }
590
754
  }, [ui.enableConfetti]);
755
+ const showUnlockedAchievement = React.useCallback((unlockedAchievement) => {
756
+ var _a, _b;
757
+ if (seenAchievementsRef.current.has(unlockedAchievement.achievementId)) {
758
+ return;
759
+ }
760
+ seenAchievementsRef.current.add(unlockedAchievement.achievementId);
761
+ if (ui.enableNotifications !== false) {
762
+ setNotifications((currentNotifications) => {
763
+ if (currentNotifications.some((notification) => notification.achievementId === unlockedAchievement.achievementId)) {
764
+ return currentNotifications;
765
+ }
766
+ return [...currentNotifications, unlockedAchievement];
767
+ });
768
+ }
769
+ const rewardConfetti = (_a = unlockedAchievement.confetti) !== null && _a !== void 0 ? _a : achievementConfettiRef.current.get(unlockedAchievement.achievementId);
770
+ if (ui.enableConfetti !== false && rewardConfetti !== false) {
771
+ if (confettiTimerRef.current) {
772
+ clearTimeout(confettiTimerRef.current);
773
+ }
774
+ const resolvedConfettiConfig = Object.assign(Object.assign({}, globalConfettiConfigRef.current), (rewardConfetti || {}));
775
+ const resolvedConfettiDuration = (_b = resolvedConfettiConfig.duration) !== null && _b !== void 0 ? _b : CONFETTI_DURATION_MS;
776
+ setActiveConfettiConfig(resolvedConfettiConfig);
777
+ setShowConfetti(true);
778
+ confettiTimerRef.current = setTimeout(() => {
779
+ setShowConfetti(false);
780
+ confettiTimerRef.current = null;
781
+ }, resolvedConfettiDuration);
782
+ }
783
+ }, [ui.enableConfetti, ui.enableNotifications]);
591
784
  React.useEffect(() => {
785
+ if (!engine) {
786
+ return;
787
+ }
592
788
  const unsubscribeUnlocked = engine.on('achievement:unlocked', (event) => {
593
- var _a;
594
- if (seenAchievementsRef.current.has(event.achievementId)) {
595
- return;
596
- }
597
- seenAchievementsRef.current.add(event.achievementId);
598
- const unlockedAchievement = {
789
+ showUnlockedAchievement({
599
790
  achievementId: event.achievementId,
600
791
  achievementTitle: event.achievementTitle,
601
792
  achievementDescription: event.achievementDescription,
602
793
  achievementIconKey: event.achievementIconKey,
603
794
  isUnlocked: true,
604
- };
605
- if (ui.enableNotifications !== false) {
606
- setNotifications((currentNotifications) => {
607
- if (currentNotifications.some((notification) => notification.achievementId === unlockedAchievement.achievementId)) {
608
- return currentNotifications;
609
- }
610
- return [...currentNotifications, unlockedAchievement];
611
- });
612
- }
613
- const rewardConfetti = achievementConfettiRef.current.get(event.achievementId);
614
- if (ui.enableConfetti !== false && rewardConfetti !== false) {
615
- if (confettiTimerRef.current) {
616
- clearTimeout(confettiTimerRef.current);
617
- }
618
- const resolvedConfettiConfig = Object.assign(Object.assign({}, globalConfettiConfigRef.current), (rewardConfetti || {}));
619
- const resolvedConfettiDuration = (_a = resolvedConfettiConfig.duration) !== null && _a !== void 0 ? _a : CONFETTI_DURATION_MS;
620
- setActiveConfettiConfig(resolvedConfettiConfig);
621
- setShowConfetti(true);
622
- confettiTimerRef.current = setTimeout(() => {
623
- setShowConfetti(false);
624
- confettiTimerRef.current = null;
625
- }, resolvedConfettiDuration);
626
- }
795
+ });
627
796
  });
628
797
  const unsubscribeStateChanged = engine.on('state:changed', () => {
629
798
  const unlocked = new Set(engine.getUnlocked());
@@ -640,7 +809,18 @@ const AchievementEffects = ({ icons, ui, achievementConfetti }) => {
640
809
  clearTimeout(confettiTimerRef.current);
641
810
  }
642
811
  };
643
- }, [engine, ui.enableConfetti, ui.enableNotifications]);
812
+ }, [engine, showUnlockedAchievement]);
813
+ React.useEffect(() => {
814
+ achievementContext.recentlyUnlocked.forEach(showUnlockedAchievement);
815
+ }, [achievementContext.recentlyUnlocked, showUnlockedAchievement]);
816
+ React.useEffect(() => {
817
+ const unlocked = new Set(achievementContext.snapshot.unlockedIds);
818
+ seenAchievementsRef.current.forEach((achievementId) => {
819
+ if (!unlocked.has(achievementId)) {
820
+ seenAchievementsRef.current.delete(achievementId);
821
+ }
822
+ });
823
+ }, [achievementContext.snapshot.unlockedIds]);
644
824
  const NotificationComponent = ui.NotificationComponent || BuiltInNotification;
645
825
  const ConfettiComponentResolved = ui.ConfettiComponent || BuiltInConfetti;
646
826
  return (React.createElement(React.Fragment, null,
@@ -661,14 +841,6 @@ const AchievementProvider = (_a) => {
661
841
  React.createElement(AchievementEffects, { achievementConfetti: confettiByAchievementId, icons: icons, ui: ui }))));
662
842
  };
663
843
 
664
- const useAchievements = () => {
665
- const context = React.useContext(AchievementContext);
666
- if (!context) {
667
- throw new Error('useAchievements must be used within an AchievementProvider');
668
- }
669
- return context;
670
- };
671
-
672
844
  const useAchievementState = () => {
673
845
  const { snapshot } = useAchievements();
674
846
  return {
@@ -1188,11 +1360,11 @@ const LevelProgress = ({ level, currentXP, nextLevelXP, label = 'Level', valueLa
1188
1360
  * Provides the v4 happy path for direct metric updates plus explicit state names.
1189
1361
  */
1190
1362
  const useSimpleAchievements = () => {
1191
- const { update, reset, getState, exportData, importData, engine } = useAchievements();
1363
+ const { update, increment: incrementMetric, event, refresh, reset, getState, exportData, importData, isLoading, error, } = useAchievements();
1192
1364
  const achievementState = useAchievementState();
1193
1365
  const track = (metric, value) => update({ [metric]: value });
1194
1366
  const increment = (metric, amount = 1) => {
1195
- return engine.increment(metric, amount);
1367
+ return incrementMetric(metric, amount);
1196
1368
  };
1197
1369
  const trackMultiple = (metrics) => update(metrics);
1198
1370
  return {
@@ -1205,6 +1377,11 @@ const useSimpleAchievements = () => {
1205
1377
  unlockedCount: achievementState.unlockedCount,
1206
1378
  totalCount: achievementState.totalCount,
1207
1379
  metrics: achievementState.metrics,
1380
+ isLoading,
1381
+ error,
1382
+ event,
1383
+ emit: event,
1384
+ refresh,
1208
1385
  reset,
1209
1386
  getState,
1210
1387
  exportData,
@@ -1221,6 +1398,110 @@ const useSimpleAchievements = () => {
1221
1398
  };
1222
1399
  };
1223
1400
 
1401
+ /**
1402
+ * Access the active AchievementEngine instance.
1403
+ *
1404
+ * In v4 this works with both provider-created engines (`achievements` prop) and
1405
+ * injected engines (`engine` prop).
1406
+ */
1407
+ const useAchievementEngine = () => {
1408
+ const context = React.useContext(AchievementContext);
1409
+ if (!context) {
1410
+ throw new Error('useAchievementEngine must be used within an AchievementProvider.\n\n' +
1411
+ 'Wrap your component tree:\n' +
1412
+ '<AchievementProvider achievements={achievements}>\n' +
1413
+ ' <YourComponent />\n' +
1414
+ '</AchievementProvider>');
1415
+ }
1416
+ if (!context.engine) {
1417
+ throw new Error('useAchievementEngine is only available when AchievementProvider is using the legacy in-browser engine. ' +
1418
+ 'For server-backed achievements, use useSimpleAchievements().event(), track(), or increment().');
1419
+ }
1420
+ return context.engine;
1421
+ };
1422
+
1423
+ const trimTrailingSlash = (value) => value.replace(/\/$/, '');
1424
+ class RestAchievementClient {
1425
+ constructor(config) {
1426
+ this.baseUrl = trimTrailingSlash(config.baseUrl);
1427
+ this.fetcher = config.fetcher || fetch.bind(globalThis);
1428
+ this.headers = config.headers;
1429
+ this.credentials = config.credentials;
1430
+ this.timeout = config.timeout;
1431
+ }
1432
+ getSnapshot() {
1433
+ return __awaiter(this, void 0, void 0, function* () {
1434
+ return this.request('', { method: 'GET' });
1435
+ });
1436
+ }
1437
+ track(metric, value) {
1438
+ return __awaiter(this, void 0, void 0, function* () {
1439
+ return this.request('/track', {
1440
+ method: 'POST',
1441
+ body: JSON.stringify({ metric, value }),
1442
+ });
1443
+ });
1444
+ }
1445
+ trackMany(metrics) {
1446
+ return __awaiter(this, void 0, void 0, function* () {
1447
+ return this.request('/track', {
1448
+ method: 'POST',
1449
+ body: JSON.stringify({ metrics }),
1450
+ });
1451
+ });
1452
+ }
1453
+ increment(metric_1) {
1454
+ return __awaiter(this, arguments, void 0, function* (metric, amount = 1) {
1455
+ return this.request('/increment', {
1456
+ method: 'POST',
1457
+ body: JSON.stringify({ metric, amount }),
1458
+ });
1459
+ });
1460
+ }
1461
+ event(name, payload) {
1462
+ return __awaiter(this, void 0, void 0, function* () {
1463
+ return this.request('/event', {
1464
+ method: 'POST',
1465
+ body: JSON.stringify({ name, payload }),
1466
+ });
1467
+ });
1468
+ }
1469
+ reset() {
1470
+ return __awaiter(this, void 0, void 0, function* () {
1471
+ return this.request('/reset', { method: 'POST' });
1472
+ });
1473
+ }
1474
+ request(path, init) {
1475
+ return __awaiter(this, void 0, void 0, function* () {
1476
+ const controller = this.timeout ? new AbortController() : undefined;
1477
+ const timeoutId = controller
1478
+ ? setTimeout(() => controller.abort(), this.timeout)
1479
+ : undefined;
1480
+ try {
1481
+ const response = yield this.fetcher(`${this.baseUrl}${path}`, Object.assign(Object.assign({}, init), { credentials: this.credentials, headers: Object.assign(Object.assign({ 'Content-Type': 'application/json' }, (yield this.resolveHeaders())), init.headers), signal: controller === null || controller === void 0 ? void 0 : controller.signal }));
1482
+ if (!response.ok) {
1483
+ throw new Error(`Achievement request failed: HTTP ${response.status}`);
1484
+ }
1485
+ return yield response.json();
1486
+ }
1487
+ finally {
1488
+ if (timeoutId) {
1489
+ clearTimeout(timeoutId);
1490
+ }
1491
+ }
1492
+ });
1493
+ }
1494
+ resolveHeaders() {
1495
+ return __awaiter(this, void 0, void 0, function* () {
1496
+ if (!this.headers) {
1497
+ return {};
1498
+ }
1499
+ return typeof this.headers === 'function' ? this.headers() : this.headers;
1500
+ });
1501
+ }
1502
+ }
1503
+ const createRestAchievementClient = (config) => new RestAchievementClient(config);
1504
+
1224
1505
  /**
1225
1506
  * Built-in modal component
1226
1507
  * Modern, theme-aware achievement modal with smooth animations
@@ -1576,6 +1857,8 @@ exports.BuiltInNotification = BuiltInNotification;
1576
1857
  exports.ConfettiWrapper = ConfettiWrapper;
1577
1858
  exports.HeadlessAchievementProvider = AchievementProvider$1;
1578
1859
  exports.LevelProgress = LevelProgress;
1860
+ exports.RestAchievementClient = RestAchievementClient;
1861
+ exports.createRestAchievementClient = createRestAchievementClient;
1579
1862
  exports.defaultAchievementIcons = defaultAchievementIcons;
1580
1863
  exports.defaultStyles = defaultStyles;
1581
1864
  exports.isAsyncStorage = isAsyncStorage;