about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app.json12
-rw-r--r--docs/build.md5
-rw-r--r--src/lib/app-info.ts3
-rw-r--r--src/lib/hooks/useOTAUpdate.ts74
-rw-r--r--src/view/screens/Settings.tsx2
-rw-r--r--src/view/shell/index.tsx2
6 files changed, 95 insertions, 3 deletions
diff --git a/app.json b/app.json
index 9016a3646..16061a24a 100644
--- a/app.json
+++ b/app.json
@@ -5,6 +5,9 @@
     "scheme": "bluesky",
     "owner": "blueskysocial",
     "version": "1.29.0",
+    "runtimeVersion": {
+      "policy": "appVersion"
+    },
     "orientation": "portrait",
     "icon": "./assets/icon.png",
     "userInterfaceStyle": "light",
@@ -63,9 +66,15 @@
     "web": {
       "favicon": "./assets/favicon.png"
     },
+    "updates": {
+      "enabled": true,
+      "fallbackToCacheTimeout": 1000,
+      "url": "https://u.expo.dev/55bd077a-d905-4184-9c7f-94789ba0f302"
+    },
     "plugins": [
       "expo-localization",
       "react-native-background-fetch",
+      "sentry-expo",
       [
         "expo-build-properties",
         {
@@ -79,8 +88,7 @@
         {
           "username": "blueskysocial"
         }
-      ],
-      "sentry-expo"
+      ]
     ],
     "extra": {
       "eas": {
diff --git a/docs/build.md b/docs/build.md
index 715018ed0..a4b03fc7e 100644
--- a/docs/build.md
+++ b/docs/build.md
@@ -115,3 +115,8 @@ upload-sourcemaps \
 --dist <iOS Update ID> \
 --rewrite \
 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/)
diff --git a/src/lib/app-info.ts b/src/lib/app-info.ts
index a365e7e9f..3f026d3fe 100644
--- a/src/lib/app-info.ts
+++ b/src/lib/app-info.ts
@@ -1,2 +1,5 @@
 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})`
diff --git a/src/lib/hooks/useOTAUpdate.ts b/src/lib/hooks/useOTAUpdate.ts
new file mode 100644
index 000000000..ae6035223
--- /dev/null
+++ b/src/lib/hooks/useOTAUpdate.ts
@@ -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
+}
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index 798893855..52be35a48 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -445,7 +445,7 @@ export const SettingsScreen = withAuthRequired(
             </Link>
           ) : null}
           <Text type="sm" style={[styles.buildInfo, pal.textLight]}>
-            Build version {AppInfo.appVersion}
+            Build version {AppInfo.appVersion} {AppInfo.updateChannel}
           </Text>
           <View style={s.footerSpacer} />
         </ScrollView>
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index a6066b25f..36f7442dc 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -18,9 +18,11 @@ import {RoutesContainer, TabsNavigator} from '../../Navigation'
 import {isStateAtTabRoot} from 'lib/routes/helpers'
 import {isAndroid} from 'platform/detection'
 import {SafeAreaProvider} from 'react-native-safe-area-context'
+import {useOTAUpdate} from 'lib/hooks/useOTAUpdate'
 
 const ShellInner = observer(() => {
   const store = useStores()
+  useOTAUpdate() // this hook polls for OTA updates every few seconds
   const winDim = useWindowDimensions()
   const safeAreaInsets = useSafeAreaInsets()
   const containerPadding = React.useMemo(