use-tutorial.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import { useCallback, useState } from 'react'
  2. import * as eventTracking from '@/infrastructure/event-tracking'
  3. import { postJSON } from '@/infrastructure/fetch-json'
  4. import { debugConsole } from '@/utils/debugging'
  5. import { useTutorialContext } from '@/shared/context/tutorial-context'
  6. type CompleteTutorialParams = {
  7. event: string
  8. action: 'complete' | 'postpone'
  9. } & Record<string, any>
  10. type CompleteTutorialOptions = {
  11. // Whether to ignore errors if the request fails. Defaults to true. If
  12. // successfull completion is required, set this to false.
  13. failSilently?: boolean
  14. }
  15. const useTutorial = (
  16. tutorialKey: string,
  17. eventData: Record<string, any> = {}
  18. ) => {
  19. const [showPopup, setShowPopup] = useState(false)
  20. const {
  21. deactivateTutorial,
  22. currentPopup,
  23. currentPopupRef,
  24. setCurrentPopup,
  25. inactiveTutorials,
  26. } = useTutorialContext()
  27. const checkCompletion = useCallback(
  28. () => inactiveTutorials.includes(tutorialKey),
  29. // currentPopup is a dependency so consumers re-run their effect when the
  30. // popup slot frees up and can retry tryShowingPopup.
  31. // eslint-disable-next-line react-hooks/exhaustive-deps
  32. [inactiveTutorials, tutorialKey, currentPopup]
  33. )
  34. const clearPopup = useCallback(() => {
  35. // popups should only clear themselves, in cases they need to cleanup or shouldnt show anymore
  36. // allow forcing the clear if needed, eg: higher prio alert needs to show
  37. if (currentPopupRef.current === tutorialKey) {
  38. setCurrentPopup(null)
  39. setShowPopup(false)
  40. }
  41. }, [currentPopupRef, setCurrentPopup, setShowPopup, tutorialKey])
  42. const completeTutorial = useCallback(
  43. async (
  44. {
  45. event = 'promo-click',
  46. action = 'complete',
  47. ...rest
  48. }: CompleteTutorialParams,
  49. options: CompleteTutorialOptions = {}
  50. ) => {
  51. eventTracking.sendMB(event, { ...eventData, ...rest })
  52. try {
  53. await postJSON(`/tutorial/${tutorialKey}/${action}`)
  54. } catch (err) {
  55. const failSilently = options.failSilently ?? true
  56. if (!failSilently) {
  57. throw err
  58. }
  59. debugConsole.error(err)
  60. }
  61. deactivateTutorial(tutorialKey)
  62. clearPopup()
  63. },
  64. [deactivateTutorial, eventData, tutorialKey, clearPopup]
  65. )
  66. const dismissTutorial = useCallback(
  67. async (eventName: string = 'promo-dismiss') => {
  68. await completeTutorial({
  69. event: eventName,
  70. action: 'complete',
  71. })
  72. },
  73. [completeTutorial]
  74. )
  75. const maybeLater = useCallback(async () => {
  76. await completeTutorial({
  77. event: 'promo-click',
  78. action: 'postpone',
  79. button: 'maybe-later',
  80. })
  81. }, [completeTutorial])
  82. // try to show the popup if we don't already have one showing, returns true if it can show, false if it can't
  83. const tryShowingPopup = useCallback(
  84. (eventName: string = 'promo-prompt') => {
  85. // Check the lock via ref so concurrent callers in the same render see
  86. // each other's claim; useState would batch and both would read stale null.
  87. if (currentPopupRef.current !== null) {
  88. return false
  89. }
  90. setCurrentPopup(tutorialKey)
  91. setShowPopup(true)
  92. eventTracking.sendMB(eventName, eventData)
  93. return true
  94. },
  95. [currentPopupRef, setCurrentPopup, tutorialKey, eventData]
  96. )
  97. const clearAndShow = useCallback(
  98. (eventName: string = 'promo-prompt') => {
  99. setCurrentPopup(tutorialKey)
  100. setShowPopup(true)
  101. eventTracking.sendMB(eventName, eventData)
  102. },
  103. [setCurrentPopup, setShowPopup, tutorialKey, eventData]
  104. )
  105. const hideUntilReload = useCallback(() => {
  106. clearPopup()
  107. deactivateTutorial(tutorialKey)
  108. }, [clearPopup, deactivateTutorial, tutorialKey])
  109. return {
  110. completeTutorial,
  111. dismissTutorial,
  112. maybeLater,
  113. tryShowingPopup,
  114. clearPopup,
  115. clearAndShow,
  116. showPopup,
  117. hideUntilReload,
  118. checkCompletion,
  119. }
  120. }
  121. export default useTutorial