- Description:
- Query award progress, listen for award events, and generate certificates. **Query Functions** (read-only): - `getContentAwards(contentId)` - Get awards for a learning path/course - `getContentAwardsByIds(contentIds)` - Get awards for multiple content items (batch optimized) - `getCompletedAwards(brand)` - Get user's earned awards - `getInProgressAwards(brand)` - Get awards user is working toward - `getAwardStatistics(brand)` - Get aggregate award stats **Event Callbacks**: - `registerAwardCallback(fn)` - Listen for new awards earned - `registerProgressCallback(fn)` - Listen for progress updates **Certificates**: - `fetchCertificate(awardId)` - Generate certificate (Web only)
- Source:
Examples
Quick Start
import {
getContentAwards,
getCompletedAwards,
registerAwardCallback
} from 'musora-content-services'
// Show awards for a learning path
const { hasAwards, awards } = await getContentAwards(learningPathId)
// Get user's completed awards
const completed = await getCompletedAwards('drumeo')
// Listen for new awards (show notification)
useEffect(() => {
return registerAwardCallback((award) => {
showCelebration(award.name, award.badge, award.completion_data.message)
})
}, [])
How Awards Update (Collection Context)
// Award progress updates automatically when you save content progress.
// CRITICAL: Pass collection context when inside a learning path!
import { contentStatusCompleted } from 'musora-content-services'
// Correct - passes collection context
const collection = { type: 'learning-path-v2', id: learningPathId }
await contentStatusCompleted(lessonId, collection)
// Wrong - no collection context (affects wrong awards!)
await contentStatusCompleted(lessonId, null)
Methods
(static) getAwardStatistics(brandopt) → {Promise.<AwardStatistics>}
- Description:
- Returns aggregate statistics about the user's award progress. Use this for profile stats, gamification dashboards, or achievement summaries. Returns an object with: - `totalAvailable` - Total awards that can be earned - `completed` - Number of awards earned - `inProgress` - Number of awards started but not completed - `notStarted` - Number of awards not yet started - `completionPercentage` - Overall completion % (0-100, one decimal) Returns all zeros on error (never throws).
- Source:
Examples
// Profile stats card
function ProfileStatsCard() {
const [stats, setStats] = useState(null)
const brand = useBrand()
useEffect(() => {
getAwardStatistics(brand).then(setStats)
}, [brand])
if (!stats) return <LoadingSpinner />
return (
<View style={styles.statsCard}>
<StatItem label="Awards Earned" value={stats.completed} />
<StatItem label="In Progress" value={stats.inProgress} />
<StatItem label="Available" value={stats.totalAvailable} />
<ProgressRing
progress={stats.completionPercentage / 100}
label={`${stats.completionPercentage}%`}
/>
</View>
)
}
// Achievement progress bar
const stats = await getAwardStatistics('drumeo')
return (
<View>
<Text>{stats.completed} of {stats.totalAvailable} awards earned</Text>
<ProgressBar progress={stats.completionPercentage / 100} />
</View>
)
Parameters:
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
brand |
string |
<optional> |
null
|
Brand to filter by (drumeo, pianote, guitareo, singeo), or null for all brands |
Returns:
Statistics object with award counts and completion percentage
- Type
- Promise.<AwardStatistics>
(static) getCompletedAwards(brandopt, optionsopt) → {Promise.<Array.<AwardInfo>>}
- Description:
- Returns all awards the user has completed. Use this for "My Achievements" or profile award gallery screens. Each award includes: - Badge and award images for display - Completion date for "Earned on X" display - `completionData.message` - Pre-generated congratulations text - `completionData.XXX` - other fields are award type dependant Returns empty array `[]` on error (never throws).
- Source:
Examples
// Awards gallery screen
function AwardsGalleryScreen() {
const [awards, setAwards] = useState([])
const brand = useBrand() // 'drumeo', 'pianote', etc.
useEffect(() => {
getCompletedAwards(brand).then(setAwards)
}, [brand])
return (
<FlatList
data={awards}
keyExtractor={item => item.awardId}
numColumns={2}
renderItem={({ item }) => (
<AwardBadge
badge={item.badge}
title={item.awardTitle}
earnedDate={new Date(item.completedAt).toLocaleDateString()}
onPress={() => showAwardDetail(item)}
/>
)}
/>
)
}
// Show award detail with practice stats
function AwardDetailModal({ award }) {
return (
<Modal>
<Image source={{ uri: award.award }} />
<Text>{award.awardTitle}</Text>
<Text>Instructor: {award.instructorName}</Text>
<Text>Completed: {new Date(award.completedAt).toLocaleDateString()}</Text>
<Text>Practice time: {award.completionData.practice_minutes} minutes</Text>
<Text>Days practiced: {award.completionData.days_user_practiced}</Text>
<Text>{award.completionData.message}</Text>
</Modal>
)
}
// Paginated loading
const PAGE_SIZE = 12
const loadMore = async (page) => {
const newAwards = await getCompletedAwards(brand, {
limit: PAGE_SIZE,
offset: page * PAGE_SIZE
})
setAwards(prev => [...prev, ...newAwards])
}
Parameters:
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
brand |
string |
<optional> |
null
|
Brand to filter by (drumeo, pianote, guitareo, singeo), or null for all brands |
options |
AwardPaginationOptions |
<optional> |
{}
|
Optional pagination and filtering |
Returns:
Array of completed award objects sorted by completion date (newest first)
- Type
- Promise.<Array.<AwardInfo>>
(static) getContentAwards(contentId) → {Promise.<ContentAwardsResponse>}
- Description:
- Returns awards associated with a content item and the user's progress toward each. Use this to display award progress on learning path or course detail pages. - Pass a **learning path ID** to get awards for that learning path - Pass a **course ID** to get awards for that course - Pass a **lesson ID** to get all awards that include that lesson in their requirements Returns `{ hasAwards: false, awards: [] }` on error (never throws).
- Source:
Examples
// Display award card on learning path detail page
function LearningPathAwardCard({ learningPathId }) {
const [awardData, setAwardData] = useState({ hasAwards: false, awards: [] })
useEffect(() => {
getContentAwards(learningPathId).then(setAwardData)
}, [learningPathId])
if (!awardData.hasAwards) return null
const award = awardData.awards[0] // Learning paths typically have one award
return (
<AwardCard
badge={award.badge}
title={award.awardTitle}
progress={award.progressPercentage}
isCompleted={award.isCompleted}
completedAt={award.completedAt}
instructorName={award.instructorName}
/>
)
}
// Check award status before showing certificate button
const { hasAwards, awards } = await getContentAwards(learningPathId)
const completedAward = awards.find(a => a.isCompleted)
if (completedAward) {
showCertificateButton(completedAward.awardId)
}
Parameters:
| Name | Type | Description |
|---|---|---|
contentId |
number | Railcontent ID of the content item (lesson, course, or learning path) |
Returns:
Status object with award information
- Type
- Promise.<ContentAwardsResponse>
(static) getContentAwardsByIds(contentIds) → {Promise.<Object.<number, ContentAwardsResponse>>}
- Description:
- Returns awards for multiple content items at once. More efficient than calling `getContentAwards()` multiple times. Returns an object where keys are content IDs and values are the same structure as `getContentAwards()`. Content IDs without awards will have `{ hasAwards: false, awards: [] }` in the result. Returns empty object `{}` on error (never throws).
- Source:
Examples
const learningPathIds = [12345, 67890, 11111]
const awardsMap = await getContentAwardsByIds(learningPathIds)
learningPathIds.forEach(id => {
const { hasAwards, awards } = awardsMap[id] || { hasAwards: false, awards: [] }
if (hasAwards) {
console.log(`Learning path ${id} has ${awards.length} award(s)`)
}
})
function CourseListWithAwards({ courseIds }) {
const [awardsMap, setAwardsMap] = useState({})
useEffect(() => {
getContentAwardsByIds(courseIds).then(setAwardsMap)
}, [courseIds])
return courseIds.map(courseId => {
const { hasAwards, awards } = awardsMap[courseId] || { hasAwards: false, awards: [] }
return (
<CourseCard key={courseId} courseId={courseId}>
{hasAwards && <AwardBadge award={awards[0]} />}
</CourseCard>
)
})
}
Parameters:
| Name | Type | Description |
|---|---|---|
contentIds |
Array.<number> | Array of Railcontent IDs to fetch awards for |
Returns:
Object mapping content IDs to their award data
- Type
- Promise.<Object.<number, ContentAwardsResponse>>
(static) getInProgressAwards(brandopt, optionsopt) → {Promise.<Array.<AwardInfo>>}
- Description:
- Returns awards the user has started but not yet completed. Sorted by progress percentage (highest first) so awards closest to completion appear first. Use this for "Continue Learning" or dashboard widgets. Progress is calculated based on lessons completed within the correct collection context. For learning paths, only lessons completed within that specific learning path count toward its award. Returns empty array `[]` on error (never throws).
- Source:
Examples
// "Almost There" widget on home screen
function AlmostThereWidget() {
const [topAwards, setTopAwards] = useState([])
const brand = useBrand()
useEffect(() => {
// Get top 3 closest to completion
getInProgressAwards(brand, { limit: 3 }).then(setTopAwards)
}, [brand])
if (topAwards.length === 0) return null
return (
<View>
<Text>Almost There!</Text>
{topAwards.map(award => (
<TouchableOpacity
key={award.awardId}
onPress={() => navigateToLearningPath(award)}
>
<Image source={{ uri: award.badge }} />
<Text>{award.awardTitle}</Text>
<ProgressBar progress={award.progressPercentage / 100} />
<Text>{award.progressPercentage}% complete</Text>
</TouchableOpacity>
))}
</View>
)
}
// Full in-progress awards list
function InProgressAwardsScreen() {
const [awards, setAwards] = useState([])
useEffect(() => {
getInProgressAwards().then(setAwards)
}, [])
return (
<FlatList
data={awards}
keyExtractor={item => item.awardId}
renderItem={({ item }) => (
<AwardProgressCard
badge={item.badge}
title={item.awardTitle}
progress={item.progressPercentage}
brand={item.brand}
/>
)}
/>
)
}
Parameters:
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
brand |
string |
<optional> |
null
|
Brand to filter by (drumeo, pianote, guitareo, singeo), or null for all brands |
options |
AwardPaginationOptions |
<optional> |
{}
|
Optional pagination options |
Returns:
Array of in-progress award objects sorted by progress percentage (highest first)
- Type
- Promise.<Array.<AwardInfo>>
(static) registerAwardCallback(callback) → {UnregisterFunction}
- Description:
- Registers a callback to be notified when the user earns a new award. Only one callback can be registered at a time - registering a new one replaces the previous. Always call the returned cleanup function when your component unmounts. The callback receives an award object with: - `awardId` - Unique Sanity award ID - `name` - Display name of the award - `badge` - URL to badge image - `contentType` - Content type ('guided-course' or 'learning-path-v2') - `completed_at` - ISO timestamp - `isCompleted` - Boolean indicating the award is completed (always true for granted awards) - `completion_data.message` - Pre-generated congratulations message - `completion_data.practice_minutes` - Total practice time - `completion_data.days_user_practiced` - Days spent practicing - `completion_data.content_title` - Title of completed content
- Source:
Examples
// React Native - Show award celebration modal
function useAwardNotification() {
const [award, setAward] = useState(null)
useEffect(() => {
return registerAwardCallback((awardData) => {
setAward({
title: awardData.name,
badge: awardData.badge,
message: awardData.completion_data.message,
practiceMinutes: awardData.completion_data.practice_minutes
})
})
}, [])
return { award, dismissAward: () => setAward(null) }
}
// Track award in analytics
useEffect(() => {
return registerAwardCallback((award) => {
analytics.track('Award Earned', {
awardId: award.awardId,
awardName: award.name,
practiceMinutes: award.completion_data.practice_minutes,
contentTitle: award.completion_data.content_title
})
})
}, [])
Parameters:
| Name | Type | Description |
|---|---|---|
callback |
AwardCallbackFunction | Function called with award data when an award is earned |
Returns:
Cleanup function to unregister this callback
- Type
- UnregisterFunction
(static) registerProgressCallback(callback) → {UnregisterFunction}
- Description:
- Registers a callback to be notified when award progress changes (but award is not yet complete). Only one callback can be registered at a time. Use this to update progress bars or show "almost there" encouragement. The callback receives: - `awardId` - Unique Sanity award ID - `progressPercentage` - Current completion percentage (0-99) Note: When an award reaches 100%, `registerAwardCallback` fires instead.
- Source:
Examples
// React Native - Update progress in learning path screen
function LearningPathScreen({ learningPathId }) {
const [awardProgress, setAwardProgress] = useState({})
useEffect(() => {
return registerProgressCallback(({ awardId, progressPercentage }) => {
setAwardProgress(prev => ({
...prev,
[awardId]: progressPercentage
}))
})
}, [])
// Use awardProgress to update UI
}
// Show encouragement toast at milestones
useEffect(() => {
return registerProgressCallback(({ awardId, progressPercentage }) => {
if (progressPercentage === 50) {
showToast('Halfway to your award!')
} else if (progressPercentage >= 90) {
showToast('Almost there! Just a few more lessons.')
}
})
}, [])
Parameters:
| Name | Type | Description |
|---|---|---|
callback |
ProgressCallbackFunction | Function called with progress data when award progress changes |
Returns:
Cleanup function to unregister this callback
- Type
- UnregisterFunction
(static) resetAllAwards() → {Promise.<{deletedCount: number}>}
- Source:
Returns:
- Type
- Promise.<{deletedCount: number}>
(async, inner) fetchCertificate(awardId) → {Promise.<Certificate>}
- Description:
- Fetch certificate data for a completed award with all images converted to base64. Returns certificate information ready for rendering in a PDF or image format. All image URLs (logos, signatures, ribbons) are converted to base64 strings for offline use.
- Source:
Examples
Generate certificate PDF (Web)
const cert = await fetchCertificate('abc-123')
generatePDF({
userName: cert.user_name,
awardTitle: cert.title,
completedAt: new Date(cert.completed_at).toLocaleDateString(),
message: cert.message,
brandLogo: `data:image/png;base64,${cert.brand_logo_64}`,
signature: cert.instructor_signature_64
? `data:image/png;base64,${cert.instructor_signature_64}`
: null
})
Display certificate preview (Web)
const cert = await fetchCertificate(awardId)
return (
<CertificatePreview
userName={cert.user_name}
awardTitle={cert.title}
message={cert.message}
awardImage={`data:image/png;base64,${cert.award_image_64}`}
instructorName={cert.instructor_name}
signature={cert.instructor_signature_64}
/>
)
React Native Implementation
// This function is NOT compatible with React Native due to FileReader/Blob APIs.
// For React Native, implement certificate generation using:
//
// 1. Use react-native-blob-util for base64 image conversion:
// import ReactNativeBlobUtil from 'react-native-blob-util'
// const base64 = await ReactNativeBlobUtil.fetch('GET', imageUrl)
// .then(res => res.base64())
//
// 2. Use react-native-html-to-pdf for PDF generation:
// import RNHTMLtoPDF from 'react-native-html-to-pdf'
// const pdf = await RNHTMLtoPDF.convert({
// html: certificateHtmlTemplate,
// fileName: `certificate-${awardId}`,
// directory: 'Documents',
// })
//
// 3. Build certificate data using getContentAwards() or getCompletedAwards()
// which ARE React Native compatible, then handle image conversion
// and PDF generation in your RN app layer.
Parameters:
| Name | Type | Description |
|---|---|---|
awardId |
string | Unique Sanity award ID |
Throws:
-
If award is not found or not completed
- Type
- Error
Returns:
Certificate object with base64-encoded images:
- award_id {string} - Sanity award ID
- user_name {string} - User's display name
- user_id {number} - User's ID
- completed_at {string} - ISO timestamp of completion
- message {string} - Certificate message for display
- type {string} - Award type (e.g., 'content-award')
- title {string} - Award title/name
- musora_logo {string} - URL to Musora logo
- musora_logo_64 {string} - Base64-encoded Musora logo
- musora_bg_logo {string} - URL to Musora background logo
- musora_bg_logo_64 {string} - Base64-encoded background logo
- brand_logo {string} - URL to brand logo
- brand_logo_64 {string} - Base64-encoded brand logo
- ribbon_image {string} - URL to ribbon decoration
- ribbon_image_64 {string} - Base64-encoded ribbon image
- award_image {string} - URL to award image
- award_image_64 {string} - Base64-encoded award image
- instructor_name {string} - Instructor's name
- instructor_signature {string|undefined} - URL to signature (if available)
- instructor_signature_64 {string|undefined} - Base64-encoded signature
- Type
- Promise.<Certificate>