musora-content-services 2.136.4 → 2.137.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.
@@ -1,7 +1,7 @@
1
1
  name: Deploy Docs to GitHub Pages
2
2
  on:
3
3
  push:
4
- branches: [main, project-v2]
4
+ branches: [main]
5
5
 
6
6
  # Required permissions for GitHub Pages deployment
7
7
  permissions:
@@ -28,37 +28,24 @@ jobs:
28
28
  ref: main
29
29
  path: main-content
30
30
 
31
- - name: Checkout project-v2 branch
32
- uses: actions/checkout@v4
33
- with:
34
- ref: project-v2
35
- path: v2-content
36
-
37
31
  - name: Setup Node.js
38
32
  uses: actions/setup-node@v4
39
33
  with:
40
34
  node-version: '20'
41
35
 
42
36
  - name: Install dependencies for v2
43
- working-directory: v2-content
37
+ working-directory: main-content
44
38
  run: npm ci
45
39
 
46
40
  - name: Generate v2 documentation
47
- working-directory: v2-content
41
+ working-directory: main-content
48
42
  run: npm run doc
49
43
 
50
44
  - name: Combine v1 and v2 docs into deployment structure
51
45
  run: |
52
46
  mkdir -p _site
53
- # Copy existing v1 docs from main branch (already committed)
54
- if [ -d "main-content/docs" ]; then
55
- cp -r main-content/docs/* _site/
56
- echo "✅ Copied existing v1 docs from main branch"
57
- fi
58
- # Copy v2 docs to /v2/ subdirectory
59
- mkdir -p _site/v2
60
- cp -r v2-content/docs/* _site/v2/
61
- echo "✅ Combined v1 (root) and v2 (/v2/) documentation"
47
+ cp -r main-content/docs/* _site/
48
+ echo "✅ Copied existing docs from main branch"
62
49
 
63
50
  - name: Setup GitHub Pages
64
51
  uses: actions/configure-pages@v4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [2.137.1](https://github.com/railroadmedia/musora-content-services/compare/v2.137.0...v2.137.1) (2026-02-25)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **T3PS-1975:** Add functions for sub platform request ([dea8900](https://github.com/railroadmedia/musora-content-services/commit/dea89007b67fe3dfdffaa45feb9f4f0af8a42123))
11
+ * **T3PS-2159:** Add hasCertificate to award object in callback ([f742b8e](https://github.com/railroadmedia/musora-content-services/commit/f742b8e32556854518316f51003134544421eb8c))
12
+ * unset syncmanager instance late ([#830](https://github.com/railroadmedia/musora-content-services/issues/830)) ([a908c03](https://github.com/railroadmedia/musora-content-services/commit/a908c03bd4de1ec4b1020ef63b1eeab7eeff6bea))
13
+
14
+ ## [2.137.0](https://github.com/railroadmedia/musora-content-services/compare/v2.136.4...v2.137.0) (2026-02-24)
15
+
16
+
17
+ ### Features
18
+
19
+ * add collection data in fetch sibling content ([#836](https://github.com/railroadmedia/musora-content-services/issues/836)) ([6472764](https://github.com/railroadmedia/musora-content-services/commit/6472764b41da98b0754aec91b4714b70444f8b84))
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * **T3PS-2156:** changes what is shown on Your Progress page (lessons) ([#821](https://github.com/railroadmedia/musora-content-services/issues/821)) ([58cfc1b](https://github.com/railroadmedia/musora-content-services/commit/58cfc1b6660471fcb50ebfe15865a9b69c8a8830))
25
+ * **T3PS-2273:** Add function for retrieving latest subscription platform ([c3e74ce](https://github.com/railroadmedia/musora-content-services/commit/c3e74cecb43619d69912a04644e01ffbb0ff4cc1))
26
+
5
27
  ### [2.136.4](https://github.com/railroadmedia/musora-content-services/compare/v2.136.3...v2.136.4) (2026-02-20)
6
28
 
7
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.136.4",
3
+ "version": "2.137.1",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -22,6 +22,10 @@ export const SINGLE_PARENT_TYPES = ['course-lesson', 'pack-bundle-lesson', 'song
22
22
 
23
23
  export const LEARNING_PATH_LESSON = 'learning-path-lesson-v2'
24
24
 
25
+ export const parentField = 'parent_content_data[0]'
26
+
27
+ export const grandParentField = 'parent_content_data[1]'
28
+
25
29
  export const genreField = `genre[]->{
26
30
  name,
27
31
  'slug': slug.current,
@@ -224,11 +228,7 @@ export const individualLessonsTypes = [
224
228
  ...studentArchivesLessonTypes,
225
229
  ]
226
230
 
227
- export const coursesLessonTypes = [
228
- 'course',
229
- 'course-collection',
230
- 'guided-course',
231
- ]
231
+ export const coursesLessonTypes = ['course', 'course-collection', 'guided-course']
232
232
 
233
233
  export const skillLessonTypes = ['skill-pack']
234
234
 
@@ -246,7 +246,7 @@ export const collectionLessonTypes = [...coursesLessonTypes]
246
246
 
247
247
  export const lessonTypesMapping = {
248
248
  lessons: singleLessonTypes,
249
- 'practice alongs': [ ...practiceAlongsLessonTypes, 'routine'],
249
+ 'practice alongs': [...practiceAlongsLessonTypes, 'routine'],
250
250
  'live archives': liveArchivesLessonTypes,
251
251
  performances: performancesLessonTypes,
252
252
  'student archives': studentArchivesLessonTypes,
@@ -272,7 +272,7 @@ export const lessonTypesMapping = {
272
272
  ...studentArchivesLessonTypes,
273
273
  ...practiceAlongsLessonTypes,
274
274
  ],
275
- routines: ['routine']
275
+ routines: ['routine'],
276
276
  }
277
277
 
278
278
  export const getNextLessonLessonParentTypes = [
@@ -294,7 +294,7 @@ export const progressTypesMapping = {
294
294
  'documentary-lesson',
295
295
  'live',
296
296
  'course-lesson',
297
- 'routine'
297
+ 'routine',
298
298
  ],
299
299
  course: ['course'],
300
300
  show: showsLessonTypes,
@@ -326,7 +326,7 @@ export const filterTypes = {
326
326
  ...coursesLessonTypes,
327
327
  ...skillLessonTypes,
328
328
  ...entertainmentLessonTypes,
329
- 'routine'
329
+ 'routine',
330
330
  ],
331
331
  songs: [
332
332
  ...tutorialsLessonTypes,
@@ -336,26 +336,22 @@ export const filterTypes = {
336
336
  ],
337
337
  }
338
338
 
339
+ const lessonRecentTypes = [
340
+ ...individualLessonsTypes,
341
+ 'skill-pack-lesson',
342
+ ...entertainmentLessonTypes,
343
+ 'course-lesson',
344
+ 'guided-course-lesson',
345
+ 'quick-tips',
346
+ 'routine',
347
+ ]
348
+
349
+ const songsRecentTypes = [...SONG_TYPES]
350
+
339
351
  export const recentTypes = {
340
- lessons: [
341
- ...individualLessonsTypes,
342
- 'skill-pack-lesson',
343
- ...entertainmentLessonTypes,
344
- 'course-lesson',
345
- 'guided-course-lesson',
346
- 'quick-tips',
347
- 'routine'
348
- ],
349
- songs: [...SONG_TYPES],
350
- home: [
351
- ...individualLessonsTypes,
352
- ...transcriptionsLessonTypes,
353
- ...playAlongLessonTypes,
354
- ...showsLessonTypes,
355
- ...getNextLessonLessonParentTypes,
356
- 'live',
357
- 'routine'
358
- ],
352
+ lessons: lessonRecentTypes,
353
+ songs: songsRecentTypes,
354
+ home: [...lessonRecentTypes, ...songsRecentTypes],
359
355
  }
360
356
 
361
357
  export const ownedContentTypes = {
@@ -694,7 +690,7 @@ export function getNewReleasesTypes(brand) {
694
690
  'song',
695
691
  'play-along',
696
692
  'course',
697
- 'skill-pack'
693
+ 'skill-pack',
698
694
  ]
699
695
  switch (brand) {
700
696
  case 'drumeo':
@@ -980,29 +976,29 @@ export const getFormattedType = (type, brand) => {
980
976
 
981
977
  export const awardTemplate = {
982
978
  drumeo: {
983
- front: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/drumeo.svg",
984
- rear: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/drumeo-rear.svg",
985
- unearned: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/drumeo-unearned.svg",
979
+ front: 'https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/drumeo.svg',
980
+ rear: 'https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/drumeo-rear.svg',
981
+ unearned: 'https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/drumeo-unearned.svg',
986
982
  },
987
983
  guitareo: {
988
- front: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/guitareo.svg",
989
- rear: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/guitareo-rear.svg",
990
- unearned: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/guitareo-unearned.svg",
984
+ front: 'https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/guitareo.svg',
985
+ rear: 'https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/guitareo-rear.svg',
986
+ unearned: 'https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/guitareo-unearned.svg',
991
987
  },
992
988
  pianote: {
993
- front: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/pianote.svg",
994
- rear: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/pianote-rear.svg",
995
- unearned: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/pianote-unearned.svg",
989
+ front: 'https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/pianote.svg',
990
+ rear: 'https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/pianote-rear.svg',
991
+ unearned: 'https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/pianote-unearned.svg',
996
992
  },
997
993
  singeo: {
998
- front: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/singeo.svg",
999
- rear: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/singeo-rear.svg",
1000
- unearned: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/singeo-unearned.svg",
994
+ front: 'https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/singeo.svg',
995
+ rear: 'https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/singeo-rear.svg',
996
+ unearned: 'https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/singeo-unearned.svg',
1001
997
  },
1002
998
  playbass: {
1003
- front: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/playbass.svg",
1004
- rear: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/playbass-rear.svg",
1005
- unearned: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/playbass-unearned.svg",
999
+ front: 'https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/playbass.svg',
1000
+ rear: 'https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/playbass-rear.svg',
1001
+ unearned: 'https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/playbass-unearned.svg',
1006
1002
  },
1007
1003
  musora: {
1008
1004
  front: null,
package/src/index.d.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
 
8
8
  import {
9
9
  getAwardStatistics,
10
+ getBadgeFields,
10
11
  getCompletedAwards,
11
12
  getContentAwards,
12
13
  getContentAwardsByIds,
@@ -369,6 +370,8 @@ import {
369
370
  } from './services/user/management.js';
370
371
 
371
372
  import {
373
+ fetchHasActivePlatformSubscription,
374
+ fetchLastSubscriptionPlatform,
372
375
  fetchMemberships,
373
376
  fetchRechargeTokens,
374
377
  getUpgradePrice,
@@ -523,11 +526,13 @@ declare module 'musora-content-services' {
523
526
  fetchGenreBySlug,
524
527
  fetchGenreLessons,
525
528
  fetchGenres,
529
+ fetchHasActivePlatformSubscription,
526
530
  fetchHierarchy,
527
531
  fetchInstructorBySlug,
528
532
  fetchInstructorLessons,
529
533
  fetchInstructors,
530
534
  fetchInterests,
535
+ fetchLastSubscriptionPlatform,
531
536
  fetchLatestThreads,
532
537
  fetchLearningPathHierarchy,
533
538
  fetchLearningPathLessons,
@@ -604,6 +609,7 @@ declare module 'musora-content-services' {
604
609
  getAllStarted,
605
610
  getAllStartedOrCompleted,
606
611
  getAwardStatistics,
612
+ getBadgeFields,
607
613
  getCompletedAwards,
608
614
  getContentAwards,
609
615
  getContentAwardsByIds,
package/src/index.js CHANGED
@@ -11,6 +11,7 @@ import {
11
11
 
12
12
  import {
13
13
  getAwardStatistics,
14
+ getBadgeFields,
14
15
  getCompletedAwards,
15
16
  getContentAwards,
16
17
  getContentAwardsByIds,
@@ -373,6 +374,8 @@ import {
373
374
  } from './services/user/management.js';
374
375
 
375
376
  import {
377
+ fetchHasActivePlatformSubscription,
378
+ fetchLastSubscriptionPlatform,
376
379
  fetchMemberships,
377
380
  fetchRechargeTokens,
378
381
  getUpgradePrice,
@@ -522,11 +525,13 @@ export {
522
525
  fetchGenreBySlug,
523
526
  fetchGenreLessons,
524
527
  fetchGenres,
528
+ fetchHasActivePlatformSubscription,
525
529
  fetchHierarchy,
526
530
  fetchInstructorBySlug,
527
531
  fetchInstructorLessons,
528
532
  fetchInstructors,
529
533
  fetchInterests,
534
+ fetchLastSubscriptionPlatform,
530
535
  fetchLatestThreads,
531
536
  fetchLearningPathHierarchy,
532
537
  fetchLearningPathLessons,
@@ -603,6 +608,7 @@ export {
603
608
  getAllStarted,
604
609
  getAllStartedOrCompleted,
605
610
  getAwardStatistics,
611
+ getBadgeFields,
606
612
  getCompletedAwards,
607
613
  getContentAwards,
608
614
  getContentAwardsByIds,
@@ -76,6 +76,7 @@ export function registerAwardCallback(callback) {
76
76
  brand: definition.brand,
77
77
  ...getBadgeFields(definition),
78
78
  contentType: definition.content_type,
79
+ hasCertificate: definition.type === 'content-award',
79
80
  completedAt: completionData.completed_at,
80
81
  isCompleted: true,
81
82
  completionData: {
@@ -275,10 +275,10 @@ export async function getLearningPathLessonsByIds(contentIds, learningPathId) {
275
275
  * @param options.parentContentId
276
276
  */
277
277
  export function mapContentToParent(
278
- lessons: any,
278
+ lessons: any,
279
279
  options?: { lessonType?: string; parentContentId?: number }
280
280
  ) {
281
- if (!lessons) return lessons
281
+ if (!lessons || (Array.isArray(lessons) && lessons.length === 0)) return lessons
282
282
 
283
283
  function mapIt(lesson: any) {
284
284
  const mappedLesson = { ...lesson }
@@ -342,6 +342,8 @@ export async function fetchLearningPathLessons(
342
342
  userDate: Date
343
343
  ) {
344
344
  const learningPath = await getEnrichedLearningPath(learningPathId)
345
+ if (!learningPath || learningPath.children?.length === 0) return null
346
+
345
347
  let dailySession = (await getDailySession(brand, userDate)) as DailySessionResponse
346
348
 
347
349
  const isActiveLearningPath = (dailySession?.active_learning_path_id || 0) == learningPathId
@@ -93,6 +93,9 @@ export async function getTabResults(brand, pageName, tabName, {
93
93
  sort = 'recommended',
94
94
  selectedFilters = []
95
95
  } = {}) {
96
+
97
+ if (!tabName && ['lessons', 'songs'].includes(pageName)) return { type: TabResponseType.CATALOG, data: [], meta: { filters: [], sort: {} } }
98
+
96
99
  // Extract and handle 'progress' filter separately
97
100
  const progressFilter = selectedFilters.find(f => f.startsWith('progress,')) || 'progress,all';
98
101
  const progressValue = progressFilter.split(',')[1].toLowerCase();
@@ -60,14 +60,13 @@ export function buildImageSRC(url, options = {}) {
60
60
  * @private
61
61
  */
62
62
  export function applySanityTransformations(url, options) {
63
- const { width, height, quality } = options
63
+ const { width, height } = options
64
64
 
65
- const sanityOptions = ['fm=webp']
65
+ const sanityOptions = ['q=100']
66
66
 
67
67
  // Dimensions
68
68
  if (width) sanityOptions.push(`w=${width}`)
69
69
  if (height) sanityOptions.push(`h=${height}`)
70
- if (quality) sanityOptions.push(`q=${quality}`)
71
70
 
72
71
  // Add parameters to Sanity URL
73
72
  const sanityQuery = sanityOptions.length > 0 ? `?${sanityOptions.join('&')}` : ''
@@ -8,15 +8,31 @@ import { getProgressState } from '../../contentProgress'
8
8
  import {COLLECTION_TYPE, STATE} from '../../sync/models/ContentProgress'
9
9
 
10
10
  export async function getMethodCard(brand) {
11
- const introVideo = await fetchMethodV2IntroVideo(brand)
12
-
13
- if (!introVideo) {
11
+ let introVideo
12
+ try {
13
+ introVideo = await fetchMethodV2IntroVideo(brand)
14
+ } catch (error) {
15
+ console.error('Error fetching method intro video:', error)
14
16
  return null
15
17
  }
16
18
 
17
- const introVideoProgressState = await getProgressState(introVideo?.id)
19
+ if (!introVideo) return null
18
20
 
19
- const activeLearningPath = await getActivePath(brand)
21
+ let introVideoProgressState
22
+ try {
23
+ introVideoProgressState = await getProgressState(introVideo?.id)
24
+ } catch (error) {
25
+ console.error('Error fetching progress state for method intro video:', error)
26
+ return null
27
+ }
28
+
29
+ let activeLearningPath
30
+ try {
31
+ activeLearningPath = await getActivePath(brand)
32
+ } catch (error) {
33
+ console.error('Error fetching active learning path:', error)
34
+ return null
35
+ }
20
36
 
21
37
  if (introVideoProgressState !== STATE.COMPLETED || !activeLearningPath) {
22
38
  const timestamp = Math.floor(Date.now())
@@ -41,76 +57,21 @@ export async function getMethodCard(brand) {
41
57
  progressTimestamp: timestamp,
42
58
  }
43
59
  } else {
44
- const learningPath = await fetchLearningPathLessons(
45
- activeLearningPath.active_learning_path_id,
46
- brand,
47
- new Date()
48
- )
49
-
50
- if (!learningPath) {
60
+ let learningPath
61
+ try {
62
+ learningPath = await fetchLearningPathLessons(
63
+ activeLearningPath.active_learning_path_id,
64
+ brand,
65
+ new Date()
66
+ )
67
+ } catch (e) {
68
+ console.error('Failed to fetch learning path lessons', e)
51
69
  return null
52
70
  }
53
71
 
54
- // need to calculate based on all dailies
55
- const allDailies = [
56
- ...learningPath.previous_learning_path_dailies,
57
- ...learningPath.learning_path_dailies,
58
- ...learningPath.next_learning_path_dailies
59
- ]
60
-
61
- let allDailiesCompleted = true;
62
- let anyDailiesStarted = false;
63
- let noDailiesStarted = true;
64
- let nextIncompleteDaily = null;
65
-
66
- for (const lesson of allDailies) {
67
- switch (lesson.progressStatus) {
68
- case STATE.COMPLETED:
69
- anyDailiesStarted = true;
70
- noDailiesStarted = false;
71
- break;
72
- case STATE.STARTED:
73
- anyDailiesStarted = true;
74
- noDailiesStarted = false;
75
- allDailiesCompleted = false;
76
- if (!nextIncompleteDaily) {
77
- nextIncompleteDaily = lesson;
78
- }
79
- break;
80
- default:
81
- allDailiesCompleted = false;
82
- if (!nextIncompleteDaily) {
83
- nextIncompleteDaily = lesson;
84
- }
85
- break;
86
- }
87
- if (!allDailiesCompleted && anyDailiesStarted && nextIncompleteDaily) {
88
- break;
89
- }
90
- }
72
+ if (!learningPath) return null
91
73
 
92
- // get the first incomplete lesson from upcoming and next learning path lessons
93
- const nextLesson = [
94
- ...learningPath?.upcoming_lessons,
95
- ...learningPath?.next_learning_path_dailies,
96
- ]?.find((lesson) => lesson.progressStatus !== STATE.COMPLETED)
97
-
98
- let ctaText, action
99
- if (noDailiesStarted) {
100
- ctaText = 'Start Session'
101
- action = getMethodActionCTA(nextIncompleteDaily)
102
- } else if (anyDailiesStarted && !allDailiesCompleted) {
103
- ctaText = 'Continue Session'
104
- action = getMethodActionCTA(nextIncompleteDaily)
105
- } else if (allDailiesCompleted) {
106
- ctaText = nextLesson ? 'Start Next Lesson' : 'Browse Lessons'
107
- action = nextLesson
108
- ? getMethodActionCTA(nextLesson)
109
- : {
110
- type: 'method-complete',
111
- brand,
112
- }
113
- }
74
+ const { ctaText, action } = getCtaAndText(learningPath)
114
75
 
115
76
  let maxProgressTimestamp = Math.max(
116
77
  ...learningPath?.children.map((lesson) => lesson.progressTimestamp)
@@ -139,10 +100,92 @@ export async function getMethodCard(brand) {
139
100
 
140
101
  function getMethodActionCTA(item) {
141
102
  return {
142
- type: item.type,
143
- brand: item.brand,
144
- id: item.id,
145
- slug: item.slug,
146
- parent_id: item.parent_id,
103
+ type: item.type ?? null,
104
+ brand: item.brand ?? null,
105
+ id: item.id ?? null,
106
+ slug: item.slug ?? null,
107
+ parent_id: item.parent_id ?? null,
147
108
  }
148
109
  }
110
+
111
+ function getCtaAndText(learningPath) {
112
+ const {
113
+ allDailiesCompleted,
114
+ anyDailiesStarted,
115
+ noDailiesStarted,
116
+ nextIncompleteDaily
117
+ } = analyzeDailySession(learningPath)
118
+
119
+ // get the first incomplete lesson from upcoming and next learning path lessons
120
+ const nextLesson = [
121
+ ...learningPath?.upcoming_lessons,
122
+ ...learningPath?.next_learning_path_dailies,
123
+ ]?.find((lesson) => lesson.progressStatus !== STATE.COMPLETED)
124
+
125
+ let ctaText, action
126
+ if (noDailiesStarted) {
127
+ ctaText = 'Start Session'
128
+ action = getMethodActionCTA(nextIncompleteDaily)
129
+ } else if (anyDailiesStarted && !allDailiesCompleted) {
130
+ ctaText = 'Continue Session'
131
+ action = getMethodActionCTA(nextIncompleteDaily)
132
+ } else if (allDailiesCompleted) {
133
+ ctaText = nextLesson ? 'Start Next Lesson' : 'Browse Lessons'
134
+ action = nextLesson
135
+ ? getMethodActionCTA(nextLesson)
136
+ : {
137
+ type: 'method-complete',
138
+ brand: learningPath.brand,
139
+ }
140
+ }
141
+
142
+ return { action, ctaText }
143
+ }
144
+
145
+
146
+ function analyzeDailySession(learningPath) {
147
+ const allDailies = [
148
+ ...learningPath.previous_learning_path_dailies,
149
+ ...learningPath.learning_path_dailies,
150
+ ...learningPath.next_learning_path_dailies
151
+ ]
152
+
153
+ let allDailiesCompleted = true;
154
+ let anyDailiesStarted = false;
155
+ let noDailiesStarted = true;
156
+ let nextIncompleteDaily = null;
157
+
158
+ for (const lesson of allDailies) {
159
+ switch (lesson.progressStatus) {
160
+ case STATE.COMPLETED:
161
+ anyDailiesStarted = true;
162
+ noDailiesStarted = false;
163
+ break;
164
+ case STATE.STARTED:
165
+ anyDailiesStarted = true;
166
+ noDailiesStarted = false;
167
+ allDailiesCompleted = false;
168
+ if (!nextIncompleteDaily) {
169
+ nextIncompleteDaily = lesson;
170
+ }
171
+ break;
172
+ default:
173
+ allDailiesCompleted = false;
174
+ if (!nextIncompleteDaily) {
175
+ nextIncompleteDaily = lesson;
176
+ }
177
+ break;
178
+ }
179
+ if (!allDailiesCompleted && anyDailiesStarted && nextIncompleteDaily) {
180
+ break;
181
+ }
182
+ }
183
+
184
+ return {
185
+ allDailiesCompleted,
186
+ anyDailiesStarted,
187
+ noDailiesStarted,
188
+ nextIncompleteDaily
189
+ }
190
+ }
191
+
@@ -34,6 +34,8 @@ import {
34
34
  liveFields,
35
35
  postProcessBadge,
36
36
  contentAwardField,
37
+ parentField,
38
+ grandParentField,
37
39
  } from '../contentTypeConfig.js'
38
40
  import { fetchSimilarItems } from './recommendations.js'
39
41
  import { getSongType, processMetadata, ALWAYS_VISIBLE_TABS, CONTENT_STATUSES } from '../contentMetaData.js'
@@ -1117,8 +1119,12 @@ export async function fetchSiblingContent(railContentId, brand = null) {
1117
1119
  const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, artist->, "permission_id": permission_v2, "genre": genre[]->name, "parent_id": parent_content_data[0].id`
1118
1120
 
1119
1121
  const query = `*[railcontent_id == ${railContentId}${brandString}]{
1120
- _type, parent_type, 'parent_id': parent_content_data[0].id, railcontent_id,
1121
- 'for-calculations': *[${filterGetParent}][0]{
1122
+ _type,
1123
+ parent_type,
1124
+ railcontent_id,
1125
+ 'parent_id': ${parentField}.id,
1126
+ 'grandparent_id':${grandParentField}.id,
1127
+ 'for-calculations': *[${filterGetParent}][0]{
1122
1128
  'siblings-list': child[]->railcontent_id,
1123
1129
  'parents-list': *[${filterForParentList}][0].child[]->railcontent_id
1124
1130
  },
@@ -1136,6 +1142,11 @@ export async function fetchSiblingContent(railContentId, brand = null) {
1136
1142
  const currentSiblingIndex = calc['siblings-list'].indexOf(result['railcontent_id']) + 1
1137
1143
 
1138
1144
  delete result['for-calculations']
1145
+
1146
+ if (result['grandparent_id']) {
1147
+ result['collection_data'] = await fetchCourseCollectionData(result['grandparent_id'])
1148
+ }
1149
+
1139
1150
  result = { ...result, parentCount, currentParentIndex, siblingCount, currentSiblingIndex }
1140
1151
  return result
1141
1152
  } else {
@@ -29,10 +29,8 @@ export default class SyncManager {
29
29
  const teardown = instance.setup()
30
30
 
31
31
  return async (mode: SyncTeardownMode = 'reset') => {
32
- SyncManager.instance = null
33
- return teardown(mode).catch((error) => {
34
- SyncManager.instance = instance // restore instance on teardown failure
35
- throw error
32
+ return teardown(mode).then(() => {
33
+ SyncManager.instance = null
36
34
  })
37
35
  }
38
36
  }
@@ -76,6 +76,14 @@ export interface RestorePurchasesSetupAccountResponse {
76
76
  originalAppUserId: string
77
77
  }
78
78
 
79
+ /**
80
+ * Represents response for latest subscription platform as best we can determine.
81
+ */
82
+ export interface SubscriptionPlatform {
83
+ last_platform: 'ios' | 'android' | 'web' | null
84
+ has_active_platform_subscription: boolean
85
+ }
86
+
79
87
  /**
80
88
  * Represents all possible responses from RevenueCat purchase restoration
81
89
  */
@@ -230,7 +238,7 @@ export async function restorePurchases(
230
238
  *
231
239
  * @returns {Promise<{price: number, currency: string, period: string|null}>} - The upgrade price information
232
240
  * @property {number} price - The upgrade cost in USD (monthly for month/year, annual for lifetime)
233
- * @property {string} currency - The currency
241
+ * @property {string} currency - The currency
234
242
  * @property {string|null} period - The billing period for the price ('month' or 'year'). Note: lifetime subscribers return 'year' period with annual price
235
243
  *
236
244
  * @example
@@ -250,3 +258,21 @@ export async function getUpgradePrice() {
250
258
  const httpClient = new HttpClient(globalConfig.baseUrl)
251
259
  return httpClient.get(`${baseUrl}/v1/upgrade-price`)
252
260
  }
261
+
262
+ /**
263
+ * @returns {Promise<'ios' | 'android' | 'web' | null>} The platform of the user's last known subscription
264
+ */
265
+ export async function fetchLastSubscriptionPlatform(): Promise<'ios' | 'android' | 'web' | null> {
266
+ const httpClient = new HttpClient(globalConfig.baseUrl)
267
+ const response = await httpClient.get<SubscriptionPlatform>(`${baseUrl}/v1/subscription-platform`)
268
+ return response.last_platform
269
+ }
270
+
271
+ /**
272
+ * @returns {Promise<boolean>} Whether the user has any subscription from a known platform (web, ios, android)
273
+ */
274
+ export async function fetchHasActivePlatformSubscription(): Promise<boolean> {
275
+ const httpClient = new HttpClient(globalConfig.baseUrl)
276
+ const response = await httpClient.get<SubscriptionPlatform>(`${baseUrl}/v1/subscription-platform`)
277
+ return response.has_active_platform_subscription
278
+ }
@@ -1,39 +0,0 @@
1
- # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2
- # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3
-
4
- name: Node.js CI
5
-
6
- on:
7
- push:
8
- branches: [ ]
9
- pull_request:
10
- branches: [ ]
11
-
12
- jobs:
13
- build:
14
-
15
- runs-on: ubuntu-latest
16
-
17
- strategy:
18
- matrix:
19
- node-version: [20.x]
20
- # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21
-
22
- steps:
23
- - uses: actions/checkout@v4
24
- - name: Use Node.js ${{ matrix.node-version }}
25
- uses: actions/setup-node@v4
26
- with:
27
- node-version: ${{ matrix.node-version }}
28
- cache: 'npm'
29
- - run: npm ci
30
- - run: npm run build --if-present
31
- - name: 'Create env file'
32
- run: |
33
- touch .env
34
- echo SANITY_API_TOKEN=${{ secrets.SANITY_API_TOKEN }} >> .env
35
- echo SANITY_PROJECT_ID=4032r8py >> .env
36
- echo SANITY_DATASET=staging >> .env
37
- echo SANITY_USE_CACHED_API=false >> .env
38
- cat .env
39
- - run: npm test