From fc69a5a4563d88e8919d4ea61ca417f5dd9ce848 Mon Sep 17 00:00:00 2001 From: Amanda Bullington <35536439+albullington@users.noreply.github.com> Date: Tue, 8 Apr 2025 12:45:45 -0700 Subject: [PATCH] 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 --- e2e/sharedFlows/closeOnboarding.js | 6 - .../MyObservationsEmptySimple.js | 7 -- .../Onboarding/OnboardingCarousel.js | 107 +++++++++++++----- .../Onboarding/OnboardingCarouselModal.js | 23 ---- .../OnboardingStackNavigator.js | 23 ++++ src/navigation/rootDrawerNavigator.js | 58 ++++++---- 6 files changed, 140 insertions(+), 84 deletions(-) delete mode 100644 src/components/Onboarding/OnboardingCarouselModal.js create mode 100644 src/navigation/StackNavigators/OnboardingStackNavigator.js diff --git a/e2e/sharedFlows/closeOnboarding.js b/e2e/sharedFlows/closeOnboarding.js index 8cc393978..24a6da2b4 100644 --- a/e2e/sharedFlows/closeOnboarding.js +++ b/e2e/sharedFlows/closeOnboarding.js @@ -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" ) ) ); diff --git a/src/components/MyObservations/MyObservationsEmptySimple.js b/src/components/MyObservations/MyObservationsEmptySimple.js index 4c8779d2f..33cce11b2 100644 --- a/src/components/MyObservations/MyObservationsEmptySimple.js +++ b/src/components/MyObservations/MyObservationsEmptySimple.js @@ -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 ( - setOnboardingShown( true )} - /> {!!currentUser && ( diff --git a/src/components/Onboarding/OnboardingCarousel.js b/src/components/Onboarding/OnboardingCarousel.js index 10db586eb..4f8d10ccb 100644 --- a/src/components/Onboarding/OnboardingCarousel.js +++ b/src/components/Onboarding/OnboardingCarousel.js @@ -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 } ) => ( { /> ); + 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 ( + + + + ); + } + return ( diff --git a/src/components/Onboarding/OnboardingCarouselModal.js b/src/components/Onboarding/OnboardingCarouselModal.js deleted file mode 100644 index ac3474368..000000000 --- a/src/components/Onboarding/OnboardingCarouselModal.js +++ /dev/null @@ -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 -} ) => ( - - )} - /> -); - -export default OnboardingCarouselModal; diff --git a/src/navigation/StackNavigators/OnboardingStackNavigator.js b/src/navigation/StackNavigators/OnboardingStackNavigator.js new file mode 100644 index 000000000..830bf2113 --- /dev/null +++ b/src/navigation/StackNavigators/OnboardingStackNavigator.js @@ -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 => ( + + + +); + +export default OnboardingStackNavigator; diff --git a/src/navigation/rootDrawerNavigator.js b/src/navigation/rootDrawerNavigator.js index 420b88d34..91ce4a9d8 100644 --- a/src/navigation/rootDrawerNavigator.js +++ b/src/navigation/rootDrawerNavigator.js @@ -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 => ( - - - - - -); +const RootDrawerNavigator = ( ): Node => { + const [onboardingShown] = useOnboardingShown( ); + + return ( + + {!onboardingShown + ? ( + + ) + : ( + + + )} + + + + ); +}; export default RootDrawerNavigator;