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.
Files changed (143) hide show
  1. package/.claude/settings.local.json +8 -3
  2. package/.coderabbit.yaml +0 -0
  3. package/.editorconfig +0 -0
  4. package/.github/pull_request_template.md +0 -0
  5. package/.github/workflows/conventional-commits.yaml +0 -0
  6. package/.github/workflows/docs.js.yml +0 -0
  7. package/.github/workflows/node.js.yml +0 -0
  8. package/.prettierignore +0 -0
  9. package/.prettierrc +0 -0
  10. package/.yarnrc.yml +1 -0
  11. package/CHANGELOG.md +31 -0
  12. package/README.md +0 -0
  13. package/jest.config.js +0 -0
  14. package/package.json +1 -1
  15. package/src/contentMetaData.js +0 -0
  16. package/src/index.d.ts +10 -0
  17. package/src/index.js +10 -0
  18. package/src/infrastructure/http/HttpClient.ts +0 -0
  19. package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
  20. package/src/infrastructure/http/index.ts +0 -0
  21. package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
  22. package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
  23. package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
  24. package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
  25. package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
  26. package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
  27. package/src/lib/brands.ts +0 -0
  28. package/src/lib/httpHelper.js +0 -0
  29. package/src/lib/lastUpdated.js +0 -0
  30. package/src/services/api/types.js +0 -0
  31. package/src/services/api/types.ts +0 -0
  32. package/src/services/content/content.ts +0 -0
  33. package/src/services/content-org/content-org.js +0 -0
  34. package/src/services/content-org/guided-courses.ts +0 -0
  35. package/src/services/content-org/learning-paths.ts +15 -2
  36. package/src/services/content-org/playlists-types.js +0 -0
  37. package/src/services/content-org/playlists.js +0 -0
  38. package/src/services/content.js +0 -0
  39. package/src/services/contentAggregator.js +161 -2
  40. package/src/services/contentLikes.js +0 -0
  41. package/src/services/contentProgress.js +122 -4
  42. package/src/services/dataContext.js +0 -0
  43. package/src/services/dateUtils.js +0 -0
  44. package/src/services/eventsAPI.js +0 -0
  45. package/src/services/forums/categories.ts +0 -0
  46. package/src/services/forums/forums.ts +0 -0
  47. package/src/services/forums/posts.ts +0 -0
  48. package/src/services/forums/threads.ts +0 -0
  49. package/src/services/forums/types.ts +0 -0
  50. package/src/services/gamification/gamification.js +0 -0
  51. package/src/services/imageSRCBuilder.js +0 -0
  52. package/src/services/imageSRCVerify.js +0 -0
  53. package/src/services/liveTesting.ts +0 -0
  54. package/src/services/permissions/PermissionsAdapter.ts +0 -0
  55. package/src/services/permissions/PermissionsAdapterFactory.ts +0 -0
  56. package/src/services/permissions/PermissionsV1Adapter.ts +0 -0
  57. package/src/services/permissions/PermissionsV2Adapter.ts +0 -0
  58. package/src/services/permissions/README.md +0 -0
  59. package/src/services/permissions/index.ts +0 -0
  60. package/src/services/railcontent.js +0 -0
  61. package/src/services/recommendations.js +0 -0
  62. package/src/services/reporting/README.md +0 -0
  63. package/src/services/reporting/reporting.ts +0 -0
  64. package/src/services/reporting/types.ts +0 -0
  65. package/src/services/sanity.js +6 -0
  66. package/src/services/sentry/.indexignore +0 -0
  67. package/src/services/sentry/index.ts +0 -0
  68. package/src/services/sync/.indexignore +0 -0
  69. package/src/services/sync/adapters/factory.ts +0 -0
  70. package/src/services/sync/adapters/lokijs.ts +0 -0
  71. package/src/services/sync/adapters/sqlite.ts +0 -0
  72. package/src/services/sync/concurrency-safety.ts +0 -0
  73. package/src/services/sync/context/index.ts +0 -0
  74. package/src/services/sync/context/providers/base.ts +0 -0
  75. package/src/services/sync/context/providers/connectivity.ts +0 -0
  76. package/src/services/sync/context/providers/durability.ts +0 -0
  77. package/src/services/sync/context/providers/index.ts +0 -0
  78. package/src/services/sync/context/providers/session.ts +0 -0
  79. package/src/services/sync/context/providers/tabs.ts +0 -0
  80. package/src/services/sync/context/providers/visibility.ts +0 -0
  81. package/src/services/sync/database/factory.ts +0 -0
  82. package/src/services/sync/errors/boundary.ts +0 -0
  83. package/src/services/sync/errors/index.ts +0 -0
  84. package/src/services/sync/index.ts +0 -0
  85. package/src/services/sync/models/Base.ts +0 -0
  86. package/src/services/sync/models/ContentLike.ts +0 -0
  87. package/src/services/sync/models/Practice.ts +0 -0
  88. package/src/services/sync/models/PracticeDayNote.ts +0 -0
  89. package/src/services/sync/repositories/base.ts +0 -0
  90. package/src/services/sync/repositories/content-likes.ts +0 -0
  91. package/src/services/sync/repositories/content-progress.ts +35 -10
  92. package/src/services/sync/repositories/practice-day-notes.ts +0 -0
  93. package/src/services/sync/resolver.ts +0 -0
  94. package/src/services/sync/run-scope.ts +0 -0
  95. package/src/services/sync/serializers/index.ts +0 -0
  96. package/src/services/sync/serializers/model.ts +0 -0
  97. package/src/services/sync/serializers/raw.ts +0 -0
  98. package/src/services/sync/strategies/base.ts +0 -0
  99. package/src/services/sync/strategies/index.ts +0 -0
  100. package/src/services/sync/strategies/initial.ts +0 -0
  101. package/src/services/sync/strategies/polling.ts +0 -0
  102. package/src/services/sync/telemetry/index.ts +0 -0
  103. package/src/services/sync/telemetry/sampling.ts +0 -0
  104. package/src/services/sync/utils/event-emitter.ts +0 -0
  105. package/src/services/sync/utils/index.ts +0 -0
  106. package/src/services/sync/utils/throttle.ts +0 -0
  107. package/src/services/sync/utils/timers.ts +0 -0
  108. package/src/services/types.js +0 -0
  109. package/src/services/user/account.ts +0 -0
  110. package/src/services/user/chat.js +0 -0
  111. package/src/services/user/interests.js +0 -0
  112. package/src/services/user/management.js +0 -0
  113. package/src/services/user/memberships.ts +32 -0
  114. package/src/services/user/notifications.js +0 -0
  115. package/src/services/user/payments.ts +0 -0
  116. package/src/services/user/permissions.js +0 -0
  117. package/src/services/user/profile.js +0 -0
  118. package/src/services/user/sessions.js +0 -0
  119. package/src/services/user/types.d.ts +0 -0
  120. package/src/services/user/types.js +0 -0
  121. package/src/services/user/user-management-system.js +0 -0
  122. package/test/content.test.js +0 -0
  123. package/test/contentLikes.test.js +0 -0
  124. package/test/contentProgress.test.js +0 -0
  125. package/test/dataContext.test.js +0 -0
  126. package/test/forum.test.js +0 -0
  127. package/test/imageSRCBuilder.test.js +0 -0
  128. package/test/imageSRCVerify.test.js +0 -0
  129. package/test/lib/lastUpdated.test.js +0 -0
  130. package/test/live/contentProgressLive.test.js +0 -0
  131. package/test/live/railcontentLive.test.js +0 -0
  132. package/test/localStorageMock.js +0 -0
  133. package/test/log.js +0 -0
  134. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
  135. package/test/mockData/mockData_progress_content.json +0 -0
  136. package/test/mockData/mockData_sanity_progress_content.json +0 -0
  137. package/test/mockData/mockData_user_practices.json +0 -0
  138. package/test/notifications.test.js +0 -0
  139. package/test/progressRows.test.js +0 -0
  140. package/test/reporting.test.js +132 -0
  141. package/test/streakMessage.test.js +0 -0
  142. package/test/user/permissions.test.js +0 -0
  143. 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
- async function getById(contentId, dataKey, defaultValue) {
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
@@ -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
@@ -1,6 +1,15 @@
1
- import SyncRepository, { Q } from './base'
2
- import ContentProgress, { COLLECTION_TYPE, COLLECTION_ID_SELF, STATE } from '../models/ContentProgress'
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: { type: COLLECTION_TYPE; id: number } | null = null) {
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
- { collection }: { collection?: { type: COLLECTION_TYPE; id: number } | null } = {}
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: { type: COLLECTION_TYPE; id: number } | null = null
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
- recordProgress(contentId: number, collection: { type: COLLECTION_TYPE; id: number } | null, progressPct: number, resumeTime?: number) {
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: { type: COLLECTION_TYPE; id: number } | null,
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: { type: COLLECTION_TYPE; id: number } | null
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: { type: COLLECTION_TYPE; id: number } | null) {
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: { type: COLLECTION_TYPE; id: number } | null
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
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
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/test/log.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes