move onboarding to screens
parent
84e065667a
commit
edfd326069
|
@ -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={{
|
||||||
|
|
|
@ -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 & {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {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: {
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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'>
|
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
|
||||||
|
|
||||||
|
|
|
@ -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]}>
|
||||||
|
|
Loading…
Reference in New Issue