move onboarding to screens
parent
84e065667a
commit
edfd326069
|
@ -67,6 +67,8 @@ import {getRoutingInstrumentation} from 'lib/sentry'
|
|||
import {bskyTitle} from 'lib/strings/headings'
|
||||
import {JSX} from 'react/jsx-runtime'
|
||||
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>()
|
||||
|
||||
|
@ -219,6 +221,18 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
|||
component={SavedFeeds}
|
||||
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() {
|
||||
const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark)
|
||||
|
||||
return (
|
||||
<HomeTab.Navigator
|
||||
screenOptions={{
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {NavigationState, PartialState} from '@react-navigation/native'
|
||||
import type {NativeStackNavigationProp} from '@react-navigation/native-stack'
|
||||
import {OnboardingScreenSteps} from 'state/models/discovery/onboarding'
|
||||
|
||||
export type {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
|
@ -29,6 +30,10 @@ export type CommonNavigatorParams = {
|
|||
CopyrightPolicy: undefined
|
||||
AppPasswords: undefined
|
||||
SavedFeeds: undefined
|
||||
} & OnboardingScreenParams
|
||||
|
||||
export type OnboardingScreenParams = {
|
||||
[K in keyof typeof OnboardingScreenSteps]: undefined
|
||||
}
|
||||
|
||||
export type BottomTabNavigatorParams = CommonNavigatorParams & {
|
||||
|
|
|
@ -3,19 +3,22 @@ import {RootStoreModel} from '../root-store'
|
|||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {hasProp} from 'lib/type-guards'
|
||||
|
||||
enum OnboardingStep {
|
||||
WELCOME = 'WELCOME',
|
||||
BROWSE_FEEDS = 'BROWSE_FEEDS',
|
||||
COMPLETE = 'COMPLETE',
|
||||
}
|
||||
export const OnboardingScreenSteps = {
|
||||
Welcome: 'Welcome',
|
||||
RecommendedFeeds: 'RecommendedFeeds',
|
||||
Complete: 'Complete',
|
||||
} as const
|
||||
|
||||
type OnboardingStep =
|
||||
(typeof OnboardingScreenSteps)[keyof typeof OnboardingScreenSteps]
|
||||
const OnboardingStepsArray = Object.values(OnboardingScreenSteps)
|
||||
export class OnboardingModel {
|
||||
// state
|
||||
step: OnboardingStep
|
||||
|
||||
constructor(public rootStore: RootStoreModel) {
|
||||
makeAutoObservable(this, {rootStore: false})
|
||||
this.step = OnboardingStep.WELCOME
|
||||
this.step = 'Welcome'
|
||||
}
|
||||
|
||||
serialize() {
|
||||
|
@ -26,26 +29,31 @@ export class OnboardingModel {
|
|||
|
||||
hydrate(v: unknown) {
|
||||
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
|
||||
}
|
||||
}
|
||||
// if there is no valid state, we'll just reset
|
||||
this.reset()
|
||||
}
|
||||
|
||||
nextStep(navigation?: NavigationProp) {
|
||||
switch (this.step) {
|
||||
case OnboardingStep.WELCOME:
|
||||
this.step = OnboardingStep.COMPLETE
|
||||
break
|
||||
case OnboardingStep.BROWSE_FEEDS:
|
||||
this.step = OnboardingStep.COMPLETE
|
||||
break
|
||||
case OnboardingStep.COMPLETE:
|
||||
if (!navigation) {
|
||||
throw new Error('Navigation prop required to complete onboarding')
|
||||
}
|
||||
this.complete(navigation)
|
||||
break
|
||||
nextScreenName() {
|
||||
console.log('currentScreen', this.step)
|
||||
if (this.step === 'Welcome') {
|
||||
this.step = 'RecommendedFeeds'
|
||||
return this.step
|
||||
} else if (this.step === 'RecommendedFeeds') {
|
||||
this.step = 'Complete'
|
||||
return this.step
|
||||
} else if (this.step === 'Complete') {
|
||||
return 'Home'
|
||||
} else {
|
||||
// if we get here, we're in an invalid state, let's just go Home
|
||||
return 'Home'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,10 +62,14 @@ export class OnboardingModel {
|
|||
}
|
||||
|
||||
reset() {
|
||||
this.step = OnboardingStep.WELCOME
|
||||
this.step = 'Welcome'
|
||||
}
|
||||
|
||||
get isComplete() {
|
||||
return this.step === OnboardingStep.COMPLETE
|
||||
return this.step === 'Complete'
|
||||
}
|
||||
|
||||
get isRemaining() {
|
||||
return !this.isComplete
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
|
@ -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,
|
||||
},
|
||||
})
|
|
@ -5,9 +5,23 @@ import {s} from 'lib/styles'
|
|||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
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 store = useStores()
|
||||
|
||||
const next = () => {
|
||||
const nextScreenName = store.onboarding.nextScreenName()
|
||||
if (nextScreenName) {
|
||||
navigation.navigate(nextScreenName)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container]}>
|
||||
<View testID="welcomeScreen">
|
||||
|
@ -60,12 +74,13 @@ export const Welcome = ({next}: {next: () => void}) => {
|
|||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
marginVertical: 60,
|
||||
marginHorizontal: 16,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
title: {
|
||||
|
|
|
@ -29,7 +29,6 @@ import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
|||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||
import * as PreferencesHomeFeed from './PreferencesHomeFeed'
|
||||
import * as OnboardingModal from './OnboardingModal'
|
||||
import * as ModerationDetailsModal from './ModerationDetails'
|
||||
|
||||
const DEFAULT_SNAPPOINTS = ['90%']
|
||||
|
@ -134,9 +133,6 @@ export const ModalsContainer = observer(function ModalsContainer() {
|
|||
} else if (activeModal?.name === 'preferences-home-feed') {
|
||||
snapPoints = PreferencesHomeFeed.snapPoints
|
||||
element = <PreferencesHomeFeed.Component />
|
||||
} else if (activeModal?.name === 'onboarding') {
|
||||
snapPoints = OnboardingModal.snapPoints
|
||||
element = <OnboardingModal.Component />
|
||||
} else if (activeModal?.name === 'moderation-details') {
|
||||
snapPoints = ModerationDetailsModal.snapPoints
|
||||
element = <ModerationDetailsModal.Component {...activeModal} />
|
||||
|
|
|
@ -26,7 +26,6 @@ import * as AddAppPassword from './AddAppPasswords'
|
|||
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||
import * as OnboardingModal from './OnboardingModal'
|
||||
import * as ModerationDetailsModal from './ModerationDetails'
|
||||
|
||||
import * as PreferencesHomeFeed from './PreferencesHomeFeed'
|
||||
|
@ -109,8 +108,6 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||
element = <EditImageModal.Component {...modal} />
|
||||
} else if (modal.name === 'preferences-home-feed') {
|
||||
element = <PreferencesHomeFeed.Component />
|
||||
} else if (modal.name === 'onboarding') {
|
||||
element = <OnboardingModal.Component />
|
||||
} else if (modal.name === 'moderation-details') {
|
||||
element = <ModerationDetailsModal.Component {...modal} />
|
||||
} else {
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import React from 'react'
|
||||
import {Onboarding} from '../auth/onboarding/Onboarding'
|
||||
|
||||
export const snapPoints = ['90%']
|
||||
|
||||
export function Component() {
|
||||
return <Onboarding />
|
||||
}
|
|
@ -31,7 +31,7 @@ const POLL_FREQ = 30e3 // 30sec
|
|||
|
||||
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
|
||||
export const HomeScreen = withAuthRequired(
|
||||
observer((_opts: Props) => {
|
||||
observer(({navigation}: Props) => {
|
||||
const store = useStores()
|
||||
const pagerRef = React.useRef<PagerRef>(null)
|
||||
const [selectedPage, setSelectedPage] = React.useState(0)
|
||||
|
@ -40,6 +40,12 @@ export const HomeScreen = withAuthRequired(
|
|||
string[]
|
||||
>([])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (store.onboarding.isRemaining) {
|
||||
navigation.navigate('Welcome')
|
||||
}
|
||||
}, [store.onboarding.isRemaining, navigation])
|
||||
|
||||
React.useEffect(() => {
|
||||
const {pinned} = store.me.savedFeeds
|
||||
|
||||
|
|
|
@ -162,6 +162,11 @@ export const SettingsScreen = withAuthRequired(
|
|||
Toast.show('Preferences reset')
|
||||
}, [store])
|
||||
|
||||
const onPressResetOnboarding = React.useCallback(async () => {
|
||||
store.onboarding.reset()
|
||||
Toast.show('Onboarding reset')
|
||||
}, [store])
|
||||
|
||||
const onPressBuildInfo = React.useCallback(() => {
|
||||
Clipboard.setString(
|
||||
`Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`,
|
||||
|
@ -535,6 +540,16 @@ export const SettingsScreen = withAuthRequired(
|
|||
Reset preferences state
|
||||
</Text>
|
||||
</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}
|
||||
<View style={[styles.footer]}>
|
||||
|
|
Loading…
Reference in New Issue