From d4f8203c0e3472b64410a8f0fba4c6d921def2eb Mon Sep 17 00:00:00 2001 From: Frank Calise Date: Wed, 13 Nov 2024 10:49:49 -0500 Subject: [PATCH] fix(expo-go): update switch between cng and go (#180 by @frankcalise) --- docs/recipes/SwitchBetweenExpoGoCNG.md | 364 ++++++++++++++++++++++++- 1 file changed, 354 insertions(+), 10 deletions(-) diff --git a/docs/recipes/SwitchBetweenExpoGoCNG.md b/docs/recipes/SwitchBetweenExpoGoCNG.md index cd188d3c..43ee9f19 100644 --- a/docs/recipes/SwitchBetweenExpoGoCNG.md +++ b/docs/recipes/SwitchBetweenExpoGoCNG.md @@ -42,7 +42,7 @@ If you started with Expo CNG workflow, but your app isn't utilizing any custom n ### Steps -1. In `package.json`, modify scripts: +#### Update `package.json` scripts ```diff --"android": "npx expo run:android", @@ -51,15 +51,359 @@ If you started with Expo CNG workflow, but your app isn't utilizing any custom n ++"ios": "npx expo start --ios", ``` -2. Some libraries may need to be downgraded in order to be compatible with Expo Go. In `package.json`, you may need to downgrade dependencies so they do not exceed the version supported by Expo Go. +#### Remove native directories -```json -"@react-native-async-storage/async-storage": "1.18.2", -"@shopify/flash-list": "1.4.3", -"expo-application": "~5.3.0", -"expo-font": "~11.4.0", -"expo-localization": "~14.3.0", -"react-native": "0.72.6", +```bash +rm -rf android ios ``` -Note: View latest values in [Ignite - expoGoCompatibility.ts](https://github.com/infinitered/ignite/blob/6e8f84a786555504acc8751ceb617238f710bc26/src/tools/expoGoCompatibility.ts#L5C14-L5C42) +#### Remove`react-native-mmkv` in favor of `@react-native-async-storage/async-storage` + +1. Swap packages + +```bash +yarn remove react-native-mmkv +npx expo install @react-native-async-storage/async-storage +``` + +2. Update the storage util in `app/utils/storage.ts` + +#### Remove`react-native-keyboard-controller` + +1. Remove the package + +```bash +yarn remove react-native-keyboard-controller +``` + +2. Remove the `` in `app/app.tsx` + +```diff +-import { KeyboardProvider } from "react-native-keyboard-controller" + +// ... + +return ( + + +- + +- + + +) +``` + +3. Update `app/components/Screen.tsx` + +
+ Screen.tsx (expand to copy) + +```tsx title="/app/components/Screen.tsx" +import { useScrollToTop } from "@react-navigation/native"; +import { StatusBar, StatusBarProps, StatusBarStyle } from "expo-status-bar"; +import React, { useRef, useState } from "react"; +import { + KeyboardAvoidingView, + KeyboardAvoidingViewProps, + LayoutChangeEvent, + Platform, + ScrollView, + ScrollViewProps, + StyleProp, + View, + ViewStyle, +} from "react-native"; +import { $styles } from "@/theme"; +import { ExtendedEdge, useSafeAreaInsetsStyle } from "@/utils/useSafeAreaInsetsStyle"; +import { useAppTheme } from "@/utils/useAppTheme"; + +interface BaseScreenProps { + /** + * Children components. + */ + children?: React.ReactNode; + /** + * Style for the outer content container useful for padding & margin. + */ + style?: StyleProp; + /** + * Style for the inner content container useful for padding & margin. + */ + contentContainerStyle?: StyleProp; + /** + * Override the default edges for the safe area. + */ + safeAreaEdges?: ExtendedEdge[]; + /** + * Background color + */ + backgroundColor?: string; + /** + * Status bar setting. Defaults to dark. + */ + statusBarStyle?: StatusBarStyle; + /** + * By how much should we offset the keyboard? Defaults to 0. + */ + keyboardOffset?: number; + /** + * Pass any additional props directly to the StatusBar component. + */ + StatusBarProps?: StatusBarProps; + /** + * Pass any additional props directly to the KeyboardAvoidingView component. + */ + KeyboardAvoidingViewProps?: KeyboardAvoidingViewProps; +} + +interface FixedScreenProps extends BaseScreenProps { + preset?: "fixed"; +} +interface ScrollScreenProps extends BaseScreenProps { + preset?: "scroll"; + /** + * Should keyboard persist on screen tap. Defaults to handled. + * Only applies to scroll preset. + */ + keyboardShouldPersistTaps?: "handled" | "always" | "never"; + /** + * Pass any additional props directly to the ScrollView component. + */ + ScrollViewProps?: ScrollViewProps; +} + +interface AutoScreenProps extends Omit { + preset?: "auto"; + /** + * Threshold to trigger the automatic disabling/enabling of scroll ability. + * Defaults to `{ percent: 0.92 }`. + */ + scrollEnabledToggleThreshold?: { percent?: number; point?: number }; +} + +export type ScreenProps = ScrollScreenProps | FixedScreenProps | AutoScreenProps; + +const isIos = Platform.OS === "ios"; + +type ScreenPreset = "fixed" | "scroll" | "auto"; + +/** + * @param {ScreenPreset?} preset - The preset to check. + * @returns {boolean} - Whether the preset is non-scrolling. + */ +function isNonScrolling(preset?: ScreenPreset) { + return !preset || preset === "fixed"; +} + +/** + * Custom hook that handles the automatic enabling/disabling of scroll ability based on the content size and screen size. + * @param {UseAutoPresetProps} props - The props for the `useAutoPreset` hook. + * @returns {{boolean, Function, Function}} - The scroll state, and the `onContentSizeChange` and `onLayout` functions. + */ +function useAutoPreset(props: AutoScreenProps): { + scrollEnabled: boolean; + onContentSizeChange: (w: number, h: number) => void; + onLayout: (e: LayoutChangeEvent) => void; +} { + const { preset, scrollEnabledToggleThreshold } = props; + const { percent = 0.92, point = 0 } = scrollEnabledToggleThreshold || {}; + + const scrollViewHeight = useRef(null); + const scrollViewContentHeight = useRef(null); + const [scrollEnabled, setScrollEnabled] = useState(true); + + function updateScrollState() { + if (scrollViewHeight.current === null || scrollViewContentHeight.current === null) return; + + // check whether content fits the screen then toggle scroll state according to it + const contentFitsScreen = (function () { + if (point) { + return scrollViewContentHeight.current < scrollViewHeight.current - point; + } else { + return scrollViewContentHeight.current < scrollViewHeight.current * percent; + } + })(); + + // content is less than the size of the screen, so we can disable scrolling + if (scrollEnabled && contentFitsScreen) setScrollEnabled(false); + + // content is greater than the size of the screen, so let's enable scrolling + if (!scrollEnabled && !contentFitsScreen) setScrollEnabled(true); + } + + /** + * @param {number} w - The width of the content. + * @param {number} h - The height of the content. + */ + function onContentSizeChange(w: number, h: number) { + // update scroll-view content height + scrollViewContentHeight.current = h; + updateScrollState(); + } + + /** + * @param {LayoutChangeEvent} e = The layout change event. + */ + function onLayout(e: LayoutChangeEvent) { + const { height } = e.nativeEvent.layout; + // update scroll-view height + scrollViewHeight.current = height; + updateScrollState(); + } + + // update scroll state on every render + if (preset === "auto") updateScrollState(); + + return { + scrollEnabled: preset === "auto" ? scrollEnabled : true, + onContentSizeChange, + onLayout, + }; +} + +/** + * @param {ScreenProps} props - The props for the `ScreenWithoutScrolling` component. + * @returns {JSX.Element} - The rendered `ScreenWithoutScrolling` component. + */ +function ScreenWithoutScrolling(props: ScreenProps) { + const { style, contentContainerStyle, children } = props; + return ( + + {children} + + ); +} + +/** + * @param {ScreenProps} props - The props for the `ScreenWithScrolling` component. + * @returns {JSX.Element} - The rendered `ScreenWithScrolling` component. + */ +function ScreenWithScrolling(props: ScreenProps) { + const { + children, + keyboardShouldPersistTaps = "handled", + contentContainerStyle, + ScrollViewProps, + style, + } = props as ScrollScreenProps; + + const ref = useRef(null); + + const { scrollEnabled, onContentSizeChange, onLayout } = useAutoPreset(props as AutoScreenProps); + + // Add native behavior of pressing the active tab to scroll to the top of the content + // More info at: https://reactnavigation.org/docs/use-scroll-to-top/ + useScrollToTop(ref); + + return ( + { + onLayout(e); + ScrollViewProps?.onLayout?.(e); + }} + onContentSizeChange={(w: number, h: number) => { + onContentSizeChange(w, h); + ScrollViewProps?.onContentSizeChange?.(w, h); + }} + style={[$outerStyle, ScrollViewProps?.style, style]} + contentContainerStyle={[ + $innerStyle, + ScrollViewProps?.contentContainerStyle, + contentContainerStyle, + ]} + > + {children} + + ); +} + +/** + * Represents a screen component that provides a consistent layout and behavior for different screen presets. + * The `Screen` component can be used with different presets such as "fixed", "scroll", or "auto". + * It handles safe area insets, status bar settings, keyboard avoiding behavior, and scrollability based on the preset. + * @see [Documentation and Examples]{@link https://docs.infinite.red/ignite-cli/boilerplate/app/components/Screen/} + * @param {ScreenProps} props - The props for the `Screen` component. + * @returns {JSX.Element} The rendered `Screen` component. + */ +export function Screen(props: ScreenProps) { + const { + theme: { colors }, + themeContext, + } = useAppTheme(); + const { + backgroundColor, + KeyboardAvoidingViewProps, + keyboardOffset = 0, + safeAreaEdges, + StatusBarProps, + statusBarStyle, + } = props; + + const $containerInsets = useSafeAreaInsetsStyle(safeAreaEdges); + + return ( + + + + + {isNonScrolling(props.preset) ? ( + + ) : ( + + )} + + + ); +} + +const $containerStyle: ViewStyle = { + flex: 1, + height: "100%", + width: "100%", +}; + +const $outerStyle: ViewStyle = { + flex: 1, + height: "100%", + width: "100%", +}; + +const $innerStyle: ViewStyle = { + justifyContent: "flex-start", + alignItems: "stretch", +}; +``` + +
+
+ +#### Sync Expo packages to be compatible with Expo Go + +Running `npx expo install --check` will check all of the expo packages in their SDK against the version of `expo` that is installed to ensure compatibility. + +You can accept these changes or run `npx expo install --fix` to apply them directly without running the check. + +#### Run the app! + +That's it! You should be able to run `yarn start` and tap `i` or `a` in terminal to launch iOS or Android respectively in Expo Go.