musora-content-services 2.94.8 → 2.95.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.
Files changed (69) hide show
  1. package/.claude/settings.local.json +18 -0
  2. package/CHANGELOG.md +18 -0
  3. package/CLAUDE.md +408 -0
  4. package/babel.config.cjs +10 -0
  5. package/jsdoc.json +2 -1
  6. package/package.json +2 -2
  7. package/src/constants/award-assets.js +35 -0
  8. package/src/filterBuilder.js +7 -2
  9. package/src/index.d.ts +26 -5
  10. package/src/index.js +26 -5
  11. package/src/services/awards/award-callbacks.js +165 -0
  12. package/src/services/awards/award-query.js +495 -0
  13. package/src/services/awards/internal/.indexignore +1 -0
  14. package/src/services/awards/internal/award-definitions.js +239 -0
  15. package/src/services/awards/internal/award-events.js +102 -0
  16. package/src/services/awards/internal/award-manager.js +162 -0
  17. package/src/services/awards/internal/certificate-builder.js +66 -0
  18. package/src/services/awards/internal/completion-data-generator.js +84 -0
  19. package/src/services/awards/internal/content-progress-observer.js +137 -0
  20. package/src/services/awards/internal/image-utils.js +62 -0
  21. package/src/services/awards/internal/message-generator.js +17 -0
  22. package/src/services/awards/internal/types.js +5 -0
  23. package/src/services/awards/types.d.ts +79 -0
  24. package/src/services/awards/types.js +101 -0
  25. package/src/services/config.js +24 -4
  26. package/src/services/content-org/learning-paths.ts +19 -15
  27. package/src/services/gamification/awards.ts +114 -83
  28. package/src/services/progress-events.js +58 -0
  29. package/src/services/progress-row/method-card.js +20 -5
  30. package/src/services/sanity.js +1 -1
  31. package/src/services/sync/fetch.ts +10 -2
  32. package/src/services/sync/manager.ts +6 -0
  33. package/src/services/sync/models/ContentProgress.ts +5 -6
  34. package/src/services/sync/models/UserAwardProgress.ts +55 -0
  35. package/src/services/sync/models/index.ts +1 -0
  36. package/src/services/sync/repositories/content-progress.ts +47 -25
  37. package/src/services/sync/repositories/index.ts +1 -0
  38. package/src/services/sync/repositories/practices.ts +16 -1
  39. package/src/services/sync/repositories/user-award-progress.ts +133 -0
  40. package/src/services/sync/repository-proxy.ts +6 -0
  41. package/src/services/sync/retry.ts +12 -11
  42. package/src/services/sync/schema/index.ts +18 -3
  43. package/src/services/sync/store/index.ts +53 -8
  44. package/src/services/sync/store/push-coalescer.ts +3 -3
  45. package/src/services/sync/store-configs.ts +7 -1
  46. package/src/services/userActivity.js +0 -1
  47. package/test/HttpClient.test.js +6 -6
  48. package/test/awards/award-alacarte-observer.test.js +196 -0
  49. package/test/awards/award-auto-refresh.test.js +83 -0
  50. package/test/awards/award-calculations.test.js +33 -0
  51. package/test/awards/award-certificate-display.test.js +328 -0
  52. package/test/awards/award-collection-edge-cases.test.js +210 -0
  53. package/test/awards/award-collection-filtering.test.js +285 -0
  54. package/test/awards/award-completion-flow.test.js +213 -0
  55. package/test/awards/award-exclusion-handling.test.js +273 -0
  56. package/test/awards/award-multi-lesson.test.js +241 -0
  57. package/test/awards/award-observer-integration.test.js +325 -0
  58. package/test/awards/award-query-messages.test.js +438 -0
  59. package/test/awards/award-user-collection.test.js +412 -0
  60. package/test/awards/duplicate-prevention.test.js +118 -0
  61. package/test/awards/helpers/completion-mock.js +54 -0
  62. package/test/awards/helpers/index.js +3 -0
  63. package/test/awards/helpers/mock-setup.js +69 -0
  64. package/test/awards/helpers/progress-emitter.js +39 -0
  65. package/test/awards/message-generator.test.js +162 -0
  66. package/test/initializeTests.js +6 -0
  67. package/test/mockData/award-definitions.js +171 -0
  68. package/test/sync/models/award-database-integration.test.js +519 -0
  69. package/tools/generate-index.cjs +9 -0
package/src/index.js CHANGED
@@ -4,6 +4,18 @@ import {
4
4
  default as EventsAPI
5
5
  } from './services/eventsAPI';
6
6
 
7
+ import {
8
+ registerAwardCallback,
9
+ registerProgressCallback
10
+ } from './services/awards/award-callbacks.js';
11
+
12
+ import {
13
+ getAwardStatistics,
14
+ getCompletedAwards,
15
+ getContentAwards,
16
+ getInProgressAwards
17
+ } from './services/awards/award-query.js';
18
+
7
19
  import {
8
20
  globalConfig,
9
21
  initializeService
@@ -165,9 +177,7 @@ import {
165
177
  } from './services/forums/threads.ts';
166
178
 
167
179
  import {
168
- fetchAwardsForUser,
169
- fetchCertificate,
170
- getAwardDataForGuidedContent
180
+ fetchCertificate
171
181
  } from './services/gamification/awards.ts';
172
182
 
173
183
  import {
@@ -186,6 +196,11 @@ import {
186
196
  createTestUser
187
197
  } from './services/liveTesting.ts';
188
198
 
199
+ import {
200
+ emitProgressSaved,
201
+ onProgressSaved
202
+ } from './services/progress-events.js';
203
+
189
204
  import {
190
205
  getMethodCard
191
206
  } from './services/progress-row/method-card.js';
@@ -452,6 +467,7 @@ export {
452
467
  deleteUserActivity,
453
468
  duplicatePlaylist,
454
469
  editComment,
470
+ emitProgressSaved,
455
471
  enrollUserInGuidedCourse,
456
472
  extractSanityUrl,
457
473
  fetchAll,
@@ -461,7 +477,6 @@ export {
461
477
  fetchArtistBySlug,
462
478
  fetchArtistLessons,
463
479
  fetchArtists,
464
- fetchAwardsForUser,
465
480
  fetchByRailContentId,
466
481
  fetchByRailContentIds,
467
482
  fetchByReference,
@@ -568,10 +583,13 @@ export {
568
583
  getAllCompletedByIds,
569
584
  getAllStarted,
570
585
  getAllStartedOrCompleted,
571
- getAwardDataForGuidedContent,
586
+ getAwardStatistics,
587
+ getCompletedAwards,
588
+ getContentAwards,
572
589
  getContentRows,
573
590
  getDailySession,
574
591
  getEnrichedLearningPath,
592
+ getInProgressAwards,
575
593
  getLastInteractedOf,
576
594
  getLearningPathLessonsByIds,
577
595
  getLegacyMethods,
@@ -632,6 +650,7 @@ export {
632
650
  markNotificationAsUnread,
633
651
  markThreadAsRead,
634
652
  numberOfActiveUsers,
653
+ onProgressSaved,
635
654
  openComment,
636
655
  otherStats,
637
656
  pauseLiveEventPolling,
@@ -644,6 +663,8 @@ export {
644
663
  recordUserActivity,
645
664
  recordUserPractice,
646
665
  recordWatchSession,
666
+ registerAwardCallback,
667
+ registerProgressCallback,
647
668
  removeContentAsInterested,
648
669
  removeContentAsNotInterested,
649
670
  removeUserPractice,
@@ -0,0 +1,165 @@
1
+ /**
2
+ * @module Awards
3
+ */
4
+
5
+ import './types.js'
6
+ import { awardEvents } from './internal/award-events'
7
+
8
+ let awardGrantedCallback = null
9
+ let progressUpdateCallback = null
10
+
11
+ /**
12
+ * @param {AwardCallbackFunction} callback - Function called with award data when an award is earned
13
+ * @returns {UnregisterFunction} Cleanup function to unregister this callback
14
+ *
15
+ * @description
16
+ * Registers a callback to be notified when the user earns a new award. Only one
17
+ * callback can be registered at a time - registering a new one replaces the previous.
18
+ * Always call the returned cleanup function when your component unmounts.
19
+ *
20
+ * The callback receives an award object with:
21
+ * - `awardId` - Unique Sanity award ID
22
+ * - `name` - Display name of the award
23
+ * - `badge` - URL to badge image
24
+ * - `completed_at` - ISO timestamp
25
+ * - `completion_data.message` - Pre-generated congratulations message
26
+ * - `completion_data.practice_minutes` - Total practice time
27
+ * - `completion_data.days_user_practiced` - Days spent practicing
28
+ * - `completion_data.content_title` - Title of completed content
29
+ *
30
+ * @example // React Native - Show award celebration modal
31
+ * function useAwardNotification() {
32
+ * const [award, setAward] = useState(null)
33
+ *
34
+ * useEffect(() => {
35
+ * return registerAwardCallback((awardData) => {
36
+ * setAward({
37
+ * title: awardData.name,
38
+ * badge: awardData.badge,
39
+ * message: awardData.completion_data.message,
40
+ * practiceMinutes: awardData.completion_data.practice_minutes
41
+ * })
42
+ * })
43
+ * }, [])
44
+ *
45
+ * return { award, dismissAward: () => setAward(null) }
46
+ * }
47
+ *
48
+ * @example // Track award in analytics
49
+ * useEffect(() => {
50
+ * return registerAwardCallback((award) => {
51
+ * analytics.track('Award Earned', {
52
+ * awardId: award.awardId,
53
+ * awardName: award.name,
54
+ * practiceMinutes: award.completion_data.practice_minutes,
55
+ * contentTitle: award.completion_data.content_title
56
+ * })
57
+ * })
58
+ * }, [])
59
+ */
60
+ export function registerAwardCallback(callback) {
61
+ if (typeof callback !== 'function') {
62
+ throw new Error('registerAwardCallback requires a function')
63
+ }
64
+
65
+ unregisterAwardCallback()
66
+
67
+ awardGrantedCallback = async (payload) => {
68
+ const { awardId, definition, completionData, popupMessage } = payload
69
+
70
+ const award = {
71
+ awardId: awardId,
72
+ name: definition.name,
73
+ badge: definition.badge,
74
+ completed_at: completionData.completed_at,
75
+ completion_data: {
76
+ completed_at: completionData.completed_at,
77
+ days_user_practiced: completionData.days_user_practiced,
78
+ message: popupMessage,
79
+ practice_minutes: completionData.practice_minutes,
80
+ content_title: completionData.content_title
81
+ }
82
+ }
83
+
84
+ callback(award)
85
+ }
86
+
87
+ awardEvents.on('awardGranted', awardGrantedCallback)
88
+
89
+ return unregisterAwardCallback
90
+ }
91
+
92
+ function unregisterAwardCallback() {
93
+ if (awardGrantedCallback) {
94
+ awardEvents.off('awardGranted', awardGrantedCallback)
95
+ awardGrantedCallback = null
96
+ }
97
+ }
98
+
99
+ /**
100
+ * @param {ProgressCallbackFunction} callback - Function called with progress data when award progress changes
101
+ * @returns {UnregisterFunction} Cleanup function to unregister this callback
102
+ *
103
+ * @description
104
+ * Registers a callback to be notified when award progress changes (but award is not
105
+ * yet complete). Only one callback can be registered at a time. Use this to update
106
+ * progress bars or show "almost there" encouragement.
107
+ *
108
+ * The callback receives:
109
+ * - `awardId` - Unique Sanity award ID
110
+ * - `progressPercentage` - Current completion percentage (0-99)
111
+ *
112
+ * Note: When an award reaches 100%, `registerAwardCallback` fires instead.
113
+ *
114
+ * @example // React Native - Update progress in learning path screen
115
+ * function LearningPathScreen({ learningPathId }) {
116
+ * const [awardProgress, setAwardProgress] = useState({})
117
+ *
118
+ * useEffect(() => {
119
+ * return registerProgressCallback(({ awardId, progressPercentage }) => {
120
+ * setAwardProgress(prev => ({
121
+ * ...prev,
122
+ * [awardId]: progressPercentage
123
+ * }))
124
+ * })
125
+ * }, [])
126
+ *
127
+ * // Use awardProgress to update UI
128
+ * }
129
+ *
130
+ * @example // Show encouragement toast at milestones
131
+ * useEffect(() => {
132
+ * return registerProgressCallback(({ awardId, progressPercentage }) => {
133
+ * if (progressPercentage === 50) {
134
+ * showToast('Halfway to your award!')
135
+ * } else if (progressPercentage >= 90) {
136
+ * showToast('Almost there! Just a few more lessons.')
137
+ * }
138
+ * })
139
+ * }, [])
140
+ */
141
+ export function registerProgressCallback(callback) {
142
+ if (typeof callback !== 'function') {
143
+ throw new Error('registerProgressCallback requires a function')
144
+ }
145
+
146
+ unregisterProgressCallback()
147
+
148
+ progressUpdateCallback = (payload) => {
149
+ callback({
150
+ awardId: payload.awardId,
151
+ progressPercentage: payload.progressPercentage
152
+ })
153
+ }
154
+
155
+ awardEvents.on('awardProgress', progressUpdateCallback)
156
+
157
+ return unregisterProgressCallback
158
+ }
159
+
160
+ function unregisterProgressCallback() {
161
+ if (progressUpdateCallback) {
162
+ awardEvents.off('awardProgress', progressUpdateCallback)
163
+ progressUpdateCallback = null
164
+ }
165
+ }