[APP-107] OTA updates (#587)

* add 1000ms fallbackToCacheTimeout

* add listener via useOTAUpdate hook and show modal if update is available

* finish expo-updates setup

* setup useOTAUpdate hook

* add 1000ms fallbackToCacheTimeout

* add listener via useOTAUpdate hook and show modal if update is available

* finish expo-updates setup

* setup useOTAUpdate hook

* add OTA updates

* Update build.md

* temporarily disable ota updates

* refactor useOTAUpdate code
zio/stable
Ansh 2023-06-02 13:27:59 -07:00 committed by GitHub
parent ad4eaf5ed2
commit ba4bb46c3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 95 additions and 3 deletions

View File

@ -5,6 +5,9 @@
"scheme": "bluesky", "scheme": "bluesky",
"owner": "blueskysocial", "owner": "blueskysocial",
"version": "1.29.0", "version": "1.29.0",
"runtimeVersion": {
"policy": "appVersion"
},
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/icon.png", "icon": "./assets/icon.png",
"userInterfaceStyle": "light", "userInterfaceStyle": "light",
@ -63,9 +66,15 @@
"web": { "web": {
"favicon": "./assets/favicon.png" "favicon": "./assets/favicon.png"
}, },
"updates": {
"enabled": true,
"fallbackToCacheTimeout": 1000,
"url": "https://u.expo.dev/55bd077a-d905-4184-9c7f-94789ba0f302"
},
"plugins": [ "plugins": [
"expo-localization", "expo-localization",
"react-native-background-fetch", "react-native-background-fetch",
"sentry-expo",
[ [
"expo-build-properties", "expo-build-properties",
{ {
@ -79,8 +88,7 @@
{ {
"username": "blueskysocial" "username": "blueskysocial"
} }
], ]
"sentry-expo"
], ],
"extra": { "extra": {
"eas": { "eas": {

View File

@ -115,3 +115,8 @@ upload-sourcemaps \
--dist <iOS Update ID> \ --dist <iOS Update ID> \
--rewrite \ --rewrite \
dist/bundles/main.jsbundle dist/bundles/ios-<hash>.map` dist/bundles/main.jsbundle dist/bundles/ios-<hash>.map`
### OTA updates
To create OTA updates, run `eas update` along with the `--branch` flag to indicate which branch you want to push the update to, and the `--message` flag to indicate a message for yourself and your team that shows up on https://expo.dev. ALl the channels (which make up the options for the `--branch` flag) are given in `eas.json`. [See more here](https://docs.expo.dev/eas-update/getting-started/)
The clients which can receive an OTA update is governed by the `runtimeVersion` property in `app.json`. Right now, it is set so that only apps with the same `appVersion` (same as `version` property in `app.json`) can receive the update and install it. However, we can manually set `"runtimeVersion": "1.34.0"` or anything along those lines as well. This is useful if very little native code changes from update-to-update. If we are manually setting `runtimeVersion`, we should increment the version each time native code is changed. [See more here](https://docs.expo.dev/eas-update/runtime-versions/)

View File

@ -1,2 +1,5 @@
import VersionNumber from 'react-native-version-number' import VersionNumber from 'react-native-version-number'
import * as Updates from 'expo-updates'
export const updateChannel = Updates.channel
export const appVersion = `${VersionNumber.appVersion} (${VersionNumber.buildVersion})` export const appVersion = `${VersionNumber.appVersion} (${VersionNumber.buildVersion})`

View File

@ -0,0 +1,74 @@
import * as Updates from 'expo-updates'
import {useCallback, useEffect} from 'react'
import {AppState} from 'react-native'
import {useStores} from 'state/index'
export function useOTAUpdate() {
const store = useStores()
// HELPER FUNCTIONS
const showUpdatePopup = useCallback(() => {
store.shell.openModal({
name: 'confirm',
title: 'Update Available',
message:
'A new version of the app is available. Please update to continue using the app.',
onPressConfirm: async () => {
Updates.reloadAsync().catch(err => {
throw err
})
},
})
}, [store.shell])
const checkForUpdate = useCallback(async () => {
store.log.debug('useOTAUpdate: Checking for update...')
try {
// Check if new OTA update is available
const update = await Updates.checkForUpdateAsync()
// If updates aren't available stop the function execution
if (!update.isAvailable) {
return
}
// Otherwise fetch the update in the background, so even if the user rejects switching to latest version it will be done automatically on next relaunch.
await Updates.fetchUpdateAsync()
// show a popup modal
showUpdatePopup()
} catch (e) {
console.error('useOTAUpdate: Error while checking for update', e)
store.log.error('useOTAUpdate: Error while checking for update', e)
}
}, [showUpdatePopup, store.log])
const updateEventListener = useCallback(
(event: Updates.UpdateEvent) => {
store.log.debug('useOTAUpdate: Listening for update...')
if (event.type === Updates.UpdateEventType.ERROR) {
throw new Error(event.message)
} else if (event.type === Updates.UpdateEventType.NO_UPDATE_AVAILABLE) {
// Handle no update available
// do nothing
} else if (event.type === Updates.UpdateEventType.UPDATE_AVAILABLE) {
// Handle update available
// open modal, ask for user confirmation, and reload the app
showUpdatePopup()
}
},
[showUpdatePopup, store.log],
)
useEffect(() => {
// ADD EVENT LISTENERS
const updateEventSubscription = Updates.addListener(updateEventListener)
const appStateSubscription = AppState.addEventListener('change', state => {
if (state === 'active' && !__DEV__) {
checkForUpdate()
}
})
// REMOVE EVENT LISTENERS (CLEANUP)
return () => {
updateEventSubscription.remove()
appStateSubscription.remove()
}
}, []) // eslint-disable-line react-hooks/exhaustive-deps
// disable exhaustive deps because we don't want to run this effect again
}

View File

@ -445,7 +445,7 @@ export const SettingsScreen = withAuthRequired(
</Link> </Link>
) : null} ) : null}
<Text type="sm" style={[styles.buildInfo, pal.textLight]}> <Text type="sm" style={[styles.buildInfo, pal.textLight]}>
Build version {AppInfo.appVersion} Build version {AppInfo.appVersion} {AppInfo.updateChannel}
</Text> </Text>
<View style={s.footerSpacer} /> <View style={s.footerSpacer} />
</ScrollView> </ScrollView>

View File

@ -18,9 +18,11 @@ import {RoutesContainer, TabsNavigator} from '../../Navigation'
import {isStateAtTabRoot} from 'lib/routes/helpers' import {isStateAtTabRoot} from 'lib/routes/helpers'
import {isAndroid} from 'platform/detection' import {isAndroid} from 'platform/detection'
import {SafeAreaProvider} from 'react-native-safe-area-context' import {SafeAreaProvider} from 'react-native-safe-area-context'
import {useOTAUpdate} from 'lib/hooks/useOTAUpdate'
const ShellInner = observer(() => { const ShellInner = observer(() => {
const store = useStores() const store = useStores()
useOTAUpdate() // this hook polls for OTA updates every few seconds
const winDim = useWindowDimensions() const winDim = useWindowDimensions()
const safeAreaInsets = useSafeAreaInsets() const safeAreaInsets = useSafeAreaInsets()
const containerPadding = React.useMemo( const containerPadding = React.useMemo(