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.
- package/README.md +172 -100
- package/dist/headless.cjs +317 -0
- package/dist/headless.cjs.map +1 -0
- package/dist/headless.d.ts +176 -0
- package/dist/headless.esm.js +222 -0
- package/dist/headless.esm.js.map +1 -0
- package/dist/index.cjs +839 -881
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +163 -153
- package/dist/index.esm.js +835 -883
- package/dist/index.esm.js.map +1 -1
- package/dist/web.cjs +1416 -0
- package/dist/web.cjs.map +1 -0
- package/dist/web.d.ts +534 -0
- package/dist/web.esm.js +1306 -0
- package/dist/web.esm.js.map +1 -0
- package/package.json +13 -28
- package/dist/types/__mocks__/confetti-wrapper.d.ts +0 -5
- package/dist/types/__mocks__/react-confetti.d.ts +0 -3
- package/dist/types/__mocks__/react-toastify.d.ts +0 -13
- package/dist/types/core/components/BadgesButton.d.ts +0 -25
- package/dist/types/core/components/BadgesButtonWithModal.d.ts +0 -53
- package/dist/types/core/components/BadgesModal.d.ts +0 -14
- package/dist/types/core/components/ConfettiWrapper.d.ts +0 -6
- package/dist/types/core/errors/AchievementErrors.d.ts +0 -55
- package/dist/types/core/hooks/useWindowSize.d.ts +0 -16
- package/dist/types/core/icons/defaultIcons.d.ts +0 -8
- package/dist/types/core/storage/AsyncStorageAdapter.d.ts +0 -48
- package/dist/types/core/storage/IndexedDBStorage.d.ts +0 -29
- package/dist/types/core/storage/LocalStorage.d.ts +0 -16
- package/dist/types/core/storage/MemoryStorage.d.ts +0 -11
- package/dist/types/core/storage/OfflineQueueStorage.d.ts +0 -42
- package/dist/types/core/storage/RestApiStorage.d.ts +0 -20
- package/dist/types/core/styles/defaultStyles.d.ts +0 -2
- package/dist/types/core/types.d.ts +0 -115
- package/dist/types/core/ui/BuiltInConfetti.d.ts +0 -7
- package/dist/types/core/ui/BuiltInModal.d.ts +0 -7
- package/dist/types/core/ui/BuiltInNotification.d.ts +0 -7
- package/dist/types/core/ui/LegacyWrappers.d.ts +0 -21
- package/dist/types/core/ui/interfaces.d.ts +0 -127
- package/dist/types/core/ui/legacyDetector.d.ts +0 -40
- package/dist/types/core/ui/themes.d.ts +0 -14
- package/dist/types/core/utils/configNormalizer.d.ts +0 -3
- package/dist/types/core/utils/dataExport.d.ts +0 -34
- package/dist/types/core/utils/dataImport.d.ts +0 -50
- package/dist/types/hooks/useAchievementEngine.d.ts +0 -36
- package/dist/types/hooks/useAchievements.d.ts +0 -1
- package/dist/types/hooks/useSimpleAchievements.d.ts +0 -63
- package/dist/types/index.d.ts +0 -36
- package/dist/types/providers/AchievementProvider.d.ts +0 -47
- package/dist/types/setupTests.d.ts +0 -1
- 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
|
|
3
|
+
**Add gamification to your React app in minutes** - Track progress, unlock achievements, show badges, and celebrate milestones.
|
|
4
4
|
|
|
5
|
-
[
|
|
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
|
[](https://www.npmjs.com/package/react-achievements) [-blue.svg)](./LICENSE) [](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
|
|
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 {
|
|
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
|
|
33
|
-
const { track
|
|
42
|
+
function Game() {
|
|
43
|
+
const { track } = useSimpleAchievements();
|
|
34
44
|
|
|
35
45
|
return (
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
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}
|
|
46
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
76
|
+
## Common Placements
|
|
59
77
|
|
|
60
|
-
|
|
78
|
+
Use the same context-aware UI in whichever surface already fits your app:
|
|
61
79
|
|
|
62
80
|
```tsx
|
|
63
|
-
|
|
64
|
-
import {
|
|
81
|
+
import { useState } from 'react';
|
|
82
|
+
import {
|
|
83
|
+
AchievementsList,
|
|
84
|
+
AchievementsModal,
|
|
85
|
+
AchievementsWidget,
|
|
86
|
+
} from 'react-achievements';
|
|
65
87
|
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
};
|
|
91
|
+
// Inline nav, drawer, sidebar, or profile menu item
|
|
92
|
+
<AchievementsWidget placement="inline" label="Badges" />
|
|
75
93
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
eventMapping,
|
|
79
|
-
storage: 'local'
|
|
80
|
-
});
|
|
81
|
-
```
|
|
94
|
+
// Compact square badge grid for dense achievement catalogs
|
|
95
|
+
<AchievementsWidget density="compact" />
|
|
82
96
|
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
98
|
-
// Game.tsx
|
|
99
|
-
import { useAchievementEngine } from 'react-achievements';
|
|
103
|
+
<button onClick={() => setOpen(true)}>View achievements</button>
|
|
100
104
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
+
## Hooks
|
|
119
139
|
|
|
120
140
|
```tsx
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
144
|
-
|
|
195
|
+
import {
|
|
196
|
+
AchievementProvider,
|
|
197
|
+
useAchievementState,
|
|
198
|
+
useSimpleAchievements,
|
|
199
|
+
} from 'react-achievements/headless';
|
|
145
200
|
|
|
146
|
-
function
|
|
147
|
-
const { track
|
|
201
|
+
function CustomAchievementsPanel() {
|
|
202
|
+
const { track } = useSimpleAchievements();
|
|
203
|
+
const { allAchievements, unlockedCount, totalCount } = useAchievementState();
|
|
148
204
|
|
|
149
205
|
return (
|
|
150
|
-
<
|
|
151
|
-
<button onClick={() => track('score', 100)}>Score
|
|
152
|
-
<
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|