Skip to content

Commit

Permalink
fix(expo-go): update switch between cng and go (#180 by @frankcalise)
Browse files Browse the repository at this point in the history
  • Loading branch information
frankcalise authored Nov 13, 2024
1 parent ceb925c commit d4f8203
Showing 1 changed file with 354 additions and 10 deletions.
364 changes: 354 additions & 10 deletions docs/recipes/SwitchBetweenExpoGoCNG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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 `<KeyboardProvider>` in `app/app.tsx`

```diff
-import { KeyboardProvider } from "react-native-keyboard-controller"

// ...

return (
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<ErrorBoundary catchErrors={Config.catchErrors}>
- <KeyboardProvider>
<AppNavigator
linking={linking}
initialState={initialNavigationState}
onStateChange={onNavigationStateChange}
/>
- </KeyboardProvider>
</ErrorBoundary>
</SafeAreaProvider>
)
```

3. Update `app/components/Screen.tsx`

<details>
<summary>Screen.tsx (expand to copy)</summary>

```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<ViewStyle>;
/**
* Style for the inner content container useful for padding & margin.
*/
contentContainerStyle?: StyleProp<ViewStyle>;
/**
* 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<ScrollScreenProps, "preset"> {
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 | number>(null);
const scrollViewContentHeight = useRef<null | number>(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 (
<View style={[$outerStyle, style]}>
<View style={[$innerStyle, contentContainerStyle]}>{children}</View>
</View>
);
}

/**
* @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<ScrollView>(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 (
<ScrollView
{...{ keyboardShouldPersistTaps, scrollEnabled, ref }}
{...ScrollViewProps}
onLayout={(e) => {
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}
</ScrollView>
);
}

/**
* 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 (
<View
style={[
$containerStyle,
{ backgroundColor: backgroundColor || colors.background },
$containerInsets,
]}
>
<StatusBar
style={statusBarStyle || (themeContext === "dark" ? "light" : "dark")}
{...StatusBarProps}
/>

<KeyboardAvoidingView
behavior={isIos ? "padding" : "height"}
keyboardVerticalOffset={keyboardOffset}
{...KeyboardAvoidingViewProps}
style={[$styles.flex1, KeyboardAvoidingViewProps?.style]}
>
{isNonScrolling(props.preset) ? (
<ScreenWithoutScrolling {...props} />
) : (
<ScreenWithScrolling {...props} />
)}
</KeyboardAvoidingView>
</View>
);
}

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",
};
```

</details>
<br />

#### 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.

0 comments on commit d4f8203

Please sign in to comment.