| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
- import { useCallback, useState } from 'react'
- import * as eventTracking from '@/infrastructure/event-tracking'
- import { postJSON } from '@/infrastructure/fetch-json'
- import { debugConsole } from '@/utils/debugging'
- import { useTutorialContext } from '@/shared/context/tutorial-context'
- type CompleteTutorialParams = {
- event: string
- action: 'complete' | 'postpone'
- } & Record<string, any>
- type CompleteTutorialOptions = {
- // Whether to ignore errors if the request fails. Defaults to true. If
- // successfull completion is required, set this to false.
- failSilently?: boolean
- }
- const useTutorial = (
- tutorialKey: string,
- eventData: Record<string, any> = {}
- ) => {
- const [showPopup, setShowPopup] = useState(false)
- const {
- deactivateTutorial,
- currentPopup,
- currentPopupRef,
- setCurrentPopup,
- inactiveTutorials,
- } = useTutorialContext()
- const checkCompletion = useCallback(
- () => inactiveTutorials.includes(tutorialKey),
- // currentPopup is a dependency so consumers re-run their effect when the
- // popup slot frees up and can retry tryShowingPopup.
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [inactiveTutorials, tutorialKey, currentPopup]
- )
- const clearPopup = useCallback(() => {
- // popups should only clear themselves, in cases they need to cleanup or shouldnt show anymore
- // allow forcing the clear if needed, eg: higher prio alert needs to show
- if (currentPopupRef.current === tutorialKey) {
- setCurrentPopup(null)
- setShowPopup(false)
- }
- }, [currentPopupRef, setCurrentPopup, setShowPopup, tutorialKey])
- const completeTutorial = useCallback(
- async (
- {
- event = 'promo-click',
- action = 'complete',
- ...rest
- }: CompleteTutorialParams,
- options: CompleteTutorialOptions = {}
- ) => {
- eventTracking.sendMB(event, { ...eventData, ...rest })
- try {
- await postJSON(`/tutorial/${tutorialKey}/${action}`)
- } catch (err) {
- const failSilently = options.failSilently ?? true
- if (!failSilently) {
- throw err
- }
- debugConsole.error(err)
- }
- deactivateTutorial(tutorialKey)
- clearPopup()
- },
- [deactivateTutorial, eventData, tutorialKey, clearPopup]
- )
- const dismissTutorial = useCallback(
- async (eventName: string = 'promo-dismiss') => {
- await completeTutorial({
- event: eventName,
- action: 'complete',
- })
- },
- [completeTutorial]
- )
- const maybeLater = useCallback(async () => {
- await completeTutorial({
- event: 'promo-click',
- action: 'postpone',
- button: 'maybe-later',
- })
- }, [completeTutorial])
- // try to show the popup if we don't already have one showing, returns true if it can show, false if it can't
- const tryShowingPopup = useCallback(
- (eventName: string = 'promo-prompt') => {
- // Check the lock via ref so concurrent callers in the same render see
- // each other's claim; useState would batch and both would read stale null.
- if (currentPopupRef.current !== null) {
- return false
- }
- setCurrentPopup(tutorialKey)
- setShowPopup(true)
- eventTracking.sendMB(eventName, eventData)
- return true
- },
- [currentPopupRef, setCurrentPopup, tutorialKey, eventData]
- )
- const clearAndShow = useCallback(
- (eventName: string = 'promo-prompt') => {
- setCurrentPopup(tutorialKey)
- setShowPopup(true)
- eventTracking.sendMB(eventName, eventData)
- },
- [setCurrentPopup, setShowPopup, tutorialKey, eventData]
- )
- const hideUntilReload = useCallback(() => {
- clearPopup()
- deactivateTutorial(tutorialKey)
- }, [clearPopup, deactivateTutorial, tutorialKey])
- return {
- completeTutorial,
- dismissTutorial,
- maybeLater,
- tryShowingPopup,
- clearPopup,
- clearAndShow,
- showPopup,
- hideUntilReload,
- checkCompletion,
- }
- }
- export default useTutorial
|