about summary refs log tree commit diff
path: root/src/components/dialogs/nuxs/index.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/dialogs/nuxs/index.tsx')
-rw-r--r--src/components/dialogs/nuxs/index.tsx151
1 files changed, 151 insertions, 0 deletions
diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx
new file mode 100644
index 000000000..781696070
--- /dev/null
+++ b/src/components/dialogs/nuxs/index.tsx
@@ -0,0 +1,151 @@
+import React from 'react'
+
+import {useGate} from '#/lib/statsig/statsig'
+import {logger} from '#/logger'
+import {
+  Nux,
+  useNuxs,
+  useRemoveNuxsMutation,
+  useUpsertNuxMutation,
+} from '#/state/queries/nuxs'
+import {useSession} from '#/state/session'
+import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
+import {TenMillion} from '#/components/dialogs/nuxs/TenMillion'
+import {IS_DEV} from '#/env'
+
+type Context = {
+  activeNux: Nux | undefined
+  dismissActiveNux: () => void
+}
+
+/**
+ * If we fail to complete a NUX here, it may show again on next reload,
+ * or if prefs state updates. If `true`, this fallback ensures that the last
+ * shown NUX won't show again, at least for this session.
+ *
+ * This is temporary, and only needed for the 10Milly dialog rn, since we
+ * aren't snoozing that one in device storage.
+ */
+let __isSnoozedFallback = false
+
+const queuedNuxs: {
+  id: Nux
+  enabled(props: {gate: ReturnType<typeof useGate>}): boolean
+  /**
+   * TEMP only intended for use with the 10Milly dialog rn, since there are no
+   * other NUX dialogs configured
+   */
+  unsafe_disableSnooze: boolean
+}[] = [
+  {
+    id: Nux.TenMillionDialog,
+    enabled({gate}) {
+      return gate('ten_million_dialog')
+    },
+    unsafe_disableSnooze: true,
+  },
+]
+
+const Context = React.createContext<Context>({
+  activeNux: undefined,
+  dismissActiveNux: () => {},
+})
+
+export function useNuxDialogContext() {
+  return React.useContext(Context)
+}
+
+export function NuxDialogs() {
+  const {hasSession} = useSession()
+  return hasSession ? <Inner /> : null
+}
+
+function Inner() {
+  const gate = useGate()
+  const {nuxs} = useNuxs()
+  const [snoozed, setSnoozed] = React.useState(() => {
+    return isSnoozed()
+  })
+  const [activeNux, setActiveNux] = React.useState<Nux | undefined>()
+  const {mutateAsync: upsertNux} = useUpsertNuxMutation()
+  const {mutate: removeNuxs} = useRemoveNuxsMutation()
+
+  const snoozeNuxDialog = React.useCallback(() => {
+    snooze()
+    setSnoozed(true)
+  }, [setSnoozed])
+
+  const dismissActiveNux = React.useCallback(() => {
+    if (!activeNux) return
+    setActiveNux(undefined)
+  }, [activeNux, setActiveNux])
+
+  if (IS_DEV && typeof window !== 'undefined') {
+    // @ts-ignore
+    window.clearNuxDialog = (id: Nux) => {
+      if (!IS_DEV || !id) return
+      removeNuxs([id])
+      unsnooze()
+    }
+  }
+
+  React.useEffect(() => {
+    if (__isSnoozedFallback) return
+    if (snoozed) return
+    if (!nuxs) return
+
+    for (const {id, enabled, unsafe_disableSnooze} of queuedNuxs) {
+      const nux = nuxs.find(nux => nux.id === id)
+
+      // check if completed first
+      if (nux && nux.completed) continue
+
+      // then check gate (track exposure)
+      if (!enabled({gate})) continue
+
+      // we have a winner
+      setActiveNux(id)
+
+      /**
+       * TEMP only intended for use with the 10Milly dialog rn, since there are no
+       * other NUX dialogs configured
+       */
+      if (!unsafe_disableSnooze) {
+        // immediately snooze for a day
+        snoozeNuxDialog()
+      }
+
+      // immediately update remote data (affects next reload)
+      upsertNux({
+        id,
+        completed: true,
+        data: undefined,
+      }).catch(e => {
+        logger.error(`NUX dialogs: failed to upsert '${id}' NUX`, {
+          safeMessage: e.message,
+        })
+        /*
+         * TEMP only intended for use with the 10Milly dialog rn
+         */
+        if (unsafe_disableSnooze) {
+          __isSnoozedFallback = true
+        }
+      })
+
+      break
+    }
+  }, [nuxs, snoozed, snoozeNuxDialog, upsertNux, gate])
+
+  const ctx = React.useMemo(() => {
+    return {
+      activeNux,
+      dismissActiveNux,
+    }
+  }, [activeNux, dismissActiveNux])
+
+  return (
+    <Context.Provider value={ctx}>
+      {activeNux === Nux.TenMillionDialog && <TenMillion />}
+    </Context.Provider>
+  )
+}