move onboarding to screens

zio/stable
Ansh Nanda 2023-08-28 13:37:44 -07:00
parent 84e065667a
commit edfd326069
11 changed files with 149 additions and 107 deletions

View File

@ -67,6 +67,8 @@ import {getRoutingInstrumentation} from 'lib/sentry'
import {bskyTitle} from 'lib/strings/headings' import {bskyTitle} from 'lib/strings/headings'
import {JSX} from 'react/jsx-runtime' import {JSX} from 'react/jsx-runtime'
import {timeout} from 'lib/async/timeout' import {timeout} from 'lib/async/timeout'
import {Welcome} from 'view/com/auth/onboarding/Welcome'
import {RecommendedFeeds} from 'view/com/auth/onboarding/RecommendedFeeds'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>() const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
@ -219,6 +221,18 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
component={SavedFeeds} component={SavedFeeds}
options={{title: title('Edit My Feeds')}} options={{title: title('Edit My Feeds')}}
/> />
<Stack.Group
screenOptions={{
animation: 'slide_from_bottom',
presentation: 'modal',
}}>
<Stack.Screen
name="Welcome"
component={Welcome}
options={{title: title('Welcome')}}
/>
<Stack.Screen name="RecommendedFeeds" component={RecommendedFeeds} />
</Stack.Group>
</> </>
) )
} }
@ -254,6 +268,7 @@ function TabsNavigator() {
function HomeTabNavigator() { function HomeTabNavigator() {
const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark) const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark)
return ( return (
<HomeTab.Navigator <HomeTab.Navigator
screenOptions={{ screenOptions={{

View File

@ -1,5 +1,6 @@
import {NavigationState, PartialState} from '@react-navigation/native' import {NavigationState, PartialState} from '@react-navigation/native'
import type {NativeStackNavigationProp} from '@react-navigation/native-stack' import type {NativeStackNavigationProp} from '@react-navigation/native-stack'
import {OnboardingScreenSteps} from 'state/models/discovery/onboarding'
export type {NativeStackScreenProps} from '@react-navigation/native-stack' export type {NativeStackScreenProps} from '@react-navigation/native-stack'
@ -29,6 +30,10 @@ export type CommonNavigatorParams = {
CopyrightPolicy: undefined CopyrightPolicy: undefined
AppPasswords: undefined AppPasswords: undefined
SavedFeeds: undefined SavedFeeds: undefined
} & OnboardingScreenParams
export type OnboardingScreenParams = {
[K in keyof typeof OnboardingScreenSteps]: undefined
} }
export type BottomTabNavigatorParams = CommonNavigatorParams & { export type BottomTabNavigatorParams = CommonNavigatorParams & {

View File

@ -3,19 +3,22 @@ import {RootStoreModel} from '../root-store'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {hasProp} from 'lib/type-guards' import {hasProp} from 'lib/type-guards'
enum OnboardingStep { export const OnboardingScreenSteps = {
WELCOME = 'WELCOME', Welcome: 'Welcome',
BROWSE_FEEDS = 'BROWSE_FEEDS', RecommendedFeeds: 'RecommendedFeeds',
COMPLETE = 'COMPLETE', Complete: 'Complete',
} } as const
type OnboardingStep =
(typeof OnboardingScreenSteps)[keyof typeof OnboardingScreenSteps]
const OnboardingStepsArray = Object.values(OnboardingScreenSteps)
export class OnboardingModel { export class OnboardingModel {
// state // state
step: OnboardingStep step: OnboardingStep
constructor(public rootStore: RootStoreModel) { constructor(public rootStore: RootStoreModel) {
makeAutoObservable(this, {rootStore: false}) makeAutoObservable(this, {rootStore: false})
this.step = OnboardingStep.WELCOME this.step = 'Welcome'
} }
serialize() { serialize() {
@ -26,26 +29,31 @@ export class OnboardingModel {
hydrate(v: unknown) { hydrate(v: unknown) {
if (typeof v === 'object' && v !== null) { if (typeof v === 'object' && v !== null) {
if (hasProp(v, 'step') && typeof v.step === 'string') { if (
hasProp(v, 'step') &&
typeof v.step === 'string' &&
OnboardingStepsArray.includes(v.step as OnboardingStep)
) {
this.step = v.step as OnboardingStep this.step = v.step as OnboardingStep
} }
} }
// if there is no valid state, we'll just reset
this.reset()
} }
nextStep(navigation?: NavigationProp) { nextScreenName() {
switch (this.step) { console.log('currentScreen', this.step)
case OnboardingStep.WELCOME: if (this.step === 'Welcome') {
this.step = OnboardingStep.COMPLETE this.step = 'RecommendedFeeds'
break return this.step
case OnboardingStep.BROWSE_FEEDS: } else if (this.step === 'RecommendedFeeds') {
this.step = OnboardingStep.COMPLETE this.step = 'Complete'
break return this.step
case OnboardingStep.COMPLETE: } else if (this.step === 'Complete') {
if (!navigation) { return 'Home'
throw new Error('Navigation prop required to complete onboarding') } else {
} // if we get here, we're in an invalid state, let's just go Home
this.complete(navigation) return 'Home'
break
} }
} }
@ -54,10 +62,14 @@ export class OnboardingModel {
} }
reset() { reset() {
this.step = OnboardingStep.WELCOME this.step = 'Welcome'
} }
get isComplete() { get isComplete() {
return this.step === OnboardingStep.COMPLETE return this.step === 'Complete'
}
get isRemaining() {
return !this.isComplete
} }
} }

View File

@ -1,66 +0,0 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {usePalette} from 'lib/hooks/usePalette'
import {Welcome} from './Welcome'
import {useStores} from 'state/index'
import {track} from 'lib/analytics/analytics'
enum OnboardingStep {
WELCOME = 'WELCOME',
// SELECT_INTERESTS = 'SELECT_INTERESTS',
COMPLETE = 'COMPLETE',
}
type OnboardingState = {
currentStep: OnboardingStep
}
type Action = {type: 'NEXT_STEP'}
const initialState: OnboardingState = {
currentStep: OnboardingStep.WELCOME,
}
const reducer = (state: OnboardingState, action: Action): OnboardingState => {
switch (action.type) {
case 'NEXT_STEP':
switch (state.currentStep) {
case OnboardingStep.WELCOME:
track('Onboarding:Begin')
return {...state, currentStep: OnboardingStep.COMPLETE}
case OnboardingStep.COMPLETE:
track('Onboarding:Complete')
return state
default:
return state
}
default:
return state
}
}
export const Onboarding = () => {
const pal = usePalette('default')
const rootStore = useStores()
const [state, dispatch] = React.useReducer(reducer, initialState)
const next = React.useCallback(
() => dispatch({type: 'NEXT_STEP'}),
[dispatch],
)
React.useEffect(() => {
if (state.currentStep === OnboardingStep.COMPLETE) {
// navigate to home
rootStore.shell.closeModal()
}
}, [state.currentStep, rootStore.shell])
return (
<View style={[pal.view, styles.container]}>
{state.currentStep === OnboardingStep.WELCOME && <Welcome next={next} />}
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 20,
},
})

View File

@ -0,0 +1,55 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {Text} from 'view/com/util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {Button} from 'view/com/util/forms/Button'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {HomeTabNavigatorParams} from 'lib/routes/types'
import {useStores} from 'state/index'
import {observer} from 'mobx-react-lite'
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'RecommendedFeeds'>
export const RecommendedFeeds = observer(({navigation}: Props) => {
const pal = usePalette('default')
const store = useStores()
const next = () => {
const nextScreenName = store.onboarding.nextScreenName()
console.log('nextScreenName', store.onboarding.nextScreenName())
if (nextScreenName) {
navigation.navigate(nextScreenName)
}
}
return (
<View style={[styles.container]}>
<View testID="recommendedFeedsScreen">
<Text type="lg-bold" style={[pal.text]}>
Check out some recommended feeds. Click + to add them to your list of
pinned feeds.
</Text>
</View>
<Button
onPress={next}
label="Continue"
testID="continueBtn"
labelStyle={styles.buttonText}
/>
</View>
)
})
const styles = StyleSheet.create({
container: {
flex: 1,
marginVertical: 60,
marginHorizontal: 16,
justifyContent: 'space-between',
},
buttonText: {
textAlign: 'center',
fontSize: 18,
marginVertical: 4,
},
})

View File

@ -5,9 +5,23 @@ import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Button} from 'view/com/util/forms/Button' import {Button} from 'view/com/util/forms/Button'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {HomeTabNavigatorParams} from 'lib/routes/types'
import {useStores} from 'state/index'
import {observer} from 'mobx-react-lite'
export const Welcome = ({next}: {next: () => void}) => { type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Welcome'>
export const Welcome = observer(({navigation}: Props) => {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores()
const next = () => {
const nextScreenName = store.onboarding.nextScreenName()
if (nextScreenName) {
navigation.navigate(nextScreenName)
}
}
return ( return (
<View style={[styles.container]}> <View style={[styles.container]}>
<View testID="welcomeScreen"> <View testID="welcomeScreen">
@ -60,12 +74,13 @@ export const Welcome = ({next}: {next: () => void}) => {
/> />
</View> </View>
) )
} })
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
marginVertical: 60, marginVertical: 60,
marginHorizontal: 16,
justifyContent: 'space-between', justifyContent: 'space-between',
}, },
title: { title: {

View File

@ -29,7 +29,6 @@ import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
import * as PreferencesHomeFeed from './PreferencesHomeFeed' import * as PreferencesHomeFeed from './PreferencesHomeFeed'
import * as OnboardingModal from './OnboardingModal'
import * as ModerationDetailsModal from './ModerationDetails' import * as ModerationDetailsModal from './ModerationDetails'
const DEFAULT_SNAPPOINTS = ['90%'] const DEFAULT_SNAPPOINTS = ['90%']
@ -134,9 +133,6 @@ export const ModalsContainer = observer(function ModalsContainer() {
} else if (activeModal?.name === 'preferences-home-feed') { } else if (activeModal?.name === 'preferences-home-feed') {
snapPoints = PreferencesHomeFeed.snapPoints snapPoints = PreferencesHomeFeed.snapPoints
element = <PreferencesHomeFeed.Component /> element = <PreferencesHomeFeed.Component />
} else if (activeModal?.name === 'onboarding') {
snapPoints = OnboardingModal.snapPoints
element = <OnboardingModal.Component />
} else if (activeModal?.name === 'moderation-details') { } else if (activeModal?.name === 'moderation-details') {
snapPoints = ModerationDetailsModal.snapPoints snapPoints = ModerationDetailsModal.snapPoints
element = <ModerationDetailsModal.Component {...activeModal} /> element = <ModerationDetailsModal.Component {...activeModal} />

View File

@ -26,7 +26,6 @@ import * as AddAppPassword from './AddAppPasswords'
import * as ContentFilteringSettingsModal from './ContentFilteringSettings' import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
import * as OnboardingModal from './OnboardingModal'
import * as ModerationDetailsModal from './ModerationDetails' import * as ModerationDetailsModal from './ModerationDetails'
import * as PreferencesHomeFeed from './PreferencesHomeFeed' import * as PreferencesHomeFeed from './PreferencesHomeFeed'
@ -109,8 +108,6 @@ function Modal({modal}: {modal: ModalIface}) {
element = <EditImageModal.Component {...modal} /> element = <EditImageModal.Component {...modal} />
} else if (modal.name === 'preferences-home-feed') { } else if (modal.name === 'preferences-home-feed') {
element = <PreferencesHomeFeed.Component /> element = <PreferencesHomeFeed.Component />
} else if (modal.name === 'onboarding') {
element = <OnboardingModal.Component />
} else if (modal.name === 'moderation-details') { } else if (modal.name === 'moderation-details') {
element = <ModerationDetailsModal.Component {...modal} /> element = <ModerationDetailsModal.Component {...modal} />
} else { } else {

View File

@ -1,8 +0,0 @@
import React from 'react'
import {Onboarding} from '../auth/onboarding/Onboarding'
export const snapPoints = ['90%']
export function Component() {
return <Onboarding />
}

View File

@ -31,7 +31,7 @@ const POLL_FREQ = 30e3 // 30sec
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
export const HomeScreen = withAuthRequired( export const HomeScreen = withAuthRequired(
observer((_opts: Props) => { observer(({navigation}: Props) => {
const store = useStores() const store = useStores()
const pagerRef = React.useRef<PagerRef>(null) const pagerRef = React.useRef<PagerRef>(null)
const [selectedPage, setSelectedPage] = React.useState(0) const [selectedPage, setSelectedPage] = React.useState(0)
@ -40,6 +40,12 @@ export const HomeScreen = withAuthRequired(
string[] string[]
>([]) >([])
React.useEffect(() => {
if (store.onboarding.isRemaining) {
navigation.navigate('Welcome')
}
}, [store.onboarding.isRemaining, navigation])
React.useEffect(() => { React.useEffect(() => {
const {pinned} = store.me.savedFeeds const {pinned} = store.me.savedFeeds

View File

@ -162,6 +162,11 @@ export const SettingsScreen = withAuthRequired(
Toast.show('Preferences reset') Toast.show('Preferences reset')
}, [store]) }, [store])
const onPressResetOnboarding = React.useCallback(async () => {
store.onboarding.reset()
Toast.show('Onboarding reset')
}, [store])
const onPressBuildInfo = React.useCallback(() => { const onPressBuildInfo = React.useCallback(() => {
Clipboard.setString( Clipboard.setString(
`Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`, `Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`,
@ -535,6 +540,16 @@ export const SettingsScreen = withAuthRequired(
Reset preferences state Reset preferences state
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity
style={[pal.view, styles.linkCardNoIcon]}
onPress={onPressResetOnboarding}
accessibilityRole="button"
accessibilityHint="Reset onboarding"
accessibilityLabel="Resets the onboarding state">
<Text type="lg" style={pal.text}>
Reset onboarding state
</Text>
</TouchableOpacity>
</> </>
) : null} ) : null}
<View style={[styles.footer]}> <View style={[styles.footer]}>