Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(expo-go): update switch between cng and go #180

Merged
merged 1 commit into from
Nov 13, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Loading