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