[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 codezio/stable
parent
ad4eaf5ed2
commit
ba4bb46c3f
12
app.json
12
app.json
|
@ -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": {
|
||||||
|
|
|
@ -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/)
|
||||||
|
|
|
@ -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})`
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in New Issue