awards/award-callbacks.js

/**
 * @module Awards
 */

import './types.js'
import { awardEvents } from './internal/award-events'
import { getBadgeFields } from './award-query.js'

let awardGrantedCallback = null
let progressUpdateCallback = null

/**
 * @param {AwardCallbackFunction} callback - Function called with award data when an award is earned
 * @returns {UnregisterFunction} Cleanup function to unregister this callback
 *
 * @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
 *
 * @example // 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) }
 * }
 *
 * @example // 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
 *     })
 *   })
 * }, [])
 */
export function registerAwardCallback(callback) {
  if (typeof callback !== 'function') {
    throw new Error('registerAwardCallback requires a function')
  }

  unregisterAwardCallback()

  awardGrantedCallback = async (payload) => {
    const { awardId, definition, completionData, popupMessage } = payload

    const award = {
      awardId: awardId,
      name: definition.name,
      brand: definition.brand,
      ...getBadgeFields(definition),
      contentType: definition.content_type,
      hasCertificate: definition.type === 'content-award',
      completedAt: completionData.completed_at,
      isCompleted: true,
      completionData: {
        completedAt: completionData.completed_at,
        days_user_practiced: completionData.days_user_practiced,
        message: popupMessage,
        practice_minutes: completionData.practice_minutes,
        content_title: completionData.content_title,
      },
    }

    callback(award)
  }

  awardEvents.on('awardGranted', awardGrantedCallback)

  return unregisterAwardCallback
}

function unregisterAwardCallback() {
  if (awardGrantedCallback) {
    awardEvents.off('awardGranted', awardGrantedCallback)
    awardGrantedCallback = null
  }
}

/**
 * @param {ProgressCallbackFunction} callback - Function called with progress data when award progress changes
 * @returns {UnregisterFunction} Cleanup function to unregister this callback
 *
 * @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.
 *
 * @example // 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
 * }
 *
 * @example // 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.')
 *     }
 *   })
 * }, [])
 */
export function registerProgressCallback(callback) {
  if (typeof callback !== 'function') {
    throw new Error('registerProgressCallback requires a function')
  }

  unregisterProgressCallback()

  progressUpdateCallback = (payload) => {
    callback({
      awardId: payload.awardId,
      progressPercentage: payload.progressPercentage,
    })
  }

  awardEvents.on('awardProgress', progressUpdateCallback)

  return unregisterProgressCallback
}

function unregisterProgressCallback() {
  if (progressUpdateCallback) {
    awardEvents.off('awardProgress', progressUpdateCallback)
    progressUpdateCallback = null
  }
}