musora-content-services 2.94.7 → 2.95.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 (210) hide show
  1. package/.coderabbit.yaml +0 -0
  2. package/.editorconfig +0 -0
  3. package/.github/pull_request_template.md +0 -0
  4. package/.github/workflows/conventional-commits.yaml +0 -0
  5. package/.github/workflows/docs.js.yml +0 -0
  6. package/.github/workflows/node.js.yml +0 -0
  7. package/.prettierignore +0 -0
  8. package/.prettierrc +0 -0
  9. package/CHANGELOG.md +23 -0
  10. package/CLAUDE.md +408 -0
  11. package/README.md +0 -0
  12. package/babel.config.cjs +10 -0
  13. package/jest.config.js +0 -0
  14. package/jsdoc.json +2 -1
  15. package/package.json +2 -2
  16. package/src/constants/award-assets.js +35 -0
  17. package/src/contentMetaData.js +0 -0
  18. package/src/filterBuilder.js +7 -2
  19. package/src/index.d.ts +26 -5
  20. package/src/index.js +26 -5
  21. package/src/infrastructure/http/HttpClient.ts +0 -0
  22. package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
  23. package/src/infrastructure/http/index.ts +0 -0
  24. package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
  25. package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
  26. package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
  27. package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
  28. package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
  29. package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
  30. package/src/lib/brands.ts +0 -0
  31. package/src/lib/httpHelper.js +0 -0
  32. package/src/lib/lastUpdated.js +0 -0
  33. package/src/lib/sanity/query.ts +0 -0
  34. package/src/services/api/types.js +0 -0
  35. package/src/services/api/types.ts +0 -0
  36. package/src/services/awards/award-callbacks.js +126 -0
  37. package/src/services/awards/award-query.js +327 -0
  38. package/src/services/awards/internal/.indexignore +1 -0
  39. package/src/services/awards/internal/award-definitions.js +239 -0
  40. package/src/services/awards/internal/award-events.js +102 -0
  41. package/src/services/awards/internal/award-manager.js +162 -0
  42. package/src/services/awards/internal/certificate-builder.js +66 -0
  43. package/src/services/awards/internal/completion-data-generator.js +84 -0
  44. package/src/services/awards/internal/content-progress-observer.js +137 -0
  45. package/src/services/awards/internal/image-utils.js +62 -0
  46. package/src/services/awards/internal/message-generator.js +17 -0
  47. package/src/services/awards/internal/types.js +5 -0
  48. package/src/services/awards/types.d.ts +79 -0
  49. package/src/services/awards/types.js +101 -0
  50. package/src/services/config.js +24 -4
  51. package/src/services/content/artist.ts +0 -0
  52. package/src/services/content/content.ts +0 -0
  53. package/src/services/content/genre.ts +0 -0
  54. package/src/services/content/instructor.ts +0 -0
  55. package/src/services/content-org/content-org.js +0 -0
  56. package/src/services/content-org/guided-courses.ts +0 -0
  57. package/src/services/content-org/learning-paths.ts +19 -15
  58. package/src/services/content-org/playlists-types.js +0 -0
  59. package/src/services/content-org/playlists.js +0 -0
  60. package/src/services/content.js +0 -0
  61. package/src/services/contentAggregator.js +0 -0
  62. package/src/services/contentLikes.js +0 -0
  63. package/src/services/contentProgress.js +2 -1
  64. package/src/services/dataContext.js +0 -0
  65. package/src/services/dateUtils.js +0 -0
  66. package/src/services/eventsAPI.js +0 -0
  67. package/src/services/forums/forums.ts +0 -0
  68. package/src/services/forums/posts.ts +0 -0
  69. package/src/services/forums/threads.ts +0 -0
  70. package/src/services/forums/types.ts +0 -0
  71. package/src/services/gamification/awards.ts +114 -83
  72. package/src/services/gamification/gamification.js +0 -0
  73. package/src/services/imageSRCBuilder.js +0 -0
  74. package/src/services/imageSRCVerify.js +0 -0
  75. package/src/services/liveTesting.ts +0 -0
  76. package/src/services/permissions/PermissionsAdapter.ts +0 -0
  77. package/src/services/permissions/PermissionsAdapterFactory.ts +0 -0
  78. package/src/services/permissions/PermissionsV1Adapter.ts +0 -0
  79. package/src/services/permissions/PermissionsV2Adapter.ts +0 -0
  80. package/src/services/permissions/README.md +0 -0
  81. package/src/services/permissions/index.ts +0 -0
  82. package/src/services/progress-events.js +58 -0
  83. package/src/services/progress-row/method-card.js +20 -5
  84. package/src/services/railcontent.js +0 -0
  85. package/src/services/recommendations.js +0 -0
  86. package/src/services/reporting/README.md +0 -0
  87. package/src/services/reporting/reporting.ts +0 -0
  88. package/src/services/reporting/types.ts +0 -0
  89. package/src/services/sanity.js +1 -1
  90. package/src/services/sentry/.indexignore +0 -0
  91. package/src/services/sentry/index.ts +0 -0
  92. package/src/services/sync/.indexignore +0 -0
  93. package/src/services/sync/adapters/factory.ts +0 -0
  94. package/src/services/sync/adapters/lokijs.ts +0 -0
  95. package/src/services/sync/adapters/sqlite.ts +0 -0
  96. package/src/services/sync/concurrency-safety.ts +0 -0
  97. package/src/services/sync/context/index.ts +0 -0
  98. package/src/services/sync/context/providers/base.ts +0 -0
  99. package/src/services/sync/context/providers/connectivity.ts +0 -0
  100. package/src/services/sync/context/providers/durability.ts +0 -0
  101. package/src/services/sync/context/providers/index.ts +0 -0
  102. package/src/services/sync/context/providers/session.ts +0 -0
  103. package/src/services/sync/context/providers/tabs.ts +0 -0
  104. package/src/services/sync/context/providers/visibility.ts +0 -0
  105. package/src/services/sync/database/factory.ts +0 -0
  106. package/src/services/sync/errors/boundary.ts +0 -0
  107. package/src/services/sync/errors/index.ts +0 -0
  108. package/src/services/sync/fetch.ts +10 -2
  109. package/src/services/sync/index.ts +0 -0
  110. package/src/services/sync/manager.ts +6 -0
  111. package/src/services/sync/models/Base.ts +0 -0
  112. package/src/services/sync/models/ContentLike.ts +0 -0
  113. package/src/services/sync/models/ContentProgress.ts +5 -6
  114. package/src/services/sync/models/Practice.ts +0 -0
  115. package/src/services/sync/models/PracticeDayNote.ts +0 -0
  116. package/src/services/sync/models/UserAwardProgress.ts +55 -0
  117. package/src/services/sync/models/index.ts +1 -0
  118. package/src/services/sync/repositories/base.ts +0 -0
  119. package/src/services/sync/repositories/content-likes.ts +0 -0
  120. package/src/services/sync/repositories/content-progress.ts +47 -25
  121. package/src/services/sync/repositories/index.ts +1 -0
  122. package/src/services/sync/repositories/practice-day-notes.ts +0 -0
  123. package/src/services/sync/repositories/practices.ts +16 -1
  124. package/src/services/sync/repositories/user-award-progress.ts +133 -0
  125. package/src/services/sync/repository-proxy.ts +6 -0
  126. package/src/services/sync/resolver.ts +0 -0
  127. package/src/services/sync/retry.ts +12 -11
  128. package/src/services/sync/run-scope.ts +0 -0
  129. package/src/services/sync/schema/index.ts +18 -3
  130. package/src/services/sync/serializers/index.ts +0 -0
  131. package/src/services/sync/serializers/model.ts +0 -0
  132. package/src/services/sync/serializers/raw.ts +0 -0
  133. package/src/services/sync/store/index.ts +53 -8
  134. package/src/services/sync/store/push-coalescer.ts +3 -3
  135. package/src/services/sync/store-configs.ts +7 -1
  136. package/src/services/sync/strategies/base.ts +0 -0
  137. package/src/services/sync/strategies/index.ts +0 -0
  138. package/src/services/sync/strategies/initial.ts +0 -0
  139. package/src/services/sync/strategies/polling.ts +0 -0
  140. package/src/services/sync/telemetry/index.ts +0 -0
  141. package/src/services/sync/telemetry/sampling.ts +0 -0
  142. package/src/services/sync/utils/event-emitter.ts +0 -0
  143. package/src/services/sync/utils/index.ts +0 -0
  144. package/src/services/sync/utils/throttle.ts +0 -0
  145. package/src/services/sync/utils/timers.ts +0 -0
  146. package/src/services/types.js +0 -0
  147. package/src/services/user/account.ts +0 -0
  148. package/src/services/user/chat.js +0 -0
  149. package/src/services/user/interests.js +0 -0
  150. package/src/services/user/management.js +0 -0
  151. package/src/services/user/memberships.ts +0 -0
  152. package/src/services/user/notifications.js +0 -0
  153. package/src/services/user/onboarding.ts +0 -0
  154. package/src/services/user/payments.ts +0 -0
  155. package/src/services/user/permissions.js +0 -0
  156. package/src/services/user/profile.js +0 -0
  157. package/src/services/user/sessions.js +0 -0
  158. package/src/services/user/types.d.ts +0 -0
  159. package/src/services/user/types.js +0 -0
  160. package/src/services/user/user-management-system.js +0 -0
  161. package/src/services/userActivity.js +0 -1
  162. package/test/HttpClient.test.js +6 -6
  163. package/test/awards/award-alacarte-observer.test.js +196 -0
  164. package/test/awards/award-auto-refresh.test.js +83 -0
  165. package/test/awards/award-calculations.test.js +33 -0
  166. package/test/awards/award-certificate-display.test.js +328 -0
  167. package/test/awards/award-collection-edge-cases.test.js +210 -0
  168. package/test/awards/award-collection-filtering.test.js +285 -0
  169. package/test/awards/award-completion-flow.test.js +213 -0
  170. package/test/awards/award-exclusion-handling.test.js +273 -0
  171. package/test/awards/award-multi-lesson.test.js +241 -0
  172. package/test/awards/award-observer-integration.test.js +325 -0
  173. package/test/awards/award-query-messages.test.js +438 -0
  174. package/test/awards/award-user-collection.test.js +412 -0
  175. package/test/awards/duplicate-prevention.test.js +118 -0
  176. package/test/awards/helpers/completion-mock.js +54 -0
  177. package/test/awards/helpers/index.js +3 -0
  178. package/test/awards/helpers/mock-setup.js +69 -0
  179. package/test/awards/helpers/progress-emitter.js +39 -0
  180. package/test/awards/message-generator.test.js +162 -0
  181. package/test/content.test.js +0 -0
  182. package/test/contentLikes.test.js +0 -0
  183. package/test/contentProgress.test.js +0 -0
  184. package/test/dataContext.test.js +0 -0
  185. package/test/forum.test.js +0 -0
  186. package/test/imageSRCBuilder.test.js +0 -0
  187. package/test/imageSRCVerify.test.js +0 -0
  188. package/test/initializeTests.js +6 -0
  189. package/test/learningPaths.test.js +0 -0
  190. package/test/lib/lastUpdated.test.js +0 -0
  191. package/test/live/contentProgressLive.test.js +0 -0
  192. package/test/live/railcontentLive.test.js +0 -0
  193. package/test/localStorageMock.js +0 -0
  194. package/test/log.js +0 -0
  195. package/test/mockData/award-definitions.js +171 -0
  196. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
  197. package/test/mockData/mockData_progress_content.json +0 -0
  198. package/test/mockData/mockData_sanity_progress_content.json +0 -0
  199. package/test/mockData/mockData_user_practices.json +0 -0
  200. package/test/notifications.test.js +0 -0
  201. package/test/progressRows.test.js +0 -0
  202. package/test/sanityQueryService.test.js +0 -0
  203. package/test/streakMessage.test.js +0 -0
  204. package/test/sync/models/award-database-integration.test.js +519 -0
  205. package/test/user/permissions.test.js +0 -0
  206. package/test/userActivity.test.js +0 -0
  207. package/tools/generate-index.cjs +9 -0
  208. package/.claude/settings.local.json +0 -14
  209. package/.yarnrc.yml +0 -1
  210. package/test/reporting.test.js +0 -132
@@ -0,0 +1,137 @@
1
+ import { awardManager } from './award-manager'
2
+ import { awardDefinitions } from './award-definitions'
3
+ import { onProgressSaved } from '../../progress-events'
4
+ import { COLLECTION_TYPE } from '../../sync/models/ContentProgress'
5
+
6
+
7
+ class ContentProgressObserver {
8
+ constructor() {
9
+ this.subscription = null
10
+ this.progressEventUnsubscribe = null
11
+ this.isObserving = false
12
+ this.processingContentIds = new Set()
13
+ this.debounceTimers = new Map()
14
+ this.debounceMs = 50
15
+ this.allChildIds = new Set()
16
+ }
17
+
18
+ /** @returns {Promise<() => void>} */
19
+ async start(database) {
20
+ if (this.isObserving) {
21
+ return () => {}
22
+ }
23
+
24
+ await awardDefinitions.refresh()
25
+ const allAwards = await awardDefinitions.getAll()
26
+
27
+ this.allChildIds.clear()
28
+ for (const award of allAwards) {
29
+ if (award.content_id) {
30
+ this.allChildIds.add(award.content_id)
31
+ }
32
+ if (award.child_ids) {
33
+ award.child_ids.forEach(childId => this.allChildIds.add(childId))
34
+ }
35
+ }
36
+
37
+ if (this.allChildIds.size === 0) {
38
+ return () => {}
39
+ }
40
+
41
+ this.progressEventUnsubscribe = onProgressSaved((event) => {
42
+ if (this.allChildIds.has(event.contentId)) {
43
+ this.handleProgressChange({
44
+ content_id: event.contentId,
45
+ state: event.progressStatus,
46
+ progress_percent: event.progressPercent,
47
+ collection_type: event.collectionType,
48
+ collection_id: event.collectionId
49
+ })
50
+ }
51
+ })
52
+
53
+ this.isObserving = true
54
+
55
+ return () => this.stop()
56
+ }
57
+
58
+ /** @returns {Promise<void>} */
59
+ async handleProgressChange(progressRecord) {
60
+ try {
61
+ const childContentId = progressRecord.content_id
62
+ const collectionType = progressRecord.collection_type
63
+ const collectionId = progressRecord.collection_id
64
+
65
+ const allAwards = await awardDefinitions.getAll()
66
+ let parentAwards = []
67
+
68
+ const isLearningPathContext = collectionType === COLLECTION_TYPE.LEARNING_PATH && collectionId
69
+ if (isLearningPathContext) {
70
+ parentAwards = allAwards.filter(award => {
71
+ return award.child_ids?.includes(childContentId) &&
72
+ award.content_type === collectionType &&
73
+ award.content_id === collectionId
74
+ })
75
+ } else {
76
+ parentAwards = allAwards.filter(award => award.child_ids?.includes(childContentId))
77
+ }
78
+
79
+ for (const award of parentAwards) {
80
+ if (award.content_id) {
81
+ this.debounceAwardCheck(award.content_id)
82
+ }
83
+ }
84
+ } catch (error) {
85
+ console.error('[ContentProgressObserver] Error handling progress change:', error)
86
+ }
87
+ }
88
+
89
+ debounceAwardCheck(contentId) {
90
+ if (this.debounceTimers.has(contentId)) {
91
+ clearTimeout(this.debounceTimers.get(contentId))
92
+ }
93
+
94
+ const timerId = setTimeout(async () => {
95
+ this.debounceTimers.delete(contentId)
96
+ await this.checkAward(contentId)
97
+ }, this.debounceMs)
98
+
99
+ this.debounceTimers.set(contentId, timerId)
100
+ }
101
+
102
+ /** @returns {Promise<void>} */
103
+ async checkAward(contentId) {
104
+ if (this.processingContentIds.has(contentId)) {
105
+ return
106
+ }
107
+
108
+ this.processingContentIds.add(contentId)
109
+
110
+ try {
111
+ await awardManager.onContentCompleted(contentId)
112
+ } catch (error) {
113
+ console.error(`[ContentProgressObserver] Error checking award for content ${contentId}:`, error)
114
+ } finally {
115
+ this.processingContentIds.delete(contentId)
116
+ }
117
+ }
118
+
119
+ stop() {
120
+ if (this.progressEventUnsubscribe) {
121
+ this.progressEventUnsubscribe()
122
+ this.progressEventUnsubscribe = null
123
+ }
124
+
125
+ for (const timerId of this.debounceTimers.values()) {
126
+ clearTimeout(timerId)
127
+ }
128
+ this.debounceTimers.clear()
129
+ this.processingContentIds.clear()
130
+ this.allChildIds.clear()
131
+
132
+ this.isObserving = false
133
+ }
134
+ }
135
+
136
+
137
+ export const contentProgressObserver = new ContentProgressObserver()
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @module Awards
3
+ */
4
+
5
+
6
+ /**
7
+ * @param {string} url
8
+ * @returns {Promise<string>}
9
+ */
10
+ export async function urlToBase64(url) {
11
+ try {
12
+ if (!url) {
13
+ return ''
14
+ }
15
+
16
+ const response = await fetch(url)
17
+
18
+ if (!response.ok) {
19
+ console.warn(`Failed to fetch image: ${url}`)
20
+ return ''
21
+ }
22
+
23
+ const blob = await response.blob()
24
+
25
+ return new Promise((resolve, reject) => {
26
+ const reader = new FileReader()
27
+
28
+ reader.onloadend = () => {
29
+ const base64String = reader.result
30
+ const base64Data = base64String.split(',')[1]
31
+ resolve(base64Data || '')
32
+ }
33
+
34
+ reader.onerror = () => {
35
+ console.error(`Failed to convert image to base64: ${url}`)
36
+ reject(new Error('Failed to convert image to base64'))
37
+ }
38
+
39
+ reader.readAsDataURL(blob)
40
+ })
41
+ } catch (error) {
42
+ console.error(`Error converting URL to base64: ${url}`, error)
43
+ return ''
44
+ }
45
+ }
46
+
47
+ /**
48
+ * @param {Object<string, string>} urlMap
49
+ * @returns {Promise<Object<string, string>>}
50
+ */
51
+ export async function urlMapToBase64(urlMap) {
52
+ const entries = Object.entries(urlMap)
53
+
54
+ const results = await Promise.all(
55
+ entries.map(async ([key, url]) => {
56
+ const base64 = await urlToBase64(url)
57
+ return [key, base64]
58
+ })
59
+ )
60
+
61
+ return Object.fromEntries(results)
62
+ }
@@ -0,0 +1,17 @@
1
+ export class AwardMessageGenerator {
2
+ /** @returns {string} */
3
+ static generatePopupMessage(completionData) {
4
+ const { days_user_practiced, practice_minutes, content_title } = completionData
5
+ const daysLabel = days_user_practiced === 1 ? 'day' : 'days'
6
+
7
+ return `You received this award for completing ${content_title}! You practiced a total of ${practice_minutes} minutes over the past ${days_user_practiced} ${daysLabel}.`
8
+ }
9
+
10
+ /** @returns {string} */
11
+ static generateCertificateMessage(completionData, awardCustomText) {
12
+ const { content_title, practice_minutes } = completionData
13
+ const customText = awardCustomText ? ` ${awardCustomText}` : ''
14
+
15
+ return `You practiced for a total of ${practice_minutes} minutes during ${content_title}, earning your official certificate of completion.${customText} Well Done!`
16
+ }
17
+ }
@@ -0,0 +1,5 @@
1
+ /** @typedef {Object} AwardDefinition */
2
+ /** @typedef {Object} CompletionData */
3
+ /** @typedef {Object} CertificateData */
4
+
5
+ export {}
@@ -0,0 +1,79 @@
1
+ export interface AwardDefinition {
2
+ _id: string
3
+ is_active: boolean
4
+ name: string
5
+ logo: string | null
6
+ badge: string
7
+ award: string
8
+ content_id: number
9
+ content_type: string
10
+ type: string
11
+ brand: string
12
+ content_title: string
13
+ award_custom_text: string | null
14
+ instructor_name: string | null
15
+ child_ids: number[]
16
+ }
17
+
18
+ export interface CompletionData {
19
+ content_title: string
20
+ completed_at: string
21
+ days_user_practiced: number
22
+ practice_minutes: number
23
+ }
24
+
25
+ export interface AwardCompletionData {
26
+ completed_at: string
27
+ days_user_practiced: number
28
+ practice_minutes: number
29
+ content_title: string
30
+ message: string
31
+ }
32
+
33
+ export interface AwardInfo {
34
+ awardId: string
35
+ awardTitle: string
36
+ badge: string
37
+ award: string
38
+ brand: string
39
+ instructorName: string
40
+ progressPercentage: number
41
+ isCompleted: boolean
42
+ completedAt: string | null
43
+ completionData: AwardCompletionData | null
44
+ }
45
+
46
+ export interface ContentAwardsResponse {
47
+ hasAwards: boolean
48
+ awards: AwardInfo[]
49
+ }
50
+
51
+ export interface AwardStatistics {
52
+ totalAvailable: number
53
+ completed: number
54
+ inProgress: number
55
+ notStarted: number
56
+ completionPercentage: number
57
+ }
58
+
59
+ export interface AwardPaginationOptions {
60
+ limit?: number
61
+ offset?: number
62
+ }
63
+
64
+ export interface AwardCallbackPayload {
65
+ awardId: string
66
+ name: string
67
+ badge: string
68
+ completed_at: string
69
+ completion_data: AwardCompletionData
70
+ }
71
+
72
+ export interface ProgressCallbackPayload {
73
+ awardId: string
74
+ progressPercentage: number
75
+ }
76
+
77
+ export type AwardCallbackFunction = (award: AwardCallbackPayload) => void
78
+ export type ProgressCallbackFunction = (progress: ProgressCallbackPayload) => void
79
+ export type UnregisterFunction = () => void
@@ -0,0 +1,101 @@
1
+ /**
2
+ * @typedef {Object} AwardDefinition
3
+ * @property {string} _id - Unique Sanity award ID
4
+ * @property {boolean} is_active - Whether the award is active
5
+ * @property {string} name - Display name of the award
6
+ * @property {string|null} logo - URL to logo image
7
+ * @property {string} badge - URL to badge image
8
+ * @property {string} award - URL to award image
9
+ * @property {number} content_id - Railcontent ID of the parent content
10
+ * @property {string} content_type - Type of content (guided-course, learning-path-v2, etc.)
11
+ * @property {string} type - Sanity document type
12
+ * @property {string} brand - Brand (drumeo, pianote, guitareo, singeo)
13
+ * @property {string} content_title - Title of the associated content
14
+ * @property {string|null} award_custom_text - Custom text for the award
15
+ * @property {string|null} instructor_name - Name of the instructor
16
+ * @property {number[]} child_ids - Array of child content IDs required for completion
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} CompletionData
21
+ * @property {string} content_title - Title of the completed content
22
+ * @property {string} completed_at - ISO timestamp of completion
23
+ * @property {number} days_user_practiced - Number of days the user practiced
24
+ * @property {number} practice_minutes - Total practice time in minutes
25
+ */
26
+
27
+ /**
28
+ * @typedef {Object} AwardCompletionData
29
+ * @property {string} completed_at - ISO timestamp of completion
30
+ * @property {number} days_user_practiced - Number of days the user practiced
31
+ * @property {number} practice_minutes - Total practice time in minutes
32
+ * @property {string} content_title - Title of the completed content
33
+ * @property {string} message - Congratulations message for display
34
+ */
35
+
36
+ /**
37
+ * @typedef {Object} AwardInfo
38
+ * @property {string} awardId - Unique Sanity award ID
39
+ * @property {string} awardTitle - Display name of the award
40
+ * @property {string} badge - URL to badge image
41
+ * @property {string} award - URL to award image
42
+ * @property {string} brand - Brand (drumeo, pianote, guitareo, singeo)
43
+ * @property {string} instructorName - Name of the instructor
44
+ * @property {number} progressPercentage - Completion percentage (0-100)
45
+ * @property {boolean} isCompleted - Whether the award is fully completed
46
+ * @property {string|null} completedAt - ISO timestamp of completion, or null if not completed
47
+ * @property {AwardCompletionData|null} completionData - Practice statistics, or null if not started
48
+ */
49
+
50
+ /**
51
+ * @typedef {Object} ContentAwardsResponse
52
+ * @property {boolean} hasAwards - Whether the content has any associated awards
53
+ * @property {AwardInfo[]} awards - Array of award objects with progress information
54
+ */
55
+
56
+ /**
57
+ * @typedef {Object} AwardStatistics
58
+ * @property {number} totalAvailable - Total number of awards available
59
+ * @property {number} completed - Number of completed awards
60
+ * @property {number} inProgress - Number of awards in progress
61
+ * @property {number} notStarted - Number of awards not yet started
62
+ * @property {number} completionPercentage - Overall completion percentage (0-100)
63
+ */
64
+
65
+ /**
66
+ * @typedef {Object} AwardPaginationOptions
67
+ * @property {number} [limit] - Maximum number of results to return
68
+ * @property {number} [offset] - Number of results to skip for pagination
69
+ */
70
+
71
+ /**
72
+ * @typedef {Object} AwardCallbackPayload
73
+ * @property {string} awardId - Unique Sanity award ID
74
+ * @property {string} name - Display name of the award
75
+ * @property {string} badge - URL to badge image
76
+ * @property {string} completed_at - ISO timestamp of completion
77
+ * @property {AwardCompletionData} completion_data - Practice statistics
78
+ */
79
+
80
+ /**
81
+ * @typedef {Object} ProgressCallbackPayload
82
+ * @property {string} awardId - Unique Sanity award ID
83
+ * @property {number} progressPercentage - Completion percentage (0-100)
84
+ */
85
+
86
+ /**
87
+ * @callback AwardCallbackFunction
88
+ * @param {AwardCallbackPayload} award - Award data when an award is earned
89
+ * @returns {void}
90
+ */
91
+
92
+ /**
93
+ * @callback ProgressCallbackFunction
94
+ * @param {ProgressCallbackPayload} progress - Progress data when award progress changes
95
+ * @returns {void}
96
+ */
97
+
98
+ /**
99
+ * @callback UnregisterFunction
100
+ * @returns {void}
101
+ */
@@ -25,11 +25,11 @@ const excludeFromGeneratedIndex = []
25
25
  /**
26
26
  * Initializes the service with the given configuration.
27
27
  * This function must be called before using any other functions in this library.
28
+ * Automatically initializes award definitions with 24-hour cache in the background.
28
29
  *
29
30
  * @param {Config} config - Configuration object containing API settings.
30
31
  *
31
- * @example
32
- * // Initialize the service in your app.js
32
+ * @example Web Application
33
33
  * initializeService({
34
34
  * sanityConfig: {
35
35
  * token: 'your-sanity-api-token',
@@ -43,18 +43,30 @@ const excludeFromGeneratedIndex = []
43
43
  * token: 'your-user-api-token',
44
44
  * userId: 'current-user-id',
45
45
  * baseUrl: 'https://web-staging-one.musora.com',
46
- * authToken 'your-auth-token',
46
+ * authToken: 'your-auth-token',
47
47
  * },
48
48
  * sessionConfig: {
49
49
  * token: 'your-user-api-token',
50
50
  * userId: 'current-user-id',
51
- * authToken 'your-auth-token',
51
+ * authToken: 'your-auth-token',
52
52
  * },
53
53
  * baseUrl: 'https://web-staging-one.musora.com',
54
54
  * localStorage: localStorage,
55
55
  * isMA: false,
56
56
  * permissionsVersion: 'v1', // Optional: 'v1' (default) or 'v2'
57
57
  * });
58
+ *
59
+ * @example React Native Application
60
+ * import AsyncStorage from '@react-native-async-storage/async-storage'
61
+ *
62
+ * initializeService({
63
+ * sanityConfig: { ... },
64
+ * railcontentConfig: { ... },
65
+ * sessionConfig: { ... },
66
+ * baseUrl: 'https://web-staging-one.musora.com',
67
+ * localStorage: AsyncStorage,
68
+ * isMA: true,
69
+ * });
58
70
  */
59
71
  export function initializeService(config) {
60
72
  globalConfig.sanityConfig = config.sanityConfig
@@ -65,4 +77,12 @@ export function initializeService(config) {
65
77
  globalConfig.isMA = config.isMA || false
66
78
  globalConfig.localTimezoneString = config.localTimezoneString || null
67
79
  globalConfig.permissionsVersion = config.permissionsVersion || 'v2'
80
+
81
+ if (config.localStorage) {
82
+ import('./awards/internal/award-definitions')
83
+ .then(({ awardDefinitions }) => awardDefinitions.initialize())
84
+ .catch(error => {
85
+ console.error('Failed to initialize award definitions:', error)
86
+ })
87
+ }
68
88
  }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -173,7 +173,7 @@ export async function fetchLearningPathLessons(
173
173
  userDate: Date
174
174
  ) {
175
175
  const learningPath = await getEnrichedLearningPath(learningPathId)
176
- let dailySession = await getDailySession(brand, userDate)
176
+ let dailySession = await getDailySession(brand, userDate) // what if the call just fails, and a DS does exist?
177
177
  if (!dailySession) {
178
178
  dailySession = await updateDailySession(brand, userDate, false)
179
179
  }
@@ -185,19 +185,22 @@ export async function fetchLearningPathLessons(
185
185
  is_active_learning_path: isActiveLearningPath,
186
186
  }
187
187
  }
188
+ // this assumes that the first entry is active_path, based on user flows
188
189
  const todayContentIds = dailySession.daily_session[0]?.content_ids || []
189
- const previousLearningPathId = dailySession.daily_session[0]?.learning_path_id
190
+ const todayLearningPathId = dailySession.daily_session[0]?.learning_path_id
191
+
190
192
  const nextContentIds = dailySession.daily_session[1]?.content_ids || []
191
193
  const nextLearningPathId = dailySession.daily_session[1]?.learning_path_id
194
+
192
195
  const completedLessons = []
193
- let todaysLessons = []
194
- let nextLPLessons = []
196
+ let thisLPDailies = []
197
+ let nextLPDailies = []
195
198
  let previousLearningPathTodays = []
196
199
  const upcomingLessons = []
197
200
 
198
201
  learningPath.children.forEach((lesson: any) => {
199
202
  if (todayContentIds.includes(lesson.id)) {
200
- todaysLessons.push(lesson)
203
+ thisLPDailies.push(lesson)
201
204
  } else if (lesson.progressStatus === 'completed') {
202
205
  completedLessons.push(lesson)
203
206
  } else {
@@ -205,22 +208,23 @@ export async function fetchLearningPathLessons(
205
208
  }
206
209
  })
207
210
 
208
- if (todaysLessons.length == 0) {
211
+ if (thisLPDailies.length == 0) {
209
212
  // Daily sessions first lessons are not part of the active learning path, but next lessons are
210
213
  // load todays lessons from previous learning path
211
214
  previousLearningPathTodays = await getLearningPathLessonsByIds(
212
215
  todayContentIds,
213
- previousLearningPathId
216
+ todayLearningPathId
214
217
  )
215
- } else if (
216
- nextContentIds.length > 0 &&
217
- todaysLessons.length < 3 &&
218
- upcomingLessons.length === 0
218
+ } else if ( // show next LP dailies if they exist
219
+ nextContentIds.length > 0
219
220
  ) {
220
221
  // Daily sessions first lessons are the active learning path and the next lessons are not
221
222
  // load next lessons from next learning path
222
- // TODO: update item status to locked when the current learning path is not complete
223
- nextLPLessons = await getLearningPathLessonsByIds(nextContentIds, nextLearningPathId)
223
+ const lessons = await getLearningPathLessonsByIds(nextContentIds, nextLearningPathId)
224
+ nextLPDailies = lessons.map(lesson => ({
225
+ ...lesson,
226
+ in_next_learning_path: STATE.COMPLETED === learningPath.progressStatus
227
+ }))
224
228
  }
225
229
 
226
230
 
@@ -231,8 +235,8 @@ export async function fetchLearningPathLessons(
231
235
  active_learning_path_id: dailySession?.active_learning_path_id,
232
236
  active_learning_path_created_at: dailySession?.active_learning_path_created_at,
233
237
  upcoming_lessons: upcomingLessons,
234
- todays_lessons: todaysLessons,
235
- next_learning_path_lessons: nextLPLessons,
238
+ todays_lessons: thisLPDailies,
239
+ next_learning_path_lessons: nextLPDailies,
236
240
  next_learning_path_id: nextLearningPathId,
237
241
  completed_lessons: completedLessons,
238
242
  previous_learning_path_todays: previousLearningPathTodays,
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -432,9 +432,10 @@ function normalizeContentIds(contentIds) {
432
432
  function normalizeCollection(collection) {
433
433
  if (!collection) return null
434
434
 
435
- if (COLLECTION_TYPE.indexOf(collection.type) === -1) {
435
+ if (!Object.values(COLLECTION_TYPE).includes(collection.type)) {
436
436
  throw new Error(`Invalid collection type: ${collection.type}`)
437
437
  }
438
+
438
439
  if (typeof collection.id === 'string' && isNaN(+collection.id)) {
439
440
  throw new Error(`Invalid collection id: ${collection.id}`)
440
441
  }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes