musora-content-services 2.96.2 → 2.98.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/.claude/settings.local.json +8 -3
- package/.coderabbit.yaml +0 -0
- package/.editorconfig +0 -0
- package/.github/pull_request_template.md +0 -0
- package/.github/workflows/conventional-commits.yaml +0 -0
- package/.github/workflows/docs.js.yml +0 -0
- package/.github/workflows/node.js.yml +0 -0
- package/.prettierignore +0 -0
- package/.prettierrc +0 -0
- package/.yarnrc.yml +1 -0
- package/CHANGELOG.md +31 -0
- package/README.md +0 -0
- package/jest.config.js +0 -0
- package/package.json +1 -1
- package/src/contentMetaData.js +0 -0
- package/src/index.d.ts +10 -0
- package/src/index.js +10 -0
- package/src/infrastructure/http/HttpClient.ts +0 -0
- package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
- package/src/infrastructure/http/index.ts +0 -0
- package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
- package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
- package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
- package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
- package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
- package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
- package/src/lib/brands.ts +0 -0
- package/src/lib/httpHelper.js +0 -0
- package/src/lib/lastUpdated.js +0 -0
- package/src/services/api/types.js +0 -0
- package/src/services/api/types.ts +0 -0
- package/src/services/content/content.ts +0 -0
- package/src/services/content-org/content-org.js +0 -0
- package/src/services/content-org/guided-courses.ts +0 -0
- package/src/services/content-org/learning-paths.ts +15 -2
- package/src/services/content-org/playlists-types.js +0 -0
- package/src/services/content-org/playlists.js +0 -0
- package/src/services/content.js +0 -0
- package/src/services/contentAggregator.js +161 -2
- package/src/services/contentLikes.js +0 -0
- package/src/services/contentProgress.js +122 -4
- package/src/services/dataContext.js +0 -0
- package/src/services/dateUtils.js +0 -0
- package/src/services/eventsAPI.js +0 -0
- package/src/services/forums/categories.ts +0 -0
- package/src/services/forums/forums.ts +0 -0
- package/src/services/forums/posts.ts +0 -0
- package/src/services/forums/threads.ts +0 -0
- package/src/services/forums/types.ts +0 -0
- package/src/services/gamification/gamification.js +0 -0
- package/src/services/imageSRCBuilder.js +0 -0
- package/src/services/imageSRCVerify.js +0 -0
- package/src/services/liveTesting.ts +0 -0
- package/src/services/permissions/PermissionsAdapter.ts +0 -0
- package/src/services/permissions/PermissionsAdapterFactory.ts +0 -0
- package/src/services/permissions/PermissionsV1Adapter.ts +0 -0
- package/src/services/permissions/PermissionsV2Adapter.ts +0 -0
- package/src/services/permissions/README.md +0 -0
- package/src/services/permissions/index.ts +0 -0
- package/src/services/railcontent.js +0 -0
- package/src/services/recommendations.js +0 -0
- package/src/services/reporting/README.md +0 -0
- package/src/services/reporting/reporting.ts +0 -0
- package/src/services/reporting/types.ts +0 -0
- package/src/services/sanity.js +6 -0
- package/src/services/sentry/.indexignore +0 -0
- package/src/services/sentry/index.ts +0 -0
- package/src/services/sync/.indexignore +0 -0
- package/src/services/sync/adapters/factory.ts +0 -0
- package/src/services/sync/adapters/lokijs.ts +0 -0
- package/src/services/sync/adapters/sqlite.ts +0 -0
- package/src/services/sync/concurrency-safety.ts +0 -0
- package/src/services/sync/context/index.ts +0 -0
- package/src/services/sync/context/providers/base.ts +0 -0
- package/src/services/sync/context/providers/connectivity.ts +0 -0
- package/src/services/sync/context/providers/durability.ts +0 -0
- package/src/services/sync/context/providers/index.ts +0 -0
- package/src/services/sync/context/providers/session.ts +0 -0
- package/src/services/sync/context/providers/tabs.ts +0 -0
- package/src/services/sync/context/providers/visibility.ts +0 -0
- package/src/services/sync/database/factory.ts +0 -0
- package/src/services/sync/errors/boundary.ts +0 -0
- package/src/services/sync/errors/index.ts +0 -0
- package/src/services/sync/index.ts +0 -0
- package/src/services/sync/models/Base.ts +0 -0
- package/src/services/sync/models/ContentLike.ts +0 -0
- package/src/services/sync/models/Practice.ts +0 -0
- package/src/services/sync/models/PracticeDayNote.ts +0 -0
- package/src/services/sync/repositories/base.ts +0 -0
- package/src/services/sync/repositories/content-likes.ts +0 -0
- package/src/services/sync/repositories/content-progress.ts +35 -10
- package/src/services/sync/repositories/practice-day-notes.ts +0 -0
- package/src/services/sync/resolver.ts +0 -0
- package/src/services/sync/run-scope.ts +0 -0
- package/src/services/sync/serializers/index.ts +0 -0
- package/src/services/sync/serializers/model.ts +0 -0
- package/src/services/sync/serializers/raw.ts +0 -0
- package/src/services/sync/strategies/base.ts +0 -0
- package/src/services/sync/strategies/index.ts +0 -0
- package/src/services/sync/strategies/initial.ts +0 -0
- package/src/services/sync/strategies/polling.ts +0 -0
- package/src/services/sync/telemetry/index.ts +0 -0
- package/src/services/sync/telemetry/sampling.ts +0 -0
- package/src/services/sync/utils/event-emitter.ts +0 -0
- package/src/services/sync/utils/index.ts +0 -0
- package/src/services/sync/utils/throttle.ts +0 -0
- package/src/services/sync/utils/timers.ts +0 -0
- package/src/services/types.js +0 -0
- package/src/services/user/account.ts +0 -0
- package/src/services/user/chat.js +0 -0
- package/src/services/user/interests.js +0 -0
- package/src/services/user/management.js +0 -0
- package/src/services/user/memberships.ts +32 -0
- package/src/services/user/notifications.js +0 -0
- package/src/services/user/payments.ts +0 -0
- package/src/services/user/permissions.js +0 -0
- package/src/services/user/profile.js +0 -0
- package/src/services/user/sessions.js +0 -0
- package/src/services/user/types.d.ts +0 -0
- package/src/services/user/types.js +0 -0
- package/src/services/user/user-management-system.js +0 -0
- package/test/content.test.js +0 -0
- package/test/contentLikes.test.js +0 -0
- package/test/contentProgress.test.js +0 -0
- package/test/dataContext.test.js +0 -0
- package/test/forum.test.js +0 -0
- package/test/imageSRCBuilder.test.js +0 -0
- package/test/imageSRCVerify.test.js +0 -0
- package/test/lib/lastUpdated.test.js +0 -0
- package/test/live/contentProgressLive.test.js +0 -0
- package/test/live/railcontentLive.test.js +0 -0
- package/test/localStorageMock.js +0 -0
- package/test/log.js +0 -0
- package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
- package/test/mockData/mockData_progress_content.json +0 -0
- package/test/mockData/mockData_sanity_progress_content.json +0 -0
- package/test/mockData/mockData_user_practices.json +0 -0
- package/test/notifications.test.js +0 -0
- package/test/progressRows.test.js +0 -0
- package/test/reporting.test.js +132 -0
- package/test/streakMessage.test.js +0 -0
- package/test/user/permissions.test.js +0 -0
- package/test/userActivity.test.js +0 -0
|
@@ -4,13 +4,15 @@ import { COLLECTION_TYPE, STATE } from './sync/models/ContentProgress'
|
|
|
4
4
|
import { trackUserPractice, findIncompleteLesson } from './userActivity'
|
|
5
5
|
import { getNextLessonLessonParentTypes } from '../contentTypeConfig.js'
|
|
6
6
|
import { emitContentCompleted } from './progress-events'
|
|
7
|
+
import {getDailySession} from "./content-org/learning-paths.js";
|
|
8
|
+
import {getToday} from "./dateUtils.js";
|
|
7
9
|
|
|
8
10
|
const STATE_STARTED = STATE.STARTED
|
|
9
11
|
const STATE_COMPLETED = STATE.COMPLETED
|
|
10
12
|
const MAX_DEPTH = 3
|
|
11
13
|
|
|
12
|
-
export async function getProgressState(contentId) {
|
|
13
|
-
return getById(contentId, 'state', '')
|
|
14
|
+
export async function getProgressState(contentId, collection = null) {
|
|
15
|
+
return getById(contentId, collection, 'state', '')
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export async function getProgressStateByIds(contentIds, collection = null) {
|
|
@@ -26,6 +28,68 @@ export async function getResumeTimeSecondsByIds(contentIds, collection = null) {
|
|
|
26
28
|
)
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
export async function getResumeTimeSecondsByIdsAndCollections(tuples) {
|
|
32
|
+
return getByIdsAndCollections(tuples, 'resume_time_seconds', 0)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function getNavigateToForMethod(data) {
|
|
36
|
+
let navigateToData = {}
|
|
37
|
+
|
|
38
|
+
const brand = data[0].content.brand || null
|
|
39
|
+
const dailySessionResponse = await getDailySession(brand, getToday())
|
|
40
|
+
const dailySession = dailySessionResponse?.daily_session || null
|
|
41
|
+
const activeLearningPathId = dailySessionResponse?.active_learning_path_id || null
|
|
42
|
+
|
|
43
|
+
for (const tuple of data) {
|
|
44
|
+
if (!tuple) continue
|
|
45
|
+
|
|
46
|
+
const {content, collection} = tuple
|
|
47
|
+
|
|
48
|
+
const findFirstIncomplete = (progresses) =>
|
|
49
|
+
Object.keys(progresses).find(id => progresses[id] !== STATE_COMPLETED) || null
|
|
50
|
+
|
|
51
|
+
const findChildById = (children, id) =>
|
|
52
|
+
children?.find(child => child.id === Number(id)) || null
|
|
53
|
+
|
|
54
|
+
const getFirstOrIncompleteChild = async (content, collection) => {
|
|
55
|
+
const childrenIds = content?.children.map(child => child.id) || []
|
|
56
|
+
if (childrenIds.length === 0) return null
|
|
57
|
+
|
|
58
|
+
const progresses = await getProgressStateByIds(childrenIds, collection)
|
|
59
|
+
const incompleteId = findFirstIncomplete(progresses)
|
|
60
|
+
|
|
61
|
+
return incompleteId ? findChildById(content.children, incompleteId) : content.children[0]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const getDailySessionNavigateTo = async (content, dailySession, collection) => {
|
|
65
|
+
const dailiesIds = dailySession?.map(item => item.content_ids).flat() || []
|
|
66
|
+
const progresses = await getProgressStateByIds(dailiesIds, collection)
|
|
67
|
+
const incompleteId = findFirstIncomplete(progresses)
|
|
68
|
+
|
|
69
|
+
return incompleteId ? findChildById(content.children, incompleteId) : null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// does not support passing in 'method-v2' type yet
|
|
73
|
+
if (content.type === COLLECTION_TYPE.LEARNING_PATH) {
|
|
74
|
+
let navigateTo = null
|
|
75
|
+
|
|
76
|
+
if (content.id === activeLearningPathId) {
|
|
77
|
+
navigateTo = await getDailySessionNavigateTo(content, dailySession, collection)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!navigateTo) {
|
|
81
|
+
navigateTo = await getFirstOrIncompleteChild(content, collection)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
navigateToData[content.id] =buildNavigateTo(navigateTo, null, collection)
|
|
85
|
+
|
|
86
|
+
} else {
|
|
87
|
+
navigateToData[content.id] = null
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return navigateToData
|
|
91
|
+
}
|
|
92
|
+
|
|
29
93
|
export async function getNavigateTo(data, collection = null) {
|
|
30
94
|
collection = normalizeCollection(collection)
|
|
31
95
|
let navigateToData = {}
|
|
@@ -178,10 +242,52 @@ export async function getProgressDataByIds(contentIds, collection) {
|
|
|
178
242
|
return progress
|
|
179
243
|
}
|
|
180
244
|
|
|
181
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Get progress data for multiple content IDs, each with their own collection context.
|
|
247
|
+
* Useful when fetching progress for tuples that belong to different collections.
|
|
248
|
+
*
|
|
249
|
+
* @param {Array<{contentId: number, collection: {type: string, id: number}|null}>} tuples - Array of objects with contentId and collection
|
|
250
|
+
* @returns {Promise<Object>} - Object mapping content IDs to progress data
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* const tuples = [
|
|
254
|
+
* { contentId: 123, collection: { id: 456, type: 'learning-path-v2' } },
|
|
255
|
+
* { contentId: 789, collection: { id: 101, type: 'learning-path-v2' } },
|
|
256
|
+
* { contentId: 111, collection: null }
|
|
257
|
+
* ]
|
|
258
|
+
* const progress = await getProgressDataByIdsAndCollections(tuples)
|
|
259
|
+
* // Returns: { 123: { progress: 50, status: 'started', last_update: 123456 }, ... }
|
|
260
|
+
*/
|
|
261
|
+
|
|
262
|
+
// todo: warning: this doesnt work with having 2 items with same contentId but different collection, because
|
|
263
|
+
// of the response structure here with contentId as key
|
|
264
|
+
export async function getProgressDataByIdsAndCollections(tuples) {
|
|
265
|
+
tuples = tuples.map(t => ({contentId: normalizeContentId(t.contentId), collection: normalizeCollection(t.collection)}))
|
|
266
|
+
const progress = Object.fromEntries(tuples.map(item => [item.contentId, {
|
|
267
|
+
last_update: 0,
|
|
268
|
+
progress: 0,
|
|
269
|
+
status: '',
|
|
270
|
+
collection: {},
|
|
271
|
+
}]))
|
|
272
|
+
|
|
273
|
+
await db.contentProgress.getSomeProgressByContentIdsAndCollection(tuples).then(r => {
|
|
274
|
+
r.data.forEach(p => {
|
|
275
|
+
progress[p.content_id] = {
|
|
276
|
+
last_update: p.updated_at,
|
|
277
|
+
progress: p.progress_percent,
|
|
278
|
+
status: p.state,
|
|
279
|
+
collection: (p.collection_type && p.collection_id) ? {type: p.collection_type, id: p.collection_id} : null
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
return progress
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function getById(contentId, collection, dataKey, defaultValue) {
|
|
182
288
|
if (!contentId) return defaultValue
|
|
183
289
|
return db.contentProgress
|
|
184
|
-
.getOneProgressByContentId(contentId)
|
|
290
|
+
.getOneProgressByContentId(contentId, collection)
|
|
185
291
|
.then((r) => r.data?.[dataKey] ?? defaultValue)
|
|
186
292
|
}
|
|
187
293
|
|
|
@@ -197,6 +303,18 @@ async function getByIds(contentIds, collection, dataKey, defaultValue) {
|
|
|
197
303
|
return progress
|
|
198
304
|
}
|
|
199
305
|
|
|
306
|
+
async function getByIdsAndCollections(tuples, dataKey, defaultValue) {
|
|
307
|
+
tuples = tuples.map(t => ({contentId: normalizeContentId(t.contentId), collection: normalizeCollection(t.collection)}))
|
|
308
|
+
const progress = Object.fromEntries(tuples.map(tuple => [tuple.contentId, defaultValue]))
|
|
309
|
+
|
|
310
|
+
await db.contentProgress.getSomeProgressByContentIdsAndCollection(tuples).then(r => {
|
|
311
|
+
r.data.forEach(p => {
|
|
312
|
+
progress[p.content_id] = p[dataKey] ?? defaultValue
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
return progress
|
|
316
|
+
}
|
|
317
|
+
|
|
200
318
|
export async function getAllStarted(limit = null) {
|
|
201
319
|
return db.contentProgress.startedIds(limit).then((r) => r.data.map((id) => parseInt(id)))
|
|
202
320
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/services/sanity.js
CHANGED
|
@@ -2000,8 +2000,11 @@ export async function fetchMethodV2Structure(brand) {
|
|
|
2000
2000
|
const _type = 'method-v2'
|
|
2001
2001
|
const query = `*[_type == '${_type}' && brand == '${brand}'][0...1]{
|
|
2002
2002
|
'sanity_id': _id,
|
|
2003
|
+
brand,
|
|
2004
|
+
'intro_video_id': intro_video->railcontent_id,
|
|
2003
2005
|
'learning_paths': child[]->{
|
|
2004
2006
|
'id': railcontent_id,
|
|
2007
|
+
'intro_video_id': intro_video->railcontent_id,
|
|
2005
2008
|
'children': child[]->railcontent_id
|
|
2006
2009
|
}
|
|
2007
2010
|
}`
|
|
@@ -2017,8 +2020,11 @@ export async function fetchMethodV2StructureFromId(contentId) {
|
|
|
2017
2020
|
const _type = "method-v2";
|
|
2018
2021
|
const query = `*[_type == '${_type}' && brand == *[railcontent_id == ${contentId}][0].brand][0...1]{
|
|
2019
2022
|
'sanity_id': _id,
|
|
2023
|
+
brand,
|
|
2024
|
+
'intro_video_id': intro_video->railcontent_id,
|
|
2020
2025
|
'learning_paths': child[]->{
|
|
2021
2026
|
'id': railcontent_id,
|
|
2027
|
+
'intro_video_id': intro_video->railcontent_id,
|
|
2022
2028
|
'children': child[]->railcontent_id
|
|
2023
2029
|
}
|
|
2024
2030
|
}`
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import SyncRepository, {
|
|
2
|
-
import ContentProgress, { COLLECTION_TYPE,
|
|
1
|
+
import SyncRepository, {Q} from './base'
|
|
2
|
+
import ContentProgress, {COLLECTION_ID_SELF, COLLECTION_TYPE, STATE} from '../models/ContentProgress'
|
|
3
3
|
|
|
4
|
+
interface ContentIdCollectionTuple {
|
|
5
|
+
contentId: number,
|
|
6
|
+
collection: CollectionParameter | null,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface CollectionParameter {
|
|
10
|
+
type: COLLECTION_TYPE,
|
|
11
|
+
id: number,
|
|
12
|
+
}
|
|
4
13
|
export default class ProgressRepository extends SyncRepository<ContentProgress> {
|
|
5
14
|
// null collection only
|
|
6
15
|
async startedIds(limit?: number) {
|
|
@@ -77,7 +86,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
77
86
|
return clauses
|
|
78
87
|
}
|
|
79
88
|
|
|
80
|
-
async mostRecentlyUpdatedId(contentIds: number[], collection:
|
|
89
|
+
async mostRecentlyUpdatedId(contentIds: number[], collection: CollectionParameter | null = null) {
|
|
81
90
|
return this.queryOneId(
|
|
82
91
|
Q.where('content_id', Q.oneOf(contentIds)),
|
|
83
92
|
Q.where('collection_type', collection?.type ?? COLLECTION_TYPE.SELF),
|
|
@@ -89,7 +98,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
89
98
|
|
|
90
99
|
async getOneProgressByContentId(
|
|
91
100
|
contentId: number,
|
|
92
|
-
|
|
101
|
+
collection: CollectionParameter | null = null
|
|
93
102
|
) {
|
|
94
103
|
const clauses = [
|
|
95
104
|
Q.where('content_id', contentId),
|
|
@@ -102,7 +111,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
102
111
|
|
|
103
112
|
async getSomeProgressByContentIds(
|
|
104
113
|
contentIds: number[],
|
|
105
|
-
collection:
|
|
114
|
+
collection: CollectionParameter | null = null
|
|
106
115
|
) {
|
|
107
116
|
const clauses = [
|
|
108
117
|
Q.where('content_id', Q.oneOf(contentIds)),
|
|
@@ -113,7 +122,23 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
113
122
|
return await this.queryAll(...clauses)
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
|
|
125
|
+
async getSomeProgressByContentIdsAndCollection(tuples: ContentIdCollectionTuple[]) {
|
|
126
|
+
const clauses = []
|
|
127
|
+
|
|
128
|
+
clauses.push(...tuples.map(tuple => Q.and(...tupleClauses(tuple))))
|
|
129
|
+
|
|
130
|
+
return await this.queryAll(Q.or(...clauses))
|
|
131
|
+
|
|
132
|
+
function tupleClauses(tuple: ContentIdCollectionTuple) {
|
|
133
|
+
return [
|
|
134
|
+
Q.where('content_id', tuple.contentId),
|
|
135
|
+
Q.where('collection_type', tuple.collection?.type ?? COLLECTION_TYPE.SELF),
|
|
136
|
+
Q.where('collection_id', tuple.collection?.id ?? COLLECTION_ID_SELF)
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
recordProgress(contentId: number, collection: CollectionParameter | null, progressPct: number, resumeTime?: number) {
|
|
117
142
|
const id = ProgressRepository.generateId(contentId, collection)
|
|
118
143
|
|
|
119
144
|
const result = this.upsertOne(id, (r) => {
|
|
@@ -156,7 +181,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
156
181
|
|
|
157
182
|
recordProgresses(
|
|
158
183
|
contentIds: number[],
|
|
159
|
-
collection:
|
|
184
|
+
collection: CollectionParameter | null,
|
|
160
185
|
progressPct: number
|
|
161
186
|
) {
|
|
162
187
|
return this.upsertSome(
|
|
@@ -178,7 +203,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
178
203
|
|
|
179
204
|
recordProgressesTentative(
|
|
180
205
|
contentProgresses: Record<string, number>, // Accept plain object
|
|
181
|
-
collection:
|
|
206
|
+
collection: CollectionParameter | null
|
|
182
207
|
) {
|
|
183
208
|
const data = Object.fromEntries(
|
|
184
209
|
Object.entries(contentProgresses).map(([contentId, progressPct]) => [
|
|
@@ -196,13 +221,13 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
196
221
|
return this.upsertSomeTentative(data)
|
|
197
222
|
}
|
|
198
223
|
|
|
199
|
-
eraseProgress(contentId: number, collection:
|
|
224
|
+
eraseProgress(contentId: number, collection: CollectionParameter | null) {
|
|
200
225
|
return this.deleteOne(ProgressRepository.generateId(contentId, collection))
|
|
201
226
|
}
|
|
202
227
|
|
|
203
228
|
private static generateId(
|
|
204
229
|
contentId: number,
|
|
205
|
-
collection:
|
|
230
|
+
collection: CollectionParameter | null
|
|
206
231
|
) {
|
|
207
232
|
return `${contentId}:${collection?.type || COLLECTION_TYPE.SELF}:${collection?.id || COLLECTION_ID_SELF}`
|
|
208
233
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/services/types.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -218,3 +218,35 @@ export async function restorePurchases(
|
|
|
218
218
|
requestBody
|
|
219
219
|
)
|
|
220
220
|
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get the upgrade price from Basic to Plus membership.
|
|
224
|
+
* Returns the price based on the user's subscription interval.
|
|
225
|
+
*
|
|
226
|
+
* For monthly subscribers: Returns the monthly upgrade cost (difference between Plus and Base monthly prices, ~$5/month)
|
|
227
|
+
* For yearly subscribers: Returns the monthly equivalent upgrade cost ($3.33/month from $40/year)
|
|
228
|
+
* For lifetime subscribers: Returns the annual upgrade cost for songs add-on ($40/year)
|
|
229
|
+
* If interval cannot be determined: Defaults to monthly price
|
|
230
|
+
*
|
|
231
|
+
* @returns {Promise<{price: number, currency: string, period: string|null}>} - The upgrade price information
|
|
232
|
+
* @property {number} price - The upgrade cost in USD (monthly for month/year, annual for lifetime)
|
|
233
|
+
* @property {string} currency - The currency
|
|
234
|
+
* @property {string|null} period - The billing period for the price ('month' or 'year'). Note: lifetime subscribers return 'year' period with annual price
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* getUpgradePrice()
|
|
238
|
+
* .then(info => {
|
|
239
|
+
* console.log(`Upgrade price: $${info.price} per ${info.period}`)
|
|
240
|
+
* // Example outputs:
|
|
241
|
+
* // Monthly: "Upgrade price: $5 per month"
|
|
242
|
+
* // Yearly: "Upgrade price: $3.33 per month"
|
|
243
|
+
* // Lifetime: "Upgrade price: $40 per year"
|
|
244
|
+
* })
|
|
245
|
+
* .catch(error => {
|
|
246
|
+
* console.error('Failed to fetch upgrade price:', error)
|
|
247
|
+
* })
|
|
248
|
+
*/
|
|
249
|
+
export async function getUpgradePrice() {
|
|
250
|
+
const httpClient = new HttpClient(globalConfig.baseUrl)
|
|
251
|
+
return httpClient.get(`${baseUrl}/v1/upgrade-price`)
|
|
252
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/test/content.test.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/test/dataContext.test.js
CHANGED
|
File without changes
|
package/test/forum.test.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/test/localStorageMock.js
CHANGED
|
File without changes
|
package/test/log.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|