react-achievements 4.4.1 → 5.0.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 +48 -38
- package/dist/headless.cjs +300 -12
- package/dist/headless.cjs.map +1 -1
- package/dist/headless.d.ts +109 -17
- package/dist/headless.esm.js +299 -13
- package/dist/headless.esm.js.map +1 -1
- package/dist/index.cjs +348 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +109 -17
- package/dist/index.esm.js +347 -66
- package/dist/index.esm.js.map +1 -1
- package/dist/web.cjs +348 -65
- package/dist/web.cjs.map +1 -1
- package/dist/web.d.ts +109 -17
- package/dist/web.esm.js +347 -66
- package/dist/web.esm.js.map +1 -1
- package/package.json +3 -3
package/dist/index.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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
|
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
|
|
562
|
-
const
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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;
|