Fix: onboarding carousel without screen jumpiness (#2825)

* Don't check for prev crashes or sentinel files on a fresh install

* Make sure we're not accidentally creating a new legacy store on every install

* Revert

* Add splash screen, preload images, show onboarding as react nav modal

* Fix e2e tests
This commit is contained in:
Amanda Bullington
2025-04-08 12:45:45 -07:00
committed by GitHub
parent 6dabe7ba5f
commit fc69a5a456
6 changed files with 140 additions and 84 deletions

View File

@@ -7,12 +7,6 @@ import {
const VISIBILITY_TIMEOUT = 10_000;
export default async function closeOnboarding( ) {
const loginText = element( by.id( "use-iNaturalist-intro-text" ) );
await waitFor( loginText ).toExist().withTimeout( VISIBILITY_TIMEOUT );
// If we can see MyObs, we don't need to close the onboarding
if ( loginText.visible ) {
return Promise.resolve( );
}
const closeOnboardingButton = element(
by.label( "Close" ).withAncestor( by.id( "OnboardingCarousel" ) )
);

View File

@@ -1,7 +1,6 @@
// @flow
import { useNavigation } from "@react-navigation/native";
import AddObsModal from "components/AddObsModal/AddObsModal.tsx";
import OnboardingCarouselModal from "components/Onboarding/OnboardingCarouselModal";
import {
HeaderUser,
Heading2,
@@ -14,7 +13,6 @@ import Arrow from "images/svg/curved_arrow_down.svg";
import type { Node } from "react";
import React, { useState } from "react";
import { Pressable } from "react-native";
import { useOnboardingShown } from "sharedHelpers/installData.ts";
import { useTranslation } from "sharedHooks";
import useStore from "stores/useStore";
@@ -26,7 +24,6 @@ interface Props {
const MyObservationsEmptySimple = ( { currentUser, isConnected }: Props ): Node => {
const { t } = useTranslation();
const navigation = useNavigation();
const [onboardingShown, setOnboardingShown] = useOnboardingShown();
const [showModal, setShowModal] = useState( false );
const resetObservationFlowSlice = useStore( state => state.resetObservationFlowSlice );
const navAndCloseModal = ( screen, params ) => {
@@ -43,10 +40,6 @@ const MyObservationsEmptySimple = ( { currentUser, isConnected }: Props ): Node
return (
<ViewWrapper>
<OnboardingCarouselModal
showModal={!onboardingShown}
closeModal={() => setOnboardingShown( true )}
/>
{!!currentUser && (
<View className="flex-start ml-[18px] mt-[26px]">
<HeaderUser user={currentUser} isConnected={isConnected} />

View File

@@ -7,10 +7,13 @@ import {
INatIconButton,
ViewWrapper
} from "components/SharedComponents";
import { ImageBackground } from "components/styledComponents";
import INatLogo from "images/svg/inat_logo_onboarding.svg";
import OnBoardingIcon2 from "images/svg/onboarding_icon_2.svg";
import OnBoardingIcon3 from "images/svg/onboarding_icon_3.svg";
import React, {
useEffect,
useMemo,
useRef,
useState
} from "react";
@@ -24,6 +27,7 @@ import {
import AnimatedDotsCarousel from "react-native-animated-dots-carousel";
import Animated, { interpolate, useAnimatedStyle, useSharedValue } from "react-native-reanimated";
import Carousel from "react-native-reanimated-carousel";
import { useOnboardingShown } from "sharedHelpers/installData.ts";
import colors from "styles/tailwindColors";
const SlideItem = props => {
@@ -60,29 +64,54 @@ const SlideItem = props => {
);
};
const OnboardingCarousel = ( { closeModal } ) => {
const OnboardingCarousel = ( ) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [onboardingShown, setOnboardingShown] = useOnboardingShown();
const { width } = useWindowDimensions();
const { t } = useTranslation( );
const carouselRef = useRef( null );
const progress = useSharedValue( 0 );
const [currentIndex, setCurrentIndex] = useState( 0 );
const [imagesLoaded, setImagesLoaded] = useState( false );
const closeModal = () => setOnboardingShown( true );
const paginationColor = colors.white;
const ONBOARDING_SLIDES = [
const backgroundAnimation1 = useAnimatedStyle( () => {
const opacity = interpolate(
progress.value,
[-1, 0, 1], // Fade in/out around current index
[0, 1, 0] // Opacity transitions
);
return { opacity };
} );
const backgroundAnimation2 = useAnimatedStyle( () => {
const opacity = interpolate(
progress.value,
[0, 1, 2], // Fade in/out around current index
[0, 1, 0] // Opacity transitions
);
return { opacity };
} );
const backgroundAnimation3 = useAnimatedStyle( () => {
const opacity = interpolate(
progress.value,
[1, 2, 3], // Fade in/out around current index
[0, 1, 0] // Opacity transitions
);
return { opacity };
} );
const ONBOARDING_SLIDES = useMemo( ( ) => ( [
{
icon: null,
iconProps: { width: 70, height: 70 },
title: t( "Identify-species-anywhere" ),
text: t( "Get-an-instant-ID-of-any-plant-animal-fungus" ),
background: require( "images/background/karsten-winegeart-RAgWH6ldps0-unsplash-cropped.jpg" ),
backgroundAnimation: useAnimatedStyle( () => {
const opacity = interpolate(
progress.value,
[-1, 0, 1], // Fade in/out around current index
[0, 1, 0] // Opacity transitions
);
return { opacity };
} )
backgroundAnimation: backgroundAnimation1
},
{
icon: OnBoardingIcon2,
@@ -90,14 +119,7 @@ const OnboardingCarousel = ( { closeModal } ) => {
title: t( "Connect-with-expert-naturalists" ),
text: t( "Experts-help-verify-and-improve-IDs" ),
background: require( "images/background/shane-rounce-DNkoNXQti3c-unsplash.jpg" ),
backgroundAnimation: useAnimatedStyle( () => {
const opacity = interpolate(
progress.value,
[0, 1, 2], // Fade in/out around current index
[0, 1, 0] // Opacity transitions
);
return { opacity };
} )
backgroundAnimation: backgroundAnimation2
},
{
icon: OnBoardingIcon3,
@@ -105,16 +127,9 @@ const OnboardingCarousel = ( { closeModal } ) => {
title: t( "Help-protect-species" ),
text: t( "Verified-IDs-are-used-for-science-and-conservation" ),
background: require( "images/background/sk-yeong-cXpdNdqp7eY-unsplash.jpg" ),
backgroundAnimation: useAnimatedStyle( () => {
const opacity = interpolate(
progress.value,
[1, 2, 3], // Fade in/out around current index
[0, 1, 0] // Opacity transitions
);
return { opacity };
} )
backgroundAnimation: backgroundAnimation3
}
];
] ), [backgroundAnimation1, backgroundAnimation2, backgroundAnimation3, t] );
const renderItem = ( { style, index, item } ) => (
<SlideItem
@@ -125,6 +140,44 @@ const OnboardingCarousel = ( { closeModal } ) => {
/>
);
const totalImages = ONBOARDING_SLIDES.length;
// Preload images; show splash screen in meantime
useEffect( () => {
const imageSources = ONBOARDING_SLIDES.map( slide => slide.background );
let loadedCount = 0;
// Preload each image
imageSources.forEach( source => {
Image.prefetch( Image.resolveAssetSource( source ).uri )
.then( ( ) => {
loadedCount += 1;
if ( loadedCount === totalImages ) {
setTimeout( ( ) => {
setImagesLoaded( true );
}, 500 );
}
} )
.catch( error => console.error( "Error loading image:", error ) );
} );
}, [ONBOARDING_SLIDES, totalImages] );
if ( !imagesLoaded ) {
return (
<ImageBackground
source={require( "images/background/daniel-olah-YNUFtf4qyh0-unsplash.jpg" )}
className="flex-1 items-center justify-center"
>
<INatIcon
name="inaturalist"
size={130}
color={colors.white}
/>
</ImageBackground>
);
}
return (
<ViewWrapper wrapperClassName="bg-black">
<StatusBar barStyle="light-content" backgroundColor="black" />

View File

@@ -1,23 +0,0 @@
import OnboardingCarousel from "components/Onboarding/OnboardingCarousel";
import Modal from "components/SharedComponents/Modal.tsx";
import React from "react";
const OnboardingCarouselModal = ( {
showModal,
closeModal
} ) => (
<Modal
showModal={showModal}
fullScreen
closeModal={closeModal}
disableSwipeDirection
noAnimation
modal={(
<OnboardingCarousel
closeModal={closeModal}
/>
)}
/>
);
export default OnboardingCarouselModal;

View File

@@ -0,0 +1,23 @@
// @flow
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import OnboardingCarousel from "components/Onboarding/OnboardingCarousel";
import type { Node } from "react";
import React from "react";
const Stack = createNativeStackNavigator( );
const OnboardingStackNavigator = ( ): Node => (
<Stack.Navigator>
<Stack.Screen
name="Onboarding"
component={OnboardingCarousel}
options={{
headerShown: false,
presentation: "modal",
contentStyle: { backgroundColor: "transparent" }
}}
/>
</Stack.Navigator>
);
export default OnboardingStackNavigator;

View File

@@ -6,8 +6,10 @@ import {
} from "navigation/navigationOptions";
import LoginStackNavigator from "navigation/StackNavigators/LoginStackNavigator";
import NoBottomTabStackNavigator from "navigation/StackNavigators/NoBottomTabStackNavigator";
import OnboardingStackNavigator from "navigation/StackNavigators/OnboardingStackNavigator";
import type { Node } from "react";
import * as React from "react";
import { useOnboardingShown } from "sharedHelpers/installData.ts";
import BottomTabNavigator from "./BottomTabNavigator";
import CustomDrawerContent from "./CustomDrawerContent";
@@ -33,27 +35,41 @@ const drawerRenderer = ( { state, navigation, descriptors } ) => (
);
// DEVELOPERS: do you need to add any screens here? All the rest of our screens live in
// NoBottomTabStackNavigator, TabStackNavigator, or LoginStackNavigator
// NoBottomTabStackNavigator, TabStackNavigator, OnboardingStackNavigator, or LoginStackNavigator
const RootDrawerNavigator = ( ): Node => (
<Drawer.Navigator
screenOptions={drawerOptions}
name="Drawer"
drawerContent={drawerRenderer}
>
<Drawer.Screen
name="TabNavigator"
component={BottomTabNavigator}
/>
<Drawer.Screen
name="NoBottomTabStackNavigator"
component={NoBottomTabStackNavigator}
/>
<Drawer.Screen
name="LoginStackNavigator"
component={LoginStackNavigator}
/>
</Drawer.Navigator>
);
const RootDrawerNavigator = ( ): Node => {
const [onboardingShown] = useOnboardingShown( );
return (
<Drawer.Navigator
screenOptions={drawerOptions}
name="Drawer"
drawerContent={drawerRenderer}
>
{!onboardingShown
? (
<Drawer.Screen
name="OnboardingStackNavigator"
component={OnboardingStackNavigator}
/>
)
: (
<Drawer.Screen
name="TabNavigator"
component={BottomTabNavigator}
/>
)}
<Drawer.Screen
name="NoBottomTabStackNavigator"
component={NoBottomTabStackNavigator}
/>
<Drawer.Screen
name="LoginStackNavigator"
component={LoginStackNavigator}
/>
</Drawer.Navigator>
);
};
export default RootDrawerNavigator;