Skip to content

Commit

Permalink
Onboarding V2 flow (#977)
Browse files Browse the repository at this point in the history
* poc

* cleanup

* WIP

* testing fixes

* updated animations

* more updates

* naming tweaks

* support excluding screens

* linting

* linting

* fixed animations on first steps

* linting

---------

Co-authored-by: Shane Osbourne <[email protected]>
  • Loading branch information
shakyShane and Shane Osbourne authored Jun 24, 2024
1 parent d91c50c commit 6baeff9
Show file tree
Hide file tree
Showing 29 changed files with 677 additions and 229 deletions.
File renamed without changes.
4 changes: 4 additions & 0 deletions packages/special-pages/pages/onboarding/app/Components.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Summary } from './pages/Summary'
import { Switch } from './components/Switch'
import { useState } from 'preact/hooks'
import { Typed } from './components/Typed'
import { CleanBrowsing } from './pages/CleanBrowsing'

function noop (name) {
return () => {
Expand Down Expand Up @@ -42,6 +43,9 @@ export function Components () {
<p><a href="?env=app">Onboarding Flow</a></p>
<Header><Typed text={'Welcome to DuckDuckGo'}/></Header>
<Progress current={1} total={4}/>
<div>
<CleanBrowsing onNextPage={console.log}/>
</div>
<div>
<ButtonBar>
<NewCheck variant={'windows'}/>
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { useAutoAnimate } from '@formkit/auto-animate/preact'
import { h } from 'preact'
import { useContext } from 'preact/hooks'
import { SettingsContext } from '../settings'
import { useEnv } from '../environment'

/**
* Apply auto-animate to arbitrary elements
* @param {Object} props - The properties of the component.
* @param {import("preact").ComponentChild} props.children - The child elements to be animated.
*/
export function Animate (props) {
const { isReducedMotion } = useContext(SettingsContext)
const { isReducedMotion } = useEnv()
const [parent] = useAutoAnimate(isReducedMotion ? { duration: 0 } : undefined)
return (
<div ref={parent}>
Expand Down
108 changes: 67 additions & 41 deletions packages/special-pages/pages/onboarding/app/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { GlobalContext, GlobalDispatch } from '../global'
import { Background } from './Background'
import { GetStarted } from '../pages/Welcome'
import { PrivacyDefault } from '../pages/PrivacyDefault'
import { CleanBrowsing } from '../pages/CleanBrowsing'
import { CleanBrowsing, animation } from '../pages/CleanBrowsing'
import { SettingsStep } from '../pages/SettingsStep'
import { settingsRowItems } from '../data'
import { settingsRowItems, stepMeta } from '../data'
import { useTranslation } from '../translations'
import { SettingsContext } from '../settings'
import { useEnv } from '../environment'
import { Header } from './Header'
import { Typed } from './Typed'
import { Stack } from './Stack'
Expand All @@ -26,16 +26,22 @@ import { Progress } from './Progress'
* @param {import("preact").ComponentChild} props.children
*/
export function App ({ children }) {
const { debugState } = useContext(SettingsContext)
const { debugState, isReducedMotion } = useEnv()
const globalState = useContext(GlobalContext)
const dispatch = useContext(GlobalDispatch)
const { t } = useTranslation()

const { activeStep, activeStepVisible, exiting } = globalState
const { nextStep, activeStep, activeStepVisible, exiting, order, step } = globalState

// events
const enqueueNext = () => dispatch({ kind: 'next' })
const next = () => dispatch({ kind: 'next-for-real' })
const enqueueNext = () => {
if (isReducedMotion) {
dispatch({ kind: 'advance' })
} else {
dispatch({ kind: 'enqueue-next' })
}
}

const advance = () => dispatch({ kind: 'advance' })
const titleDone = () => dispatch({ kind: 'title-complete' })
const dismiss = () => dispatch({ kind: 'dismiss' })
const dismissToSettings = () => dispatch({ kind: 'dismiss-to-settings' })
Expand All @@ -47,30 +53,14 @@ export function App ({ children }) {

// typescript is not quite smart enough to figure this part out
const pageTitle = t(/** @type {any} */(activeStep + '_title'))
const nextPageTitle = t(/** @type {any} */(nextStep + '_title'))
const pageSubTitle = t(/** @type {any} */(activeStep + '_subtitle'))

/** @type {Record<import('../types').Step['id'], () => import("preact").ComponentChild>} */
const pages = {
const infoPages = {
welcome: () => <Timeout onComplete={enqueueNext} ignore={true} />,
getStarted: () => <GetStarted onNextPage={enqueueNext} />,
privateByDefault: () => <PrivacyDefault onNextPage={enqueueNext} />,
cleanerBrowsing: () => <CleanBrowsing onNextPage={enqueueNext} />,
systemSettings: () => (
<SettingsStep
key={'systemSettings'}
subtitle={pageSubTitle}
data={settingsRowItems}
onNextPage={enqueueNext}
/>
),
customize: () => (
<SettingsStep
key={'customize'}
subtitle={pageSubTitle}
data={settingsRowItems}
onNextPage={enqueueNext}
/>
),
summary: () => (
<Summary
values={globalState.values}
Expand All @@ -81,38 +71,66 @@ export function App ({ children }) {
}

/** @type {import('../types').Step['id'][]} */
const progress = ['privateByDefault', 'cleanerBrowsing', 'systemSettings', 'customize']
const progress = order.slice(2, -1)
const showProgress = progress.includes(activeStep)

// for screens that animate out, trigger the 'advance' when it's finished.
function animationDidFinish (e) {
if (e.target?.dataset?.exiting === 'true') {
advance()
}
}

// otherwise, for none-animating steps, just advance immediately when 'exiting' is set
const didRender = (e) => {
/** @type {import('../types').Step['id'][]} */
const ignoredSteps = ['welcome', 'getStarted']
const shouldSkipAnimation = ignoredSteps.includes(e?.dataset?.current)
if (shouldSkipAnimation && exiting === true) {
advance()
}
}

return (
<main className={styles.main}>
<Background />
{debugState && <Debug state={globalState} />}
<link rel="preload" href={['js', animation].join('/')} as="image"/>
<link rel="preload" href={['js', stepMeta.dockSingle.rows.dock.path].join('/')} as="image"/>
<link rel="preload" href={['js', stepMeta.importSingle.rows.import.path].join('/')} as="image"/>
<link rel="preload" href={['js', stepMeta.makeDefaultSingle.rows['default-browser'].path].join('/')} as="image"/>
<Background/>
{debugState && <Debug state={globalState}/>}
<div className={styles.container} data-current={activeStep}>
<ErrorBoundary didCatch={didCatch} fallback={<Fallback />}>
{/* This is used to allow an 'exit' animation to take place */}
{exiting && <Timeout onComplete={next} timeout={['welcome', 'getStarted'].includes(activeStep) ? 0 : 600} />}
<ErrorBoundary didCatch={didCatch} fallback={<Fallback/>}>
<Stack>
<Header aside={showProgress && <Progress current={progress.indexOf(activeStep) + 1} total={progress.length} />}>
<Header aside={showProgress && <Progress current={progress.indexOf(activeStep) + 1} total={progress.length}/>}>
<Typed
onComplete={titleDone}
text={pageTitle}
data-current={activeStep}
data-exiting={String(exiting)}
data-exiting={pageTitle !== nextPageTitle && String(exiting)}
/>
</Header>
<div data-current={activeStep} data-exiting={String(exiting)}>
<div data-current={activeStep} data-exiting={String(exiting)} ref={didRender} onAnimationEnd={animationDidFinish}>
{activeStepVisible && (
<Content>
{pages[activeStep]()}
{step.kind === 'settings' && (
<SettingsStep
key={activeStep}
subtitle={pageSubTitle}
data={settingsRowItems}
metaData={stepMeta}
onNextPage={enqueueNext}
/>
)}
{step.kind === 'info' && infoPages[activeStep]()}
</Content>
)}
</div>
</Stack>
<WillThrow />
<WillThrow/>
</ErrorBoundary>
</div>
{debugState && <DebugLinks current={activeStep} />}
{debugState && <DebugLinks current={activeStep}/>}
{children}
</main>
)
Expand All @@ -130,24 +148,32 @@ function Debug (props) {

function DebugLinks ({ current }) {
const globalState = useContext(GlobalContext)

const exceptionUrl = new URL(window.location.href)
exceptionUrl.searchParams.set('page', 'welcome')
exceptionUrl.searchParams.set('willThrow', 'true')

if (window.__playwright_01) return null
return (
<div style={{ display: 'flex', gap: '10px', position: 'fixed', bottom: '1rem', justifyContent: 'center', width: '100%' }}>
{Object.keys(globalState.stepDefinitions).slice(1).map(pageId => {
const next = new URL(window.location.href)
next.searchParams.set('page', pageId)
return (
<a href={`?page=${pageId}`} key={pageId} style={{
<a href={next.toString()} key={pageId} style={{
textDecoration: current === pageId ? 'none' : 'underline',
color: current === pageId ? 'black' : undefined
}}>{pageId}</a>
)
})}
<a href={'?page=welcome&willThrow=true'}>Exception</a>
<a href={exceptionUrl.toString()}>Exception</a>
</div>
)
}

function WillThrow () {
if (useContext(SettingsContext).willThrow) {
const { willThrow } = useEnv()
if (willThrow) {
throw new Error('Simulated Exception')
}
return null
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { h, Fragment } from 'preact'
import { useContext, useReducer } from 'preact/hooks'
import { useReducer } from 'preact/hooks'
import { Stack } from './Stack'
import { Button, ButtonBar } from './Buttons'
import { Play, Replay, SlideIn } from './Icons'

import styles from './BeforeAfter.module.css'
import { useTranslation } from '../translations'
import { useAutoAnimate } from '@formkit/auto-animate/preact'
import { SettingsContext } from '../settings'
import { useEnv } from '../environment'

/**
* A component that renders an image with a before and after effect.
Expand All @@ -20,7 +20,7 @@ import { SettingsContext } from '../settings'
*/
export function BeforeAfter ({ media, onDone, btnBefore, btnAfter }) {
const { t } = useTranslation()
const { isReducedMotion } = useContext(SettingsContext)
const { isReducedMotion } = useEnv()
const [imageParent] = useAutoAnimate(isReducedMotion ? { duration: 0 } : undefined)

// differentiate between initial states vs before/after
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { h } from 'preact'
import styles from './List.module.css'
import { useAutoAnimate } from '@formkit/auto-animate/preact'
import { useContext } from 'preact/hooks'
import { SettingsContext } from '../settings'
import { useEnv } from '../environment'

/**
* List component is used to display an item in a styled
Expand All @@ -11,7 +10,7 @@ import { SettingsContext } from '../settings'
* @param {boolean} [props.animate=false] - Should immediate children be animated into place?
*/
export function List (props) {
const { isReducedMotion } = useContext(SettingsContext)
const { isReducedMotion } = useEnv()
const [parent] = useAutoAnimate(isReducedMotion ? { duration: 0 } : undefined)
return (
<ul className={styles.list} ref={props.animate ? parent : null}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@ import { h } from 'preact'
import { useEffect, useRef } from 'preact/hooks'
import { Rive } from '@rive-app/canvas-single'

import animation from '../Onboarding.riv'

/**
* Renders a Rive animation on a canvas element and provides functionality to toggle inputs.
*
* @param {Object} props - The options for the RiveAnimation.
* @param {'before' | 'after'} props.state - The name of the state machine to load.
* @param {string} props.stateMachine - The name of the state machine to load.
* @param {string} props.artboard - The name of the artboard to display.
* @param {string} props.inputName - The name of the input to toggle.
* @param {string} [props.stateMachine] - The name of the state machine to load. (optional)
* @param {string} props.animation - The path to the animation file
* @param {string} [props.artboard] - The name of the artboard to display. (optional)
* @param {string} [props.inputName] - The name of the input to toggle. (optional)
* @param {boolean} props.isDarkMode - Indicates if dark mode is enabled.
*/
export function RiveAnimation ({ state, stateMachine, artboard, inputName, isDarkMode }) {
export function RiveAnimation ({ animation, state, stateMachine, artboard, inputName, isDarkMode }) {
const ref = useRef(/** @type {null | HTMLCanvasElement} */(null))
const rive = useRef(/** @type {null | Rive} */(null))

Expand All @@ -36,8 +35,10 @@ export function RiveAnimation ({ state, stateMachine, artboard, inputName, isDar

// handle a before/after value
useEffect(() => {
if (!stateMachine) return
const inputs = rive.current?.stateMachineInputs(stateMachine)
if (!inputs) return
if (!inputName) return

const toggle = inputs.find(i => i.name === inputName)
if (!toggle) return console.warn('could not find input')
Expand All @@ -48,8 +49,9 @@ export function RiveAnimation ({ state, stateMachine, artboard, inputName, isDar
// handle light/dark mode
useEffect(() => {
function handle () {
if (!stateMachine) return
const inputs = rive.current?.stateMachineInputs(stateMachine)
const themeInput = inputs?.find(i => i.name === 'Light?')
const themeInput = inputs?.find(i => i.name.startsWith('Light'))
if (themeInput) {
themeInput.value = !isDarkMode
}
Expand All @@ -62,6 +64,6 @@ export function RiveAnimation ({ state, stateMachine, artboard, inputName, isDar
}, [isDarkMode])

return (
<canvas width="432" height="208" ref={ref}></canvas>
<canvas width="432" height="208" ref={ref} style="border-radius: 12px; overflow: hidden"></canvas>
)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { h } from 'preact'
import styles from './Stack.module.css'
import { useAutoAnimate } from '@formkit/auto-animate/preact'
import { useContext } from 'preact/hooks'
import { SettingsContext } from '../settings'
import { useEnv } from '../environment'

/**
* Represents a stack component, use it for vertical spacing
Expand All @@ -15,7 +14,7 @@ import { SettingsContext } from '../settings'
* @param {boolean} [props.animate=false] - Should immediate children be animated into place?
*/
export function Stack ({ children, gap = 'var(--sp-6)', animate = false, debug = false }) {
const { isReducedMotion } = useContext(SettingsContext)
const { isReducedMotion } = useEnv()
const [parent] = useAutoAnimate({ duration: isReducedMotion ? 0 : 300 })
return (
<div class={styles.stack} ref={animate ? parent : null} data-debug={String(debug)} style={{ gap }}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { h } from 'preact'
import styles from './Switch.module.css'
import { SettingsContext } from '../settings'
import { useContext } from 'preact/hooks'
import { useEnv } from '../environment'

/**
* Switch component used to toggle between two states.
Expand All @@ -16,7 +15,8 @@ import { useContext } from 'preact/hooks'
*/
export function Switch ({ checked = false, variant, ...props }) {
const { onChecked, onUnchecked, ariaLabel, pending } = props
const platform = variant || useContext(SettingsContext).platform
const env = useEnv()
const platform = variant || env.platform
function change (e) {
if (e.target.checked === true) {
onChecked()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useContext, useEffect, useState } from 'preact/hooks'
import { useEffect, useState } from 'preact/hooks'
import { h } from 'preact'
import { SettingsContext } from '../settings'
import { useEnv } from '../environment'

/**
* Renders the first page of the application and provides an option to move to the next page.
Expand All @@ -11,7 +11,7 @@ import { SettingsContext } from '../settings'
* @param {boolean} [props.ignore] - Callback function to be called when the "Get Started" button is clicked.
*/
export function Timeout ({ onComplete, ignore, timeout = 1000 }) {
const { isReducedMotion } = useContext(SettingsContext)
const { isReducedMotion } = useEnv()
useEffect(() => {
let int
if (ignore) {
Expand All @@ -33,7 +33,7 @@ export function Timeout ({ onComplete, ignore, timeout = 1000 }) {
*/
export function Delay ({ children, ms = 1000 }) {
const [shown, setShown] = useState(false)
const { isReducedMotion } = useContext(SettingsContext)
const { isReducedMotion } = useEnv()
useEffect(() => {
const int = setTimeout(() => setShown(true), isReducedMotion ? 0 : ms)
return () => clearTimeout(int)
Expand Down
Loading

0 comments on commit 6baeff9

Please sign in to comment.