react-achievements 3.9.3 → 4.1.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.
Files changed (52) hide show
  1. package/README.md +172 -100
  2. package/dist/headless.cjs +317 -0
  3. package/dist/headless.cjs.map +1 -0
  4. package/dist/headless.d.ts +176 -0
  5. package/dist/headless.esm.js +222 -0
  6. package/dist/headless.esm.js.map +1 -0
  7. package/dist/index.cjs +839 -881
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +163 -153
  10. package/dist/index.esm.js +835 -883
  11. package/dist/index.esm.js.map +1 -1
  12. package/dist/web.cjs +1416 -0
  13. package/dist/web.cjs.map +1 -0
  14. package/dist/web.d.ts +534 -0
  15. package/dist/web.esm.js +1306 -0
  16. package/dist/web.esm.js.map +1 -0
  17. package/package.json +13 -28
  18. package/dist/types/__mocks__/confetti-wrapper.d.ts +0 -5
  19. package/dist/types/__mocks__/react-confetti.d.ts +0 -3
  20. package/dist/types/__mocks__/react-toastify.d.ts +0 -13
  21. package/dist/types/core/components/BadgesButton.d.ts +0 -25
  22. package/dist/types/core/components/BadgesButtonWithModal.d.ts +0 -53
  23. package/dist/types/core/components/BadgesModal.d.ts +0 -14
  24. package/dist/types/core/components/ConfettiWrapper.d.ts +0 -6
  25. package/dist/types/core/errors/AchievementErrors.d.ts +0 -55
  26. package/dist/types/core/hooks/useWindowSize.d.ts +0 -16
  27. package/dist/types/core/icons/defaultIcons.d.ts +0 -8
  28. package/dist/types/core/storage/AsyncStorageAdapter.d.ts +0 -48
  29. package/dist/types/core/storage/IndexedDBStorage.d.ts +0 -29
  30. package/dist/types/core/storage/LocalStorage.d.ts +0 -16
  31. package/dist/types/core/storage/MemoryStorage.d.ts +0 -11
  32. package/dist/types/core/storage/OfflineQueueStorage.d.ts +0 -42
  33. package/dist/types/core/storage/RestApiStorage.d.ts +0 -20
  34. package/dist/types/core/styles/defaultStyles.d.ts +0 -2
  35. package/dist/types/core/types.d.ts +0 -115
  36. package/dist/types/core/ui/BuiltInConfetti.d.ts +0 -7
  37. package/dist/types/core/ui/BuiltInModal.d.ts +0 -7
  38. package/dist/types/core/ui/BuiltInNotification.d.ts +0 -7
  39. package/dist/types/core/ui/LegacyWrappers.d.ts +0 -21
  40. package/dist/types/core/ui/interfaces.d.ts +0 -127
  41. package/dist/types/core/ui/legacyDetector.d.ts +0 -40
  42. package/dist/types/core/ui/themes.d.ts +0 -14
  43. package/dist/types/core/utils/configNormalizer.d.ts +0 -3
  44. package/dist/types/core/utils/dataExport.d.ts +0 -34
  45. package/dist/types/core/utils/dataImport.d.ts +0 -50
  46. package/dist/types/hooks/useAchievementEngine.d.ts +0 -36
  47. package/dist/types/hooks/useAchievements.d.ts +0 -1
  48. package/dist/types/hooks/useSimpleAchievements.d.ts +0 -63
  49. package/dist/types/index.d.ts +0 -36
  50. package/dist/types/providers/AchievementProvider.d.ts +0 -47
  51. package/dist/types/setupTests.d.ts +0 -1
  52. package/dist/types/utils/achievementHelpers.d.ts +0 -135
package/README.md CHANGED
@@ -1,183 +1,255 @@
1
1
  # React Achievements
2
2
 
3
- **Add gamification to your React app in 5 minutes** - Unlock achievements, celebrate milestones, delight users.
3
+ **Add gamification to your React app in minutes** - Track progress, unlock achievements, show badges, and celebrate milestones.
4
4
 
5
- [![Demo video](https://raw.githubusercontent.com/dave-b-b/react-achievements/main/assets/achievements.png)](https://github.com/user-attachments/assets/a33fdae5-439b-4fc9-a388-ccb2f432a3a8)
6
-
7
- [📚 Documentation](https://dave-b-b.github.io/react-achievements/) | [🎮 Interactive Demo](https://dave-b-b.github.io/react-achievements/?path=/story/introduction--page) | [📦 npm Package](https://www.npmjs.com/package/react-achievements)
5
+ [📚 Documentation](https://dave-b-b.github.io/react-achievements/) | [📦 npm Package](https://www.npmjs.com/package/react-achievements)
8
6
 
9
7
  [![npm version](https://img.shields.io/npm/v/react-achievements.svg)](https://www.npmjs.com/package/react-achievements) [![License](https://img.shields.io/badge/license-Dual%20(MIT%20%2B%20Commercial)-blue.svg)](./LICENSE) [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
10
8
 
9
+ <p align="center">
10
+ <a href="https://github.com/dave-b-b/react-achievements/raw/main/assets/Demo.mov">
11
+ <img
12
+ src="https://raw.githubusercontent.com/dave-b-b/react-achievements/main/assets/compact.png"
13
+ alt="React Achievements demo showing a LearnQuest achievements modal"
14
+ width="900"
15
+ />
16
+ </a>
17
+ </p>
18
+
11
19
  ## Installation
12
20
 
13
21
  ```bash
14
22
  npm install react-achievements
15
23
  ```
16
24
 
17
- **Requirements:** React 17.0+, Node.js 16+
25
+ **Requirements:** React 16.8+, Node.js 16+
18
26
 
19
- ---
20
-
21
- ## Start Here (60 Seconds)
27
+ ## Start Here
22
28
 
23
29
  ```tsx
24
- import { AchievementProvider, useSimpleAchievements, BadgesButtonWithModal } from 'react-achievements';
30
+ import {
31
+ AchievementProvider,
32
+ AchievementsWidget,
33
+ useSimpleAchievements,
34
+ } from 'react-achievements';
25
35
 
26
36
  const achievements = {
27
37
  score: {
28
38
  100: { title: 'Century!', description: 'Score 100 points', icon: '🏆' },
29
- }
39
+ },
30
40
  };
31
41
 
32
- function AchievementsUI() {
33
- const { track, unlocked } = useSimpleAchievements();
42
+ function Game() {
43
+ const { track } = useSimpleAchievements();
34
44
 
35
45
  return (
36
- <div>
37
- <button onClick={() => track('score', 100)}>Score 100</button>
38
- <BadgesButtonWithModal unlockedAchievements={unlocked} />
39
- </div>
46
+ <button onClick={() => track('score', 100)}>
47
+ Score 100
48
+ </button>
40
49
  );
41
50
  }
42
51
 
43
52
  export default function App() {
44
53
  return (
45
- <AchievementProvider achievements={achievements} useBuiltInUI={true}>
46
- <AchievementsUI />
54
+ <AchievementProvider achievements={achievements}>
55
+ <Game />
56
+ <AchievementsWidget />
47
57
  </AchievementProvider>
48
58
  );
49
59
  }
50
60
  ```
51
61
 
52
- ---
53
-
54
- ## Usage
62
+ `AchievementsWidget` reads from context, shows the unlocked count, and opens a modal with locked and unlocked achievements. Use `placement="inline"` to put it in a drawer, sidebar, or navigation area. For an exact visual match, pass `renderTrigger` and use your app's own drawer row, nav item, or profile menu button while the widget still controls the modal.
55
63
 
56
- React Achievements supports two tracking patterns:
64
+ ```tsx
65
+ <AchievementsWidget
66
+ placement="inline"
67
+ renderTrigger={({ buttonProps, unlockedCount, totalCount }) => (
68
+ <button {...buttonProps} className="drawer-row">
69
+ Achievements
70
+ <span>{unlockedCount}/{totalCount}</span>
71
+ </button>
72
+ )}
73
+ />
74
+ ```
57
75
 
58
- ### Pattern 1: Event-Based Tracking
76
+ ## Common Placements
59
77
 
60
- Track achievements using semantic events. Perfect for complex applications or multi-framework projects.
78
+ Use the same context-aware UI in whichever surface already fits your app:
61
79
 
62
80
  ```tsx
63
- // achievementEngine.ts
64
- import { AchievementEngine } from 'react-achievements';
81
+ import { useState } from 'react';
82
+ import {
83
+ AchievementsList,
84
+ AchievementsModal,
85
+ AchievementsWidget,
86
+ } from 'react-achievements';
65
87
 
66
- const achievements = {
67
- score: {
68
- 100: { title: 'Century!', description: 'Score 100 points', icon: '🏆' },
69
- }
70
- };
88
+ // Floating launcher in a corner
89
+ <AchievementsWidget position="bottom-right" />
71
90
 
72
- const eventMapping = {
73
- 'userScored': (data) => ({ score: data.points }),
74
- };
91
+ // Inline nav, drawer, sidebar, or profile menu item
92
+ <AchievementsWidget placement="inline" label="Badges" />
75
93
 
76
- export const engine = new AchievementEngine({
77
- achievements,
78
- eventMapping,
79
- storage: 'local'
80
- });
81
- ```
94
+ // Compact square badge grid for dense achievement catalogs
95
+ <AchievementsWidget density="compact" />
82
96
 
83
- ```tsx
84
- // App.tsx
85
- import { AchievementProvider } from 'react-achievements';
86
- import { engine } from './achievementEngine';
97
+ // Optional: blur the page behind the modal
98
+ <AchievementsWidget modalBackdropBlur={2} />
87
99
 
88
- function App() {
89
- return (
90
- <AchievementProvider engine={engine} useBuiltInUI={true}>
91
- <Game />
92
- </AchievementProvider>
93
- );
94
- }
95
- ```
100
+ // Existing button or drawer row that opens the built-in modal
101
+ const [open, setOpen] = useState(false);
96
102
 
97
- ```tsx
98
- // Game.tsx
99
- import { useAchievementEngine } from 'react-achievements';
103
+ <button onClick={() => setOpen(true)}>View achievements</button>
100
104
 
101
- function Game() {
102
- const engine = useAchievementEngine();
103
-
104
- return (
105
- <button onClick={() => engine.emit('userScored', { points: 100 })}>
106
- Score Points
107
- </button>
108
- );
109
- }
105
+ // Optional: hide scrollbar chrome while preserving scroll
106
+ <AchievementsModal
107
+ isOpen={open}
108
+ onClose={() => setOpen(false)}
109
+ hideScrollbar
110
+ backdropBlur={2}
111
+ />
112
+
113
+ // Inline achievements page, panel, drawer, or settings section
114
+ <AchievementsList showLocked />
110
115
  ```
111
116
 
112
- ➡️ **[Event-Based Tracking Guide](https://dave-b-b.github.io/react-achievements/docs/guides/event-based-tracking)**
117
+ For modal blur props, pass a number for pixels or a CSS length string. Omit the prop or pass `0` when you do not want backdrop blur.
118
+
119
+ Storybook includes examples for floating buttons, nav buttons, drawer rows, existing controls that open a modal, dashboard cards, profile menus, and inline lists.
113
120
 
114
- ---
121
+ Provider-level icons and UI options are shared across notifications, widgets, modals, and lists:
115
122
 
116
- ### Pattern 2: Direct Track Updates
123
+ ```tsx
124
+ <AchievementProvider
125
+ achievements={achievements}
126
+ icons={{ login: '🔑', streak: '🔥' }}
127
+ ui={{
128
+ theme: 'minimal',
129
+ NotificationComponent: MyNotification,
130
+ ModalComponent: MyAchievementsModal,
131
+ ConfettiComponent: MyConfetti,
132
+ }}
133
+ >
134
+ <App />
135
+ </AchievementProvider>
136
+ ```
117
137
 
118
- Update metrics directly in your React components. Perfect for simple applications or quick prototypes.
138
+ ## Hooks
119
139
 
120
140
  ```tsx
121
- // achievements.ts
122
- const achievements = {
123
- score: {
124
- 100: { title: 'Century!', description: 'Score 100 points', icon: '🏆' },
125
- }
126
- };
141
+ const {
142
+ track,
143
+ increment,
144
+ trackMultiple,
145
+ unlockedIds,
146
+ unlockedAchievements,
147
+ allAchievements,
148
+ unlockedCount,
149
+ totalCount,
150
+ } = useSimpleAchievements();
127
151
  ```
128
152
 
153
+ Deprecated aliases from v3, including `unlocked`, remain available until `4.2`.
154
+
155
+ ## Event-Based Tracking
156
+
157
+ For larger apps, create an engine and emit semantic events:
158
+
129
159
  ```tsx
130
- // App.tsx
131
- import { AchievementProvider } from 'react-achievements';
160
+ import {
161
+ AchievementEngine,
162
+ AchievementProvider,
163
+ AchievementsWidget,
164
+ useAchievementEngine,
165
+ } from 'react-achievements';
166
+
167
+ const engine = new AchievementEngine({
168
+ achievements,
169
+ eventMapping: {
170
+ userScored: (data) => ({ score: data.points }),
171
+ },
172
+ storage: 'local',
173
+ });
132
174
 
133
- function App() {
175
+ function Game() {
176
+ const engine = useAchievementEngine();
177
+ return <button onClick={() => engine.emit('userScored', { points: 100 })}>Score</button>;
178
+ }
179
+
180
+ export default function App() {
134
181
  return (
135
- <AchievementProvider achievements={achievements} useBuiltInUI={true}>
182
+ <AchievementProvider engine={engine}>
136
183
  <Game />
184
+ <AchievementsWidget />
137
185
  </AchievementProvider>
138
186
  );
139
187
  }
140
188
  ```
141
189
 
190
+ ## Headless Usage
191
+
192
+ Use the DOM-free entry point when building custom UI or preparing a React Native integration:
193
+
142
194
  ```tsx
143
- // Game.tsx
144
- import { useSimpleAchievements, BadgesButtonWithModal } from 'react-achievements';
195
+ import {
196
+ AchievementProvider,
197
+ useAchievementState,
198
+ useSimpleAchievements,
199
+ } from 'react-achievements/headless';
145
200
 
146
- function Game() {
147
- const { track, unlocked } = useSimpleAchievements();
201
+ function CustomAchievementsPanel() {
202
+ const { track } = useSimpleAchievements();
203
+ const { allAchievements, unlockedCount, totalCount } = useAchievementState();
148
204
 
149
205
  return (
150
- <div>
151
- <button onClick={() => track('score', 100)}>Score Points</button>
152
- <BadgesButtonWithModal unlockedAchievements={unlocked} />
153
- </div>
206
+ <section>
207
+ <button onClick={() => track('score', 100)}>Score 100</button>
208
+ <p>{unlockedCount} / {totalCount} unlocked</p>
209
+
210
+ {allAchievements.map((achievement) => (
211
+ <div key={achievement.achievementId}>
212
+ {achievement.isUnlocked ? 'Unlocked' : 'Locked'}: {achievement.achievementTitle}
213
+ </div>
214
+ ))}
215
+ </section>
216
+ );
217
+ }
218
+
219
+ export function App() {
220
+ return (
221
+ <AchievementProvider achievements={achievements}>
222
+ <CustomAchievementsPanel />
223
+ </AchievementProvider>
154
224
  );
155
225
  }
156
226
  ```
157
227
 
158
- ➡️ **[Direct Updates Guide](https://dave-b-b.github.io/react-achievements/docs/guides/direct-updates)**
228
+ React Native UI components are not included in the web package; use `achievements-engine` or the `/headless` React APIs with your own native UI.
159
229
 
160
- ---
230
+ ## Entry Points
161
231
 
162
- ## Documentation
232
+ - `react-achievements` - v4 web API with provider, hooks, built-in effects, widget, modal, and list components
233
+ - `react-achievements/web` - explicit web entry point
234
+ - `react-achievements/headless` - provider, hooks, engine, storage, and types without DOM UI
163
235
 
164
- 📚 **[Full Documentation](https://dave-b-b.github.io/react-achievements/)** - Complete guides, API reference, and examples
236
+ ## Migrating From v3
165
237
 
166
- ---
238
+ - Built-in UI is now the default; `useBuiltInUI` is a deprecated no-op.
239
+ - `AchievementsWidget` replaces the legacy manual `BadgesButtonWithModal` setup.
240
+ - `useSimpleAchievements()` now returns `unlockedIds`, `unlockedAchievements`, and `allAchievements`.
241
+ - External UI peer dependencies are no longer required.
242
+ - Deprecated v3 component names remain as compatibility wrappers until `4.2`.
167
243
 
168
244
  ## License
169
245
 
170
- React Achievements is **dual-licensed**:
171
-
172
- - **Free for Non-Commercial Use** (MIT License) - Personal projects, education, non-profits, open source
173
- - **Commercial License Required** - Businesses, SaaS, commercial apps, enterprise
174
-
175
- **[Get Commercial License →](https://github.com/sponsors/dave-b-b)** | **[License Details](./LICENSE)** | **[Commercial Terms](./COMMERCIAL-LICENSE.md)**
246
+ React Achievements is dual-licensed:
176
247
 
177
- ---
248
+ - **Free for Non-Commercial Use** (MIT License)
249
+ - **Commercial License Required** for businesses, SaaS, commercial apps, and enterprise use
178
250
 
179
- **Built with ❤️ by [Dave B](https://github.com/dave-b-b)** | [📚 Documentation](https://dave-b-b.github.io/react-achievements/) | [ Star on GitHub](https://github.com/dave-b-b/react-achievements)
251
+ [Get Commercial License](https://github.com/sponsors/dave-b-b) | [License Details](./LICENSE) | [Commercial Terms](./COMMERCIAL-LICENSE.md)
180
252
 
181
253
  ## AI Agents
182
254
 
183
- If you're using AI coding agents, see `AGENTS.md` for a concise integration prompt, pitfalls, and the recommended API.
255
+ If you're using AI coding agents, see `AGENTS.md` for the recommended v4 integration prompt.
@@ -0,0 +1,317 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+ var achievementsEngine = require('achievements-engine');
5
+
6
+ // Type guard to detect async storage
7
+ function isAsyncStorage(storage) {
8
+ // Check if methods return Promises
9
+ const testResult = storage.getMetrics();
10
+ return testResult && typeof testResult.then === 'function';
11
+ }
12
+ var StorageType;
13
+ (function (StorageType) {
14
+ StorageType["Local"] = "local";
15
+ StorageType["Memory"] = "memory";
16
+ StorageType["IndexedDB"] = "indexeddb";
17
+ StorageType["RestAPI"] = "restapi"; // Asynchronous REST API storage
18
+ })(StorageType || (StorageType = {}));
19
+
20
+ const warnedMessages = new Set();
21
+ function warnDeprecation(message) {
22
+ var _a, _b;
23
+ const isProduction = typeof globalThis !== 'undefined' &&
24
+ ((_b = (_a = globalThis.process) === null || _a === void 0 ? void 0 : _a.env) === null || _b === void 0 ? void 0 : _b.NODE_ENV) === 'production';
25
+ if (isProduction || warnedMessages.has(message)) {
26
+ return;
27
+ }
28
+ warnedMessages.add(message);
29
+ console.warn(`[react-achievements] ${message}`);
30
+ }
31
+
32
+ const AchievementContext = React.createContext(undefined);
33
+ const getAllAchievementRecord = (engine) => {
34
+ return Object.fromEntries(engine.getAllAchievements().map((achievement) => [
35
+ achievement.achievementId,
36
+ achievement,
37
+ ]));
38
+ };
39
+ const AchievementProvider = ({ achievements: achievementsConfig, storage = 'local', children, onError, useBuiltInUI, restApiConfig, engine: externalEngine, eventMapping, icons = {}, }) => {
40
+ if (useBuiltInUI !== undefined) {
41
+ warnDeprecation('`useBuiltInUI` is deprecated and is now a no-op because built-in UI is the default. It will be removed in 4.2.');
42
+ }
43
+ if (achievementsConfig && externalEngine) {
44
+ throw new Error('Cannot provide both "achievements" and "engine" props to AchievementProvider.\n\n' +
45
+ 'Choose one pattern:\n' +
46
+ '1. Direct metric tracking: <AchievementProvider achievements={config}>\n' +
47
+ '2. Event-based tracking: <AchievementProvider engine={myEngine}>');
48
+ }
49
+ const isProviderCreatedEngine = Boolean(achievementsConfig);
50
+ const [engine] = React.useState(() => {
51
+ if (externalEngine) {
52
+ return externalEngine;
53
+ }
54
+ if (!achievementsConfig) {
55
+ throw new Error('AchievementProvider requires either "achievements" or "engine" prop.\n\n' +
56
+ '1. Direct metric tracking: <AchievementProvider achievements={config}>\n' +
57
+ '2. Event-based tracking: <AchievementProvider engine={myEngine}>');
58
+ }
59
+ return new achievementsEngine.AchievementEngine({
60
+ achievements: achievementsConfig,
61
+ storage: storage,
62
+ restApiConfig,
63
+ onError: onError,
64
+ eventMapping,
65
+ });
66
+ });
67
+ const [achievementState, setAchievementState] = React.useState(() => ({
68
+ unlocked: [...engine.getUnlocked()],
69
+ all: getAllAchievementRecord(engine),
70
+ }));
71
+ const syncAchievementState = React.useCallback(() => {
72
+ setAchievementState({
73
+ unlocked: [...engine.getUnlocked()],
74
+ all: getAllAchievementRecord(engine),
75
+ });
76
+ }, [engine]);
77
+ React.useEffect(() => {
78
+ return () => {
79
+ if (!externalEngine) {
80
+ engine.destroy();
81
+ }
82
+ };
83
+ }, [engine, externalEngine]);
84
+ React.useEffect(() => {
85
+ const unsubscribeUnlocked = engine.on('achievement:unlocked', syncAchievementState);
86
+ const unsubscribeStateChanged = engine.on('state:changed', syncAchievementState);
87
+ return () => {
88
+ unsubscribeUnlocked();
89
+ unsubscribeStateChanged();
90
+ };
91
+ }, [engine, syncAchievementState]);
92
+ const update = (newMetrics) => {
93
+ engine.update(newMetrics);
94
+ };
95
+ const reset = () => {
96
+ engine.reset();
97
+ syncAchievementState();
98
+ };
99
+ const getState = () => {
100
+ const metrics = engine.getMetrics();
101
+ const unlocked = engine.getUnlocked();
102
+ const metricsInArrayFormat = {};
103
+ Object.entries(metrics).forEach(([key, value]) => {
104
+ metricsInArrayFormat[key] = Array.isArray(value) ? value : [value];
105
+ });
106
+ return {
107
+ metrics: metricsInArrayFormat,
108
+ unlocked: [...unlocked],
109
+ };
110
+ };
111
+ const exportData = () => {
112
+ return engine.export();
113
+ };
114
+ const importData = (jsonString, options) => {
115
+ const result = engine.import(jsonString, options);
116
+ syncAchievementState();
117
+ return result;
118
+ };
119
+ const getAllAchievements = () => {
120
+ return engine.getAllAchievements();
121
+ };
122
+ return (React.createElement(AchievementContext.Provider, { value: {
123
+ update,
124
+ achievements: achievementState,
125
+ reset,
126
+ getState,
127
+ exportData,
128
+ importData,
129
+ getAllAchievements,
130
+ engine,
131
+ icons,
132
+ _isLegacyPattern: isProviderCreatedEngine,
133
+ } }, children));
134
+ };
135
+
136
+ const useAchievements = () => {
137
+ const context = React.useContext(AchievementContext);
138
+ if (!context) {
139
+ throw new Error('useAchievements must be used within an AchievementProvider');
140
+ }
141
+ return context;
142
+ };
143
+
144
+ const useAchievementState = () => {
145
+ const { achievements, getAllAchievements, getState } = useAchievements();
146
+ const allAchievements = getAllAchievements();
147
+ const unlockedIds = achievements.unlocked;
148
+ const unlockedAchievementSet = new Set(unlockedIds);
149
+ const unlockedAchievements = allAchievements.filter((achievement) => unlockedAchievementSet.has(achievement.achievementId));
150
+ return {
151
+ unlockedIds,
152
+ unlockedAchievements,
153
+ allAchievements,
154
+ unlockedCount: unlockedIds.length,
155
+ totalCount: allAchievements.length,
156
+ metrics: getState().metrics,
157
+ };
158
+ };
159
+
160
+ /**
161
+ * A simplified hook for achievement tracking.
162
+ * Provides the v4 happy path for direct metric updates plus explicit state names.
163
+ */
164
+ const useSimpleAchievements = () => {
165
+ const { update, reset, getState, exportData, importData } = useAchievements();
166
+ const achievementState = useAchievementState();
167
+ const track = (metric, value) => update({ [metric]: value });
168
+ const increment = (metric, amount = 1) => {
169
+ const currentState = getState();
170
+ const currentMetricArray = currentState.metrics[metric] || [0];
171
+ const currentValue = Array.isArray(currentMetricArray)
172
+ ? currentMetricArray[0]
173
+ : currentMetricArray;
174
+ const newValue = (typeof currentValue === 'number' ? currentValue : 0) + amount;
175
+ update({ [metric]: newValue });
176
+ };
177
+ const trackMultiple = (metrics) => update(metrics);
178
+ return {
179
+ track,
180
+ increment,
181
+ trackMultiple,
182
+ unlockedIds: achievementState.unlockedIds,
183
+ unlockedAchievements: achievementState.unlockedAchievements,
184
+ allAchievements: achievementState.allAchievements,
185
+ unlockedCount: achievementState.unlockedCount,
186
+ totalCount: achievementState.totalCount,
187
+ metrics: achievementState.metrics,
188
+ reset,
189
+ getState,
190
+ exportData,
191
+ importData,
192
+ getAllAchievements: () => achievementState.allAchievements,
193
+ /**
194
+ * @deprecated Use `unlockedIds` instead. This alias will be removed in 4.2.
195
+ */
196
+ unlocked: achievementState.unlockedIds,
197
+ /**
198
+ * @deprecated Use `allAchievements` instead. This alias will be removed in 4.2.
199
+ */
200
+ all: achievementState.allAchievements,
201
+ };
202
+ };
203
+
204
+ /**
205
+ * Access the active AchievementEngine instance.
206
+ *
207
+ * In v4 this works with both provider-created engines (`achievements` prop) and
208
+ * injected engines (`engine` prop).
209
+ */
210
+ const useAchievementEngine = () => {
211
+ const context = React.useContext(AchievementContext);
212
+ if (!context) {
213
+ throw new Error('useAchievementEngine must be used within an AchievementProvider.\n\n' +
214
+ 'Wrap your component tree:\n' +
215
+ '<AchievementProvider achievements={achievements}>\n' +
216
+ ' <YourComponent />\n' +
217
+ '</AchievementProvider>');
218
+ }
219
+ return context.engine;
220
+ };
221
+
222
+ Object.defineProperty(exports, 'AchievementBuilder', {
223
+ enumerable: true,
224
+ get: function () { return achievementsEngine.AchievementBuilder; }
225
+ });
226
+ Object.defineProperty(exports, 'AchievementEngine', {
227
+ enumerable: true,
228
+ get: function () { return achievementsEngine.AchievementEngine; }
229
+ });
230
+ Object.defineProperty(exports, 'AchievementError', {
231
+ enumerable: true,
232
+ get: function () { return achievementsEngine.AchievementError; }
233
+ });
234
+ Object.defineProperty(exports, 'AsyncStorageAdapter', {
235
+ enumerable: true,
236
+ get: function () { return achievementsEngine.AsyncStorageAdapter; }
237
+ });
238
+ Object.defineProperty(exports, 'ConfigurationError', {
239
+ enumerable: true,
240
+ get: function () { return achievementsEngine.ConfigurationError; }
241
+ });
242
+ Object.defineProperty(exports, 'ImportValidationError', {
243
+ enumerable: true,
244
+ get: function () { return achievementsEngine.ImportValidationError; }
245
+ });
246
+ Object.defineProperty(exports, 'IndexedDBStorage', {
247
+ enumerable: true,
248
+ get: function () { return achievementsEngine.IndexedDBStorage; }
249
+ });
250
+ Object.defineProperty(exports, 'LocalStorage', {
251
+ enumerable: true,
252
+ get: function () { return achievementsEngine.LocalStorage; }
253
+ });
254
+ Object.defineProperty(exports, 'MemoryStorage', {
255
+ enumerable: true,
256
+ get: function () { return achievementsEngine.MemoryStorage; }
257
+ });
258
+ Object.defineProperty(exports, 'OfflineQueueStorage', {
259
+ enumerable: true,
260
+ get: function () { return achievementsEngine.OfflineQueueStorage; }
261
+ });
262
+ Object.defineProperty(exports, 'RestApiStorage', {
263
+ enumerable: true,
264
+ get: function () { return achievementsEngine.RestApiStorage; }
265
+ });
266
+ Object.defineProperty(exports, 'StorageError', {
267
+ enumerable: true,
268
+ get: function () { return achievementsEngine.StorageError; }
269
+ });
270
+ Object.defineProperty(exports, 'StorageQuotaError', {
271
+ enumerable: true,
272
+ get: function () { return achievementsEngine.StorageQuotaError; }
273
+ });
274
+ Object.defineProperty(exports, 'StorageType', {
275
+ enumerable: true,
276
+ get: function () { return achievementsEngine.StorageType; }
277
+ });
278
+ Object.defineProperty(exports, 'SyncError', {
279
+ enumerable: true,
280
+ get: function () { return achievementsEngine.SyncError; }
281
+ });
282
+ Object.defineProperty(exports, 'createConfigHash', {
283
+ enumerable: true,
284
+ get: function () { return achievementsEngine.createConfigHash; }
285
+ });
286
+ Object.defineProperty(exports, 'exportAchievementData', {
287
+ enumerable: true,
288
+ get: function () { return achievementsEngine.exportAchievementData; }
289
+ });
290
+ Object.defineProperty(exports, 'importAchievementData', {
291
+ enumerable: true,
292
+ get: function () { return achievementsEngine.importAchievementData; }
293
+ });
294
+ Object.defineProperty(exports, 'isAchievementError', {
295
+ enumerable: true,
296
+ get: function () { return achievementsEngine.isAchievementError; }
297
+ });
298
+ Object.defineProperty(exports, 'isRecoverableError', {
299
+ enumerable: true,
300
+ get: function () { return achievementsEngine.isRecoverableError; }
301
+ });
302
+ Object.defineProperty(exports, 'isSimpleConfig', {
303
+ enumerable: true,
304
+ get: function () { return achievementsEngine.isSimpleConfig; }
305
+ });
306
+ Object.defineProperty(exports, 'normalizeAchievements', {
307
+ enumerable: true,
308
+ get: function () { return achievementsEngine.normalizeAchievements; }
309
+ });
310
+ exports.AchievementContext = AchievementContext;
311
+ exports.AchievementProvider = AchievementProvider;
312
+ exports.isAsyncStorage = isAsyncStorage;
313
+ exports.useAchievementEngine = useAchievementEngine;
314
+ exports.useAchievementState = useAchievementState;
315
+ exports.useAchievements = useAchievements;
316
+ exports.useSimpleAchievements = useSimpleAchievements;
317
+ //# sourceMappingURL=headless.cjs.map