Skip to content

Commit

Permalink
(feat) OTA with expo-updates (#183)
Browse files Browse the repository at this point in the history
  • Loading branch information
mazenchami authored Apr 28, 2023
1 parent 5f3252d commit f52f40c
Show file tree
Hide file tree
Showing 8 changed files with 2,404 additions and 2,436 deletions.
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,12 @@ _Flows currently run against an **iOS 15.5** simulator dev client_
## Notes

- Only make JavaScript changes OTA
- Any native code changes, app.config.ts or Expo SDK update will require a store publish
- Any native code changes, `app.config.ts` or Expo SDK update will require a store publish

## Steps

1. Make the necessary JavaScript changes
a. For a `preview` build, this can just be done locally (with uncommitted changes) or from another git branch
1. Make the necessary JavaScript changes<br/>
a. For a `preview` build, this can just be done locally (with uncommitted changes) or from another git branch<br/>
b. For a `production`, you'll likely be in some release candidate branch or `main` branch after things have been QA'd
2. Create the EAS update branch `eas update --branch [update branch name] --message "info about the update"`
- _This packages up the current project code and uploads it as a package to EAS as an update_
Expand All @@ -134,8 +134,7 @@ There is a `preview` build of version 1.0 out in the wild, lacking a new feature
git checkout -b feat/my-new-feature
# make some changes to code
git commit -a -m "feature complete"
eas update --branch preview-1.1 --message "added new feature"
eas channel:update preview --branch preview-1.1
eas update --branch preview --message "added new feature"
# from a device with the preview build, open the app to grab the update, and reload it to use the new feature
```

Expand Down
3 changes: 3 additions & 0 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
backgroundColor: "#081828",
},
updates: {
checkAutomatically: "ON_ERROR_RECOVERY",
enabled: true,
fallbackToCacheTimeout: 0,
url: "https://u.expo.dev/b72c79d7-7c87-4aa7-b964-998dcff69e07",
Expand Down Expand Up @@ -90,5 +91,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
"@react-native-firebase/app",
"@react-native-firebase/crashlytics",
["expo-build-properties", { ios: { useFrameworks: "static" } }],
["expo-updates", { username: "infinitered" }],
["expo-localization"],
],
})
62 changes: 61 additions & 1 deletion app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
import "./i18n"
import "./utils/ignoreWarnings"
import { useFonts } from "expo-font"
import React, { useLayoutEffect } from "react"
import React, { useLayoutEffect, useState } from "react"
import {
initialWindowMetrics,
SafeAreaProvider,
useSafeAreaInsets,
} from "react-native-safe-area-context"
import * as Updates from "expo-updates"
import { AppNavigator, useNavigationPersistence } from "./navigators"
import { ErrorBoundary } from "./screens/ErrorScreen/ErrorBoundary"
import * as storage from "./utils/storage"
Expand All @@ -30,6 +31,9 @@ import Toast, { BaseToast, ToastConfig } from "react-native-toast-message"
import { $baseSecondaryStyle, $baseStyle } from "./components"
import { Dimensions, ViewStyle } from "react-native"
import { queryClient } from "./services/api/react-query"
import { reportCrash } from "./utils/crashReporting"
import { useAppState } from "./hooks"
import { Modal } from "./components/Modal"

// Set up Reactotron, which is a free desktop app for inspecting and debugging
// React Native apps. Learn more here: https://github.com/infinitered/reactotron
Expand Down Expand Up @@ -87,6 +91,61 @@ const CustomToast = () => {
return <Toast config={toastConfig} topOffset={insets.top} />
}

// Setting up our OTA Updates component
const OTAUpdates = () => {
const [isModalVisible, setIsModalVisible] = useState(false)
const [isUpdating, setIsUpdating] = useState(false)

async function fetchAndRestartApp() {
const fetchUpdate = await Updates.fetchUpdateAsync()
if (fetchUpdate.isNew) {
await Updates.reloadAsync()
} else {
setIsModalVisible(false)
setIsUpdating(false)
reportCrash("Fetch Update failed")
}
}

async function onFetchUpdateAsync() {
if (__DEV__ || process.env.NODE_ENV === "development") return
try {
const update = await Updates.checkForUpdateAsync()
setIsModalVisible(update.isAvailable)
} catch (error) {
reportCrash(error)
}
}

useAppState({
match: /background/,
nextAppState: "active",
callback: onFetchUpdateAsync,
})

return (
<Modal
title="ota.title"
subtitle="ota.subtitle"
confirmOnPress={{
cta: async () => {
setIsUpdating(true)
await fetchAndRestartApp()
},
label: isUpdating ? "ota.confirmLabelUpdating" : "ota.confirmLabel",
disabled: isUpdating,
}}
cancelOnPress={{
cta: () => {
setIsModalVisible(false)
},
label: "ota.cancelLabel",
}}
isVisible={isModalVisible}
/>
)
}

/**
* This is the root component of our app.
*/
Expand Down Expand Up @@ -129,6 +188,7 @@ function App(props: AppProps) {
onStateChange={onNavigationStateChange}
/>
<CustomToast />
<OTAUpdates />
</QueryClientProvider>
</ErrorBoundary>
</SafeAreaProvider>
Expand Down
79 changes: 79 additions & 0 deletions app/components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from "react"
import { Modal as RNModal, TextStyle, View, ViewStyle } from "react-native"
import { TxKeyPath } from "../i18n"
import { colors, spacing } from "../theme"
import { Button, ButtonProps } from "./Button"
import { Text } from "./Text"

interface OnPressProps extends ButtonProps {
cta: () => void
label: TxKeyPath
}

interface ModalProps {
title: TxKeyPath
subtitle?: TxKeyPath
confirmOnPress: OnPressProps
cancelOnPress: OnPressProps
isVisible?: boolean
}

export const Modal = ({
title,
subtitle,
confirmOnPress,
cancelOnPress,
isVisible,
}: ModalProps) => (
<RNModal animationType="slide" transparent={true} visible={isVisible}>
<View style={$wrapper}>
<View style={$card}>
<Text preset="subheading" tx={title} style={$textColor} />
{subtitle ? <Text text="body" style={$subtitle} tx={subtitle} /> : null}
<Button
shadowStyle={$confirmButton}
onPress={confirmOnPress.cta}
tx={confirmOnPress.label}
/>
<Button
preset="link"
style={$cancelButton}
onPress={cancelOnPress.cta}
tx={cancelOnPress.label}
/>
</View>
</View>
</RNModal>
)

const $wrapper: ViewStyle = {
flex: 1,
alignItems: "center",
justifyContent: "center",
}

const $card: ViewStyle = {
backgroundColor: colors.palette.neutral100,
borderRadius: spacing.medium,
paddingHorizontal: spacing.large,
paddingVertical: spacing.extraLarge,
width: "80%",
}

const $textColor: TextStyle = {
color: colors.palette.neutral800,
}

const $subtitle: TextStyle = {
...$textColor,
marginTop: spacing.medium,
}

const $confirmButton: ViewStyle = {
marginTop: spacing.large,
}

const $cancelButton: ViewStyle = {
alignSelf: "center",
marginTop: spacing.extraLarge,
}
8 changes: 8 additions & 0 deletions app/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ const en = {
locationLabel: "Location",
moreInfo: "More info",
},
ota: {
title: "Update Available",
subtitle:
"Good things come to those who update! Check out the newest version of our app! Blame Jamon for any bugs!",
cancelLabel: "Maybe Later",
confirmLabel: "Update",
confirmLabelUpdating: "Updating...",
},
}

export default en
Expand Down
51 changes: 41 additions & 10 deletions app/screens/DebugScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { FC } from "react"
import { TextStyle, ViewStyle } from "react-native"
import { TextStyle, View, ViewStyle } from "react-native"
import { StackScreenProps } from "@react-navigation/stack"
import { AppStackParamList } from "../navigators/AppNavigator"
import { resetRoot } from "../navigators/navigationUtilities"
Expand All @@ -12,6 +12,8 @@ import { translate } from "../i18n"
import { clear } from "../utils/storage"
import { useQueryClient } from "@tanstack/react-query"
import { BackButton } from "../navigators/BackButton"
import * as Updates from "expo-updates"
import * as Application from "expo-application"

export const DebugScreen: FC<StackScreenProps<AppStackParamList, "Debug">> = () => {
const navigation = useAppNavigation()
Expand Down Expand Up @@ -44,18 +46,34 @@ export const DebugScreen: FC<StackScreenProps<AppStackParamList, "Debug">> = ()

return (
<Screen
contentContainerStyle={$rootContainer}
style={$root}
preset="scroll"
ScrollViewProps={{ showsVerticalScrollIndicator: false }}
preset="fixed"
safeAreaEdges={["bottom"]}
>
<Text preset="bold" tx="debugScreen.pushToken" style={$subtitle} />
<Text text={fcmToken} selectable />
<Button
shadowStyle={$resetStateButtonShadow}
tx="debugScreen.resetState"
onPress={() => clearState()}
/>
<View>
<Text preset="bold" tx="debugScreen.pushToken" style={$subtitle} />
<Text text={fcmToken} selectable />
<Button
shadowStyle={$resetStateButtonShadow}
tx="debugScreen.resetState"
onPress={() => clearState()}
/>
</View>
<View style={$footer}>
<Text preset="companionHeading" text={"Specs:"} />
{Updates.channel.length > 0 ? (
<Text preset="label" text={`Channel: ${Updates.channel}`} style={$spec} />
) : null}
{Updates.updateId ? (
<Text preset="label" text={`Update ID: ${Updates.updateId}`} style={$spec} />
) : null}
<Text
preset="label"
text={`App Version: ${Application.nativeApplicationVersion}`}
style={$spec}
/>
</View>
</Screen>
)
}
Expand All @@ -65,10 +83,23 @@ const $root: ViewStyle = {
paddingHorizontal: spacing.large,
}

const $rootContainer: ViewStyle = {
flex: 1,
justifyContent: "space-between",
}

const $resetStateButtonShadow: ViewStyle = {
marginTop: spacing.large,
}

const $subtitle: TextStyle = {
marginBottom: spacing.medium,
}

const $footer: ViewStyle = {
marginBottom: spacing.medium,
}

const $spec: TextStyle = {
marginTop: spacing.extraSmall,
}
Loading

0 comments on commit f52f40c

Please sign in to comment.