gorhom / react-native-bottom-sheet

A performant interactive bottom sheet with fully configurable options 🚀
https://gorhom.dev/react-native-bottom-sheet/
MIT License
6.97k stars 765 forks source link

Bottom sheet modal doesn't open in production #94

Closed maxckelly closed 3 years ago

maxckelly commented 3 years ago

Bug

Thanks for taking the time to look at this.

In production it seems that the bottom modal sheet flicks and doesn't open. If you look at my video attached you will see that the modal flickers slightly down the bottom sometimes it opens fully for a brief half second then closes. I've checked if the dismiss call is firing on open which it's not.

Any help would be greatly appreciated.

UPDATE: I'm now calling present() in a useFocusHook and the modal is still flickering down the bottom. It appears half way up the screen for half a second then flickers and goes away.

Image from iOS

Environment info

iOS - Test Flight

Library Version
@gorhom/bottom-sheet ^1.4.1
react-native 0.62.2
react-native-reanimated ^1.9.0
react-native-gesture-handler ^1.6.1

Steps To Reproduce

1.Press on button to trigger present() Describe what you expected to happen: 1.Modal to open

Reproducible sample code

const SearchScreen = ({ navigation, userReducer, allCollectionsReducer, addBookMarkedStory, removeBookMarkedStory }) => {

  const alertContext = useAlertContext();
  const { present, dismiss } = useBottomSheetModal();

  // State
  const [search, setSearch] = useState("");
  const [recentSearchHistory, setRecentSearchHistory] = useState([]);
  const [storyResults, setStoryResults] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  // bottomSheet
  const contentSheetRef = null;

  // Makes the search full page modal appear 
  const handlePresentPress = useCallback(() => {
    present(
        <SearchModalComponent 
          updateSearch={(search) => updateSearch(search)} 
          onTagSelect={(tag, id) => (updateSearch(tag.title, id), handleDismissPress())}
          onSubmit={(search) => (saveRecentSearchToStorage(search), updateSearch(search), handleDismissPress())}
          onRecentSearchSelect={(recentSearch) => (updateSearch(recentSearch), handleDismissPress())}
          search={search} 
          recentSearchHistory={recentSearchHistory}
          handleOnDismiss={() => handleDismissPress()}
        />,
      {
        snapPoints: ['95%', '95%'], 
        dismissOnOverlayPress: false,
        dismissOnScrollDown: false
      },
    );
  }, [present, search, recentSearchHistory]);

  const handleDismissPress = useCallback(() => {
    Alert.alert('IT HAS BEEN CALLED')
    dismiss();
  }, [dismiss]);

  const updateSearch = async (search, id) => {
    setIsLoading(true);

    // Sets the search to update
    setSearch(search);

    // Calls API
    const response = await getSearchResults(search, id);

    if (response.status === 200) {
      // If no data returned still ensure that the states are empty arrays
      setStoryResults(!response.data ? [] : response.data.stories);
      return setIsLoading(false);
    } else {
      console.log('error searching')
      return setIsLoading(false);
    }
  };

  // When the user is viewing the screen for the first time this will fire.
  const userTutorial = async () => {
    const screenTutorial = await getToken(SEARCH_SCREEN_TUTORIAL);
    if (screenTutorial === null) {
      storeToken(SEARCH_SCREEN_TUTORIAL, 'complete');
      // If user is not logged in and users first open of app display this pop-up
      return alertContext.alert({
        title: "Welcome to the search page!",
        body: "You can search for users and stories here",
        display: 'modal',
        theme: 'modalWelcomeTheme',
        iconName: 'search',
        iconColor: COLOR.limeGreen,
        iconSize: ICON_SIZE.iconSizeXXLarge,
      });
    } else {
      return;
    }
  };

  // Save search to local storage 
  const saveRecentSearchToStorage = async (search) => {
    if (recentSearchHistory.length === 3) {
      recentSearchHistory.unshift({text: search});
      recentSearchHistory.pop();
    } else { 
      recentSearchHistory.unshift({text: search});
    }

    await storeToken(RECENT_SEARCH_HISTORY_IDENTIFIER, JSON.stringify(recentSearchHistory))
  };

  // Recovers recent search history from local storage
  const recoverRecentSearchHistory = async () => {
    const data = await getToken(RECENT_SEARCH_HISTORY_IDENTIFIER);
    setRecentSearchHistory(data ? JSON.parse(data) : []);
  };

  useFocusEffect(
    useCallback(() => {
      recoverRecentSearchHistory();
      userTutorial();
    }, []),
  );

  // Only sets the search with all stories on first load. If user searches and clears search it will return all stories
  useEffect(() => {
    setStoryResults(allCollectionsReducer.stories);
  }, [allCollectionsReducer.stories])

  // When the user clicks on story card
  const onStoryPress = (storyID) => {
    const userID = userReducer.id

    // Navigates to the Screens navigator then storyStack then to view Story
    return navigation.navigate(SCREENS_NAVIGATOR, {
      screen: STORY_SCREEN_STACK,
      params: {
        screen: VIEW_STORY_SCREEN,
        params: { storyID, userID }
      },
    });
  };

  // Handle when user clicks on bookmark button
  const onBookmarkPress = async (hasUserBookMarkedStory, storyID) => {

    // If user is not logged in
    if (!userReducer.id) {
      return navigation.navigate(AUTH_NAVIGATOR, {
        screen: AUTH_SCREEN_STACK,
        params: {
          screen: LOGIN_SCREEN
        }
      });
    };

    // If already bookmarked 
    if (hasUserBookMarkedStory) {
      const response = await unBookMarkStory(storyID, userReducer.id);
      response.status === 200 ? removeBookMarkedStory(storyID) : console.log("ERROR");
    } else {
      const response = await bookMarkStory(storyID, userReducer.id);
      response.status === 200 ? addBookMarkedStory(storyID) : console.log("ERROR");
    };
  };

  // Renders story cards
  const renderResults = ({item}) => {
    const hasUserLikedStory = userReducer.likedStories ? userReducer.likedStories.includes(item.id) : false;
    const hasUserBookMarkedStory = userReducer.bookMarks ? userReducer.bookMarks.includes(item.id) : false;

    return (
      <TouchableOpacity onPress={() => onStoryPress(item.id)} style={styles.card}>
        <StoryCardComponent
          title={item.title}
          description={item.description}
          tags={item.tags}
          avatarURL={item.interviewer.avatarURL}
          likes={item.likes}
          hasUserLikedStory={hasUserLikedStory}
          hasUserBookMarkedStory={hasUserBookMarkedStory}
          onBookMarkPress={() => onBookmarkPress(hasUserBookMarkedStory, item.id)}
        />
      </TouchableOpacity>
    );
  };

  return (
    <View style={styles.container}>
      <BottomSheetSearchComponent 
        onSearchButtonPress={() => handlePresentPress()}
        myRef={contentSheetRef}
        listData={[
          {title: 'Stories', data: storyResults}
        ]}
        renderList={renderResults}
        extraData={allCollectionsReducer}
        userReducer={userReducer}
        searchInput={search}
        isLoading={isLoading}
      />
    </View>
  );
};
const SearchModalComponent = ({recentSearchHistory: _recentSearchHistory, search: _search, updateSearch, onSubmit, onTagSelect, handleOnDismiss, onRecentSearchSelect, storyReducer}) => {

  const [search, setSearch] = useState(_search);
  const [recentSearchHistory, setRecentSearchHistory] = useState(_recentSearchHistory);
  const mockArray = [];

  const handleInputChange = useCallback((search) => {
    setSearch(search);
    return updateSearch(search);
  }, [updateSearch]);

  // This displays the tags in the search modal
  const displayTags = () => {
    const tag = storyReducer.allTags.map((tag) => {
      return (
        <TagComponent 
          key={tag.id}
          tag={tag}
          onSelectedTag={(tag) => onTagSelect(tag, tag.id)}
        />
      )
    });

    return tag;
  };

  const mockLoadingTags = () => {
    for (let i = 0; i < 10; i++) {
      mockArray.push(i);
    };
  };

  const displayRecentSearch = () => {
    const recentSearch = recentSearchHistory.map((recent, index) => {
      return (
        <View style={styles.recentSearchComponentContainer} key={index}>
          <RecentSearchComponent 
            title={recent.text}
            onSelect={(title) => onRecentSearchSelect(title)}
          />
        </View>
      )
    });

    return recentSearch;
  };

  return (
    <View style={styles.container}>
      <ScrollView style={styles.contentContainer} keyboardShouldPersistTaps={'handled'} showsVerticalScrollIndicator={false}>

        <TouchableOpacity onPress={() => handleOnDismiss()} style={styles.masterHeaderContainer}>
          <IconComponent
            name="chevron-left"
            type="font-awesome-5"
            size={ICON_SIZE.iconSizeSmall}
            color={COLOR.grey}
            style={styles.iconStyle}
          />
          <Text style={styles.masterHeader}>Search</Text>
        </TouchableOpacity>

        <TextInputComponent 
          onChange={(search) => handleInputChange(search)}
          onSubmitEditing={(search) => onSubmit(search)}
          containerStyle={styles.searchBarContainerStyle}
          iconName="search"
          iconType="font-awesome"
          inputContainerStyle={styles.inputContainerStyle}
          placeholder="Search for stories here"
          returnKeyType="search"
          clearTextOnFocus={true}
          enablesReturnKeyAutomatically={true}
        />
        <Text style={styles.header}>Search by recent</Text>
        <View style={styles.recentSearchContainer}>
          {displayRecentSearch()}
        </View>

        <Text style={styles.header}>Search by tags</Text>
        <View style={styles.tagContainer}>
          {storyReducer.allTags === null ? (
            mockLoadingTags(),
            mockArray.map((el, i) => {
              return <SkeletonTagComponent key={i} />
            })
          ) : (
            displayTags()
          )}
        </View>
      </ScrollView>
    </View>
  )
};
MarcusOy commented 3 years ago

I was getting this bug as well on any iOS simulator and a physical iPhone 6. Was using present() hook inside a <BottomSheetModalProvider/>. Unfortunately, I've been ignoring the issue and continued development using the Android simulator.

gorhom commented 3 years ago

thanks @maxckelly for submitting this issue, could you provide a reproducible sample code that can run directly from /example, thanks

MarcusOy commented 3 years ago

Hey @gorhom , I'm not the OP, but since I've been experiencing this same issue, I've created a stripped down version of my app for you to take a look at here. I know its not something that runs from /example, which works fine on my end, but maybe there is a problem relating with Expo.

Quick instructions: yarn install and expo start, then press either a to launch Android or i to launch iOS.

Here's two GIFs of what I'm seeing from both platforms:

Android: Working as intended. BottomSheetAndroid

iOS: Not working as intended. BottomSheetiOS

Edit: Let me know if this is a different bug, since the setup is a bit different, so that I can open up a new issue.

maxckelly commented 3 years ago

Thanks for the response @gorhom - Planning on getting to this over the weekend. :)

gorhom commented 3 years ago

hi @maxckelly, @MarcusOy I just released Alpha 5, it should fix this issue, please try it and let me know ,, thanks

yarn add @gorhom/bottom-sheet@2.0.0-alpha.5

also check out the new documents website ( still in progress ) 🎉

Andrey123815 commented 8 months ago

@gorhom Hi! I found problem after updating to the Expo SDK 49 Modal windows began to work chaotically: on ios it opens perfectly both in dev mode and in prod On Android, they work perfectly in dev mode and absolutely do not work in production mode. I will present the screen recordings below Version of "@gorhom/bottom-sheet": "^4.4.5" DEV: dev PROD: prod

Apparently, this is a bug of the library itself when building with this version of the SDK. I also upgraded to sdk version 50, but that didn't help either.

Judging by the articles on the Internet, many have already encountered such a problem and recommended using AccessibilityInfo.isReduceMotionEnabled() from 'react-native' or useReducedMotion() from 'react-native-reanimated' and using the result in BottomSheetModal prop animateOnMount. So far, this method has not been tested

Also found one intersting bug, in my opinion, the reason is in the library This effect found only on andoid, on ios it work absolutely correct

When the bottomSheetModal is maximized (without reducing it by touching a finger), the content in the form of svg icons is displayed incorrectly (the image is distorted as seen in the screenshot). However, when I use my finger to slightly reduce the size of the window, starting to minimize it, the svg begins to display clearly, as it should be.

Display when the window is minimized by finger:

correct_svg

Display without user action bug_svg

CostasCF commented 7 months ago

@Andrey123815 Hey! Did you find any work around? I am facing the same problem, mostly on android devices where the bottom sheet fails to open at specified snap points.

Andrey123815 commented 7 months ago

@Andrey123815 Hey! Did you find any work around? I am facing the same problem, mostly on android devices where the bottom sheet fails to open at specified snap points.

Unfortunately no. So far, I have not found confirmed problem solution. Hope library author can fix this in next releases or reply us)

vendramini commented 3 weeks ago

I've noticed that if you have only one snapPoint it gets randomly closed on Android (emulator and device). To fix that, instead of const snapPoints = ['90%']; I've changed to const snapPoints = ['90%', '90%'];. Unsure if it's related tho.