about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build-submit-android.yml2
-rw-r--r--.github/workflows/build-submit-ios.yml5
-rw-r--r--.github/workflows/bundle-deploy-eas-update.yml227
-rw-r--r--.gitignore6
-rw-r--r--app.config.js25
-rw-r--r--eas.json34
-rw-r--r--package.json14
-rw-r--r--patches/expo-updates+0.24.7.patch26
-rw-r--r--patches/expo-updates+0.24.7.patch.md7
-rw-r--r--scripts/bundleUpdate.sh7
-rwxr-xr-xscripts/useBuildNumberEnv.sh8
-rwxr-xr-xscripts/useBuildNumberEnvWithBump.sh11
-rw-r--r--src/App.native.tsx2
-rw-r--r--src/components/ReportDialog/SelectLabelerView.tsx8
-rw-r--r--src/components/ReportDialog/SelectReportOptionView.tsx13
-rw-r--r--src/components/moderation/LabelPreference.tsx13
-rw-r--r--src/lib/app-info.ts10
-rw-r--r--src/lib/hooks/useOTAUpdates.ts142
-rw-r--r--src/screens/Moderation/index.tsx60
-rw-r--r--src/screens/Profile/Sections/Labels.tsx27
-rw-r--r--src/view/screens/Settings/index.tsx86
21 files changed, 583 insertions, 150 deletions
diff --git a/.github/workflows/build-submit-android.yml b/.github/workflows/build-submit-android.yml
index 8cbd90984..51fa5f4c3 100644
--- a/.github/workflows/build-submit-android.yml
+++ b/.github/workflows/build-submit-android.yml
@@ -59,7 +59,7 @@ jobs:
           echo "$json" > google-services.json
 
       - name: 🏗️ EAS Build
-        run: yarn use-build-number eas build -p android --profile production --local --output build.aab --non-interactive
+        run: yarn use-build-number-with-bump eas build -p android --profile production --local --output build.aab --non-interactive
 
       - name: 🚀 Deploy
         run: eas submit -p android --non-interactive --path build.aab
diff --git a/.github/workflows/build-submit-ios.yml b/.github/workflows/build-submit-ios.yml
index f5188b4b4..c9752d862 100644
--- a/.github/workflows/build-submit-ios.yml
+++ b/.github/workflows/build-submit-ios.yml
@@ -2,14 +2,13 @@
 name: Build and Submit iOS
 
 on:
-  schedule:
-    - cron: '0 5 * * *'
   workflow_dispatch:
     inputs:
       profile:
         type: choice
         description: Build profile to use
         options:
+          - testflight
           - production
 
 jobs:
@@ -69,7 +68,7 @@ jobs:
           echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
 
       - name: 🏗️ EAS Build
-        run: yarn use-build-number eas build -p ios --profile production --local --output build.ipa --non-interactive
+        run: yarn use-build-number-with-bump eas build -p ios --profile ${{ inputs.profile || 'testflight' }} --local --output build.ipa --non-interactive
 
       - name: 🚀 Deploy
         run: eas submit -p ios --non-interactive --path build.ipa
diff --git a/.github/workflows/bundle-deploy-eas-update.yml b/.github/workflows/bundle-deploy-eas-update.yml
index 72a38eaa6..1c7e57e5c 100644
--- a/.github/workflows/bundle-deploy-eas-update.yml
+++ b/.github/workflows/bundle-deploy-eas-update.yml
@@ -4,6 +4,12 @@ name: Bundle and Deploy EAS Update
 on:
   workflow_dispatch:
     inputs:
+      channel:
+        type: choice
+        description: Deployment channel to use
+        options:
+          - testflight
+          - production
       runtimeVersion:
         type: string
         description: Runtime version (in x.x.x format) that this update is for
@@ -13,13 +19,48 @@ jobs:
   bundleDeploy:
     name: Bundle and Deploy EAS Update
     runs-on: ubuntu-latest
+    outputs:
+      fingerprint-diff: ${{ steps.fingerprint.outputs.fingerprint-diff }}
     steps:
+      - name: Check for EXPO_TOKEN
+        run: >
+          if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
+            echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
+            exit 1
+          fi
+
+      # Validate the version if one is supplied. This should generally happen if the update is for a production client
       - name: 🧐 Validate version
+        if: ${{ inputs.runtimeVersion }}
         run: |
-          [[ "${{ github.event.inputs.runtimeVersion }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "Version is valid" || exit 1
+          if [ -z "${{ inputs.runtimeVersion }}" ]; then
+            [[ "${{ inputs.runtimeVersion }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "Version is valid" || exit 1
+          fi
 
       - name: ⬇️ Checkout
         uses: actions/checkout@v4
+        with:
+          fetch-depth: 100
+
+      - name: ⬇️ Fetch commits from base branch
+        run: git fetch origin main:main --depth 100
+
+      # This should get the current production release's commit's hash to see if the update is compatible
+      - name: 🕵️ Get the base commit
+        id: base-commit
+        run: |
+          if [ -z "${{ inputs.channel == 'production' }}" ]; then
+            echo base-commit=$(git show-ref -s ${{ inputs.runtimeVersion }}) >> "$GITHUB_OUTPUT"
+          else
+            echo base-commit=$(git log -n 1 --skip 1 main --pretty=format:'%H') >> "$GITHUB_OUTPUT"
+          fi
+
+      - name: ✓ Make sure we found a base commit
+        run: |
+          if [ -z "${{ steps.base-commit.outputs.base-commit }}" ]; then
+            echo "Could not find a base commit for this release. Exiting."
+            exit 1
+          fi
 
       - name: 🔧 Setup Node
         uses: actions/setup-node@v4
@@ -30,26 +71,200 @@ jobs:
       - name: ⚙️ Install Dependencies
         run: yarn install
 
-      - name: 🪛 Install jq
-        uses: dcarbone/install-jq-action@v2
+      # Run the fingerprint
+      - name: 📷 Check fingerprint
+        id: fingerprint
+        uses: expo/expo-github-action/fingerprint@main
+        with:
+          previous-git-commit: ${{ steps.base-commit.outputs.base-commit }}
+
+      - name: 👀 Debug fingerprint
+        run: |
+          echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}"
+          echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}"
+
+      - name: 🔨 Setup EAS
+        uses: expo/expo-github-action@v8
+        if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }}
+        with:
+          expo-version: latest
+          eas-version: latest
+          token: ${{ secrets.EXPO_TOKEN }}
 
       - name: ⛏️ Setup Expo
+        if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }}
         run: yarn global add eas-cli-local-build-plugin
 
+      - name: 🪛 Setup jq
+        if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }}
+        uses: dcarbone/install-jq-action@v2
+
       - name: 🔤 Compile Translations
+        if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }}
         run: yarn intl:build
 
       - name: ✏️ Write environment variables
+        if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }}
         run: |
           export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
           echo "${{ secrets.ENV_TOKEN }}" > .env
           echo "$json" > google-services.json
 
       - name: 🏗️ Create Bundle
-        run: yarn export
+        if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }}
+        run: EXPO_PUBLIC_ENV="${{ inputs.channel || 'testflight' }}" yarn export
 
       - name: 📦 Package Bundle and 🚀 Deploy
-        run: yarn make-deploy-bundle
+        if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }}
+        run: yarn use-build-number bash scripts/bundleUpdate.sh
         env:
           DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
-          RUNTIME_VERSION: ${{ github.event.inputs.runtimeVersion }}
+          RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
+          CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
+
+  # GitHub actions are horrible so let's just copy paste this in
+  buildIfNecessaryIOS:
+    name: Build and Submit iOS
+    runs-on: macos-14
+    needs: [bundleDeploy]
+    # Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
+    # available here
+    if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.fingerprint-diff != '[]' }}
+    steps:
+      - name: Check for EXPO_TOKEN
+        run: >
+          if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
+            echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
+            exit 1
+          fi
+
+      - name: ⬇️ Checkout
+        uses: actions/checkout@v4
+
+      - name: 🔧 Setup Node
+        uses: actions/setup-node@v4
+        with:
+          node-version-file: .nvmrc
+          cache: yarn
+
+      - name: 🔨 Setup EAS
+        uses: expo/expo-github-action@v8
+        with:
+          expo-version: latest
+          eas-version: latest
+          token: ${{ secrets.EXPO_TOKEN }}
+
+      - name: ⛏️ Setup EAS local builds
+        run: yarn global add eas-cli-local-build-plugin
+
+      - name: ⚙️ Install dependencies
+        run: yarn install
+
+      - name: ☕️ Setup Cocoapods
+        uses: maxim-lobanov/setup-cocoapods@v1
+        with:
+          version: 1.14.3
+
+      - name: 💾 Cache Pods
+        uses: actions/cache@v3
+        id: pods-cache
+        with:
+          path: ./ios/Pods
+          # We'll use the yarn.lock for our hash since we don't yet have a Podfile.lock. Pod versions will not
+          # change unless the yarn version changes as well.
+          key: ${{ runner.os }}-pods-${{ hashFiles('yarn.lock') }}
+
+      - name: 🔤 Compile translations
+        run: yarn intl:build
+
+      - name: ✏️ Write environment variables
+        run: |
+          echo "${{ secrets.ENV_TOKEN }}" > .env
+          echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
+
+      - name: 🏗️ EAS Build
+        run: yarn use-build-number-with-bump eas build -p ios --profile testflight --local --output build.ipa --non-interactive
+
+      - name: 🚀 Deploy
+        run: eas submit -p ios --non-interactive --path build.ipa
+
+  buildIfNecessaryAndroid:
+    name: Build and Submit Android
+    runs-on: ubuntu-latest
+    needs: [ bundleDeploy ]
+    # Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
+    # available here
+    if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.fingerprint-diff != '[]' }}
+
+    steps:
+      - name: Check for EXPO_TOKEN
+        run: >
+          if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
+            echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
+            exit 1
+          fi
+
+      - name: ⬇️ Checkout
+        uses: actions/checkout@v4
+
+      - name: 🔧 Setup Node
+        uses: actions/setup-node@v4
+        with:
+          node-version-file: .nvmrc
+          cache: yarn
+
+      - name: 🔨 Setup EAS
+        uses: expo/expo-github-action@v8
+        with:
+          expo-version: latest
+          eas-version: latest
+          token: ${{ secrets.EXPO_TOKEN }}
+
+      - name: ⛏️ Setup EAS local builds
+        run: yarn global add eas-cli-local-build-plugin
+
+      - uses: actions/setup-java@v4
+        with:
+          distribution: 'temurin'
+          java-version: '17'
+
+      - name: ⚙️ Install dependencies
+        run: yarn install
+
+      - name: 🔤 Compile translations
+        run: yarn intl:build
+
+      - name: ✏️ Write environment variables
+        run: |
+          export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
+          echo "${{ secrets.ENV_TOKEN }}" > .env
+          echo "$json" > google-services.json
+
+      - name: 🏗️ EAS Build
+        run: yarn use-build-number-with-bump eas build -p android --profile testflight-android --local --output build.apk --non-interactive
+
+      - name: ⏰ Get a timestamp
+        id: timestamp
+        uses: nanzm/get-time-action@master
+        with:
+          format: 'MM-DD-HH-mm-ss'
+
+      - name: 🚀 Upload Artifact
+        id: upload-artifact
+        uses: actions/upload-artifact@v4
+        with:
+          retention-days: 30
+          compression-level: 0
+          name: build-${{ steps.timestamp.outputs.time }}.apk
+          path: build.apk
+
+      - name: 🔔 Notify Slack
+        uses: slackapi/slack-github-action@v1.25.0
+        with:
+          payload: |
+            {
+              "text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact.outputs.artifact-url }}"
+            }
+        env:
+          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
+          SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
diff --git a/.gitignore b/.gitignore
index ddb553d26..77dbd00fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,7 +18,6 @@ xcuserdata
 *.moved-aside
 DerivedData
 *.hmap
-*.ipa
 *.xcuserstate
 
 # Android/IntelliJ
@@ -110,3 +109,8 @@ google-services.json
 # i18n
 src/locale/locales/_build/
 src/locale/locales/**/*.js
+
+# local builds
+*.apk
+*.aab
+*.ipa
diff --git a/app.config.js b/app.config.js
index c151862ab..21b794917 100644
--- a/app.config.js
+++ b/app.config.js
@@ -41,6 +41,9 @@ module.exports = function (config) {
       : process.env.BSKY_IOS_BUILD_NUMBER
 
   const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
+  const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
+
+  const UPDATES_CHANNEL = IS_TESTFLIGHT ? 'testflight' : 'production'
 
   return {
     expo: {
@@ -122,10 +125,20 @@ module.exports = function (config) {
         favicon: './assets/favicon.png',
       },
       updates: {
-        enabled: true,
-        fallbackToCacheTimeout: 1000,
-        url: 'https://u.expo.dev/55bd077a-d905-4184-9c7f-94789ba0f302',
+        url: 'https://updates.bsky.app/manifest',
+        // TODO Eventually we want to enable this for all environments, but for now it will only be used for
+        // TestFlight builds
+        enabled: IS_TESTFLIGHT,
+        fallbackToCacheTimeout: 30000,
+        codeSigningCertificate: './code-signing/certificate.pem',
+        codeSigningMetadata: {
+          keyid: 'main',
+          alg: 'rsa-v1_5-sha256',
+        },
+        checkAutomatically: 'NEVER',
+        channel: UPDATES_CHANNEL,
       },
+      assetBundlePatterns: ['**/*'],
       plugins: [
         'expo-localization',
         Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo',
@@ -146,12 +159,6 @@ module.exports = function (config) {
           },
         ],
         [
-          'expo-updates',
-          {
-            username: 'blueskysocial',
-          },
-        ],
-        [
           'expo-notifications',
           {
             icon: './assets/icon-android-notification.png',
diff --git a/eas.json b/eas.json
index 2b4c7cb61..ed647dbb9 100644
--- a/eas.json
+++ b/eas.json
@@ -16,14 +16,20 @@
       "ios": {
         "simulator": true,
         "resourceClass": "large"
+      },
+      "env": {
+        "EXPO_PUBLIC_ENV": "production"
       }
     },
     "preview": {
       "extends": "base",
       "distribution": "internal",
-      "channel": "preview",
+      "channel": "production",
       "ios": {
         "resourceClass": "large"
+      },
+      "env": {
+        "EXPO_PUBLIC_ENV": "production"
       }
     },
     "production": {
@@ -35,17 +41,37 @@
       "android": {
         "autoIncrement": true
       },
-      "channel": "production"
+      "channel": "production",
+      "env": {
+        "EXPO_PUBLIC_ENV": "production"
+      }
+    },
+    "testflight": {
+      "extends": "base",
+      "ios": {
+        "autoIncrement": true
+      },
+      "android": {
+        "autoIncrement": true
+      },
+      "channel": "testflight",
+      "env": {
+        "EXPO_PUBLIC_ENV": "testflight"
+      }
     },
-    "github": {
+    "testflight-android": {
       "extends": "base",
+      "distribution": "internal",
       "ios": {
         "autoIncrement": true
       },
       "android": {
         "autoIncrement": true
       },
-      "channel": "production"
+      "channel": "testflight",
+      "env": {
+        "EXPO_PUBLIC_ENV": "testflight"
+      }
     }
   },
   "submit": {
diff --git a/package.json b/package.json
index c51d6f22a..b4018463b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "bsky.app",
-  "version": "1.75.0",
+  "version": "1.76.0",
   "private": true,
   "engines": {
     "node": ">=18"
@@ -14,11 +14,12 @@
     "ios": "expo run:ios",
     "web": "expo start --web",
     "use-build-number": "./scripts/useBuildNumberEnv.sh",
+    "use-build-number-with-bump": "./scripts/useBuildNumberEnvWithBump.sh",
     "build-web": "expo export:web && node ./scripts/post-web-build.js && cp -v ./web-build/static/js/*.* ./bskyweb/static/js/",
-    "build-all": "yarn intl:build && yarn use-build-number eas build --platform all",
-    "build-ios": "yarn use-build-number eas build -p ios",
-    "build-android": "yarn use-build-number eas build -p android",
-    "build": "yarn use-build-number eas build",
+    "build-all": "yarn intl:build && yarn use-build-number-with-bump eas build --platform all",
+    "build-ios": "yarn use-build-number-with-bump eas build -p ios",
+    "build-android": "yarn use-build-number-with-bump eas build -p android",
+    "build": "yarn use-build-number-with-bump eas build",
     "start": "expo start --dev-client",
     "start:prod": "expo start --dev-client --no-dev --minify",
     "clean-cache": "rm -rf node_modules/.cache/babel-loader/*",
@@ -43,8 +44,7 @@
     "intl:compile": "lingui compile",
     "nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android",
     "update-extensions": "bash scripts/updateExtensions.sh",
-    "export": "npx expo export",
-    "make-deploy-bundle": "bash scripts/bundleUpdate.sh"
+    "export": "npx expo export"
   },
   "dependencies": {
     "@atproto/api": "^0.12.2",
diff --git a/patches/expo-updates+0.24.7.patch b/patches/expo-updates+0.24.7.patch
new file mode 100644
index 000000000..603ae32ef
--- /dev/null
+++ b/patches/expo-updates+0.24.7.patch
@@ -0,0 +1,26 @@
+diff --git a/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift b/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift
+index 189a5f5..8d5b8e6 100644
+--- a/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift
++++ b/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift
+@@ -68,13 +68,20 @@ public final class NewUpdate: Update {
+       processedAssets.append(asset)
+     }
+
++    // Instead of relying on various hacks to get the correct format for the specific
++    // platform on the backend, we can just add this little patch..
++    let dateFormatter = DateFormatter()
++    dateFormatter.locale = Locale(identifier: "en_US_POSIX")
++    dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
++    let date = dateFormatter.date(from:commitTime) ?? RCTConvert.nsDate(commitTime)!
++
+     return Update(
+       manifest: manifest,
+       config: config,
+       database: database,
+       updateId: uuid,
+       scopeKey: config.scopeKey,
+-      commitTime: RCTConvert.nsDate(commitTime),
++      commitTime: date,
+       runtimeVersion: runtimeVersion,
+       keep: true,
+       status: UpdateStatus.StatusPending,
diff --git a/patches/expo-updates+0.24.7.patch.md b/patches/expo-updates+0.24.7.patch.md
new file mode 100644
index 000000000..8a8848127
--- /dev/null
+++ b/patches/expo-updates+0.24.7.patch.md
@@ -0,0 +1,7 @@
+# Expo-Updates Patch
+
+This is a small patch to convert timestamp formats that are returned from the backend. Instead of relying on the
+backend to return the correct format for a specific format (the format required on Android is not the same as on iOS)
+we can just add this conversion in.
+
+Don't remove unless we make changes on the backend to support both platforms.
\ No newline at end of file
diff --git a/scripts/bundleUpdate.sh b/scripts/bundleUpdate.sh
index 18db81a20..5927a36c8 100644
--- a/scripts/bundleUpdate.sh
+++ b/scripts/bundleUpdate.sh
@@ -9,10 +9,13 @@ rm -rf bundle.tar.gz
 echo "Creating tarball..."
 node scripts/bundleUpdate.js
 
-cd bundleTempDir || exit
+if [ -z "$RUNTIME_VERSION" ]; then
+  RUNTIME_VERSION=$(cat package.json | jq '.version' -r)
+fi
 
+cd bundleTempDir || exit
 BUNDLE_VERSION=$(date +%s)
-DEPLOYMENT_URL="https://updates.bsky.app/v1/upload?runtime-version=$RUNTIME_VERSION&bundle-version=$BUNDLE_VERSION"
+DEPLOYMENT_URL="https://updates.bsky.app/v1/upload?runtime-version=$RUNTIME_VERSION&bundle-version=$BUNDLE_VERSION&channel=$CHANNEL_NAME&ios-build-number=$BSKY_IOS_BUILD_NUMBER&android-build-number=$BSKY_ANDROID_VERSION_CODE"
 
 tar czvf bundle.tar.gz ./*
 
diff --git a/scripts/useBuildNumberEnv.sh b/scripts/useBuildNumberEnv.sh
index fe273d394..2251c0907 100755
--- a/scripts/useBuildNumberEnv.sh
+++ b/scripts/useBuildNumberEnv.sh
@@ -1,11 +1,7 @@
 #!/bin/bash
 outputIos=$(eas build:version:get -p ios)
 outputAndroid=$(eas build:version:get -p android)
-currentIosVersion=${outputIos#*buildNumber - }
-currentAndroidVersion=${outputAndroid#*versionCode - }
-
-BSKY_IOS_BUILD_NUMBER=$((currentIosVersion+1))
-BSKY_ANDROID_VERSION_CODE=$((currentAndroidVersion+1))
+BSKY_IOS_BUILD_NUMBER=${outputIos#*buildNumber - }
+BSKY_ANDROID_VERSION_CODE=${outputAndroid#*versionCode - }
 
 bash -c "BSKY_IOS_BUILD_NUMBER=$BSKY_IOS_BUILD_NUMBER BSKY_ANDROID_VERSION_CODE=$BSKY_ANDROID_VERSION_CODE $*"
-
diff --git a/scripts/useBuildNumberEnvWithBump.sh b/scripts/useBuildNumberEnvWithBump.sh
new file mode 100755
index 000000000..fe273d394
--- /dev/null
+++ b/scripts/useBuildNumberEnvWithBump.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+outputIos=$(eas build:version:get -p ios)
+outputAndroid=$(eas build:version:get -p android)
+currentIosVersion=${outputIos#*buildNumber - }
+currentAndroidVersion=${outputAndroid#*versionCode - }
+
+BSKY_IOS_BUILD_NUMBER=$((currentIosVersion+1))
+BSKY_ANDROID_VERSION_CODE=$((currentAndroidVersion+1))
+
+bash -c "BSKY_IOS_BUILD_NUMBER=$BSKY_IOS_BUILD_NUMBER BSKY_ANDROID_VERSION_CODE=$BSKY_ANDROID_VERSION_CODE $*"
+
diff --git a/src/App.native.tsx b/src/App.native.tsx
index d6e726a59..2c880f217 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -19,6 +19,7 @@ import {init as initPersistedState} from '#/state/persisted'
 import * as persisted from '#/state/persisted'
 import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
 import {useIntentHandler} from 'lib/hooks/useIntentHandler'
+import {useOTAUpdates} from 'lib/hooks/useOTAUpdates'
 import * as notifications from 'lib/notifications/notifications'
 import {
   asyncStoragePersister,
@@ -60,6 +61,7 @@ function InnerApp() {
   const theme = useColorModeTheme()
   const {_} = useLingui()
   useIntentHandler()
+  useOTAUpdates()
 
   // init
   useEffect(() => {
diff --git a/src/components/ReportDialog/SelectLabelerView.tsx b/src/components/ReportDialog/SelectLabelerView.tsx
index 383d1b95f..dd07cafa3 100644
--- a/src/components/ReportDialog/SelectLabelerView.tsx
+++ b/src/components/ReportDialog/SelectLabelerView.tsx
@@ -1,18 +1,16 @@
 import React from 'react'
 import {View} from 'react-native'
+import {AppBskyLabelerDefs} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {AppBskyLabelerDefs} from '@atproto/api'
 
 export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
 import {getLabelingServiceTitle} from '#/lib/moderation'
-
-import {atoms as a, useTheme, useBreakpoints} from '#/alf'
-import {Text} from '#/components/Typography'
+import {atoms as a, useBreakpoints, useTheme} from '#/alf'
 import {Button, useButtonContext} from '#/components/Button'
 import {Divider} from '#/components/Divider'
 import * as LabelingServiceCard from '#/components/LabelingServiceCard'
-
+import {Text} from '#/components/Typography'
 import {ReportDialogProps} from './types'
 
 export function SelectLabelerView({
diff --git a/src/components/ReportDialog/SelectReportOptionView.tsx b/src/components/ReportDialog/SelectReportOptionView.tsx
index 54844cfda..c67698348 100644
--- a/src/components/ReportDialog/SelectReportOptionView.tsx
+++ b/src/components/ReportDialog/SelectReportOptionView.tsx
@@ -1,16 +1,15 @@
 import React from 'react'
 import {View} from 'react-native'
+import {AppBskyLabelerDefs} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {AppBskyLabelerDefs} from '@atproto/api'
 
-import {useReportOptions, ReportOption} from '#/lib/moderation/useReportOptions'
-import {DMCA_LINK} from '#/components/ReportDialog/const'
+import {ReportOption, useReportOptions} from '#/lib/moderation/useReportOptions'
 import {Link} from '#/components/Link'
+import {DMCA_LINK} from '#/components/ReportDialog/const'
 export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
 
-import {atoms as a, useTheme, useBreakpoints} from '#/alf'
-import {Text} from '#/components/Typography'
+import {atoms as a, useBreakpoints, useTheme} from '#/alf'
 import {
   Button,
   ButtonIcon,
@@ -19,11 +18,11 @@ import {
 } from '#/components/Button'
 import {Divider} from '#/components/Divider'
 import {
-  ChevronRight_Stroke2_Corner0_Rounded as ChevronRight,
   ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft,
+  ChevronRight_Stroke2_Corner0_Rounded as ChevronRight,
 } from '#/components/icons/Chevron'
 import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from '#/components/icons/SquareArrowTopRight'
-
+import {Text} from '#/components/Typography'
 import {ReportDialogProps} from './types'
 
 export function SelectReportOptionView({
diff --git a/src/components/moderation/LabelPreference.tsx b/src/components/moderation/LabelPreference.tsx
index 7d4bd9c32..028bd1a39 100644
--- a/src/components/moderation/LabelPreference.tsx
+++ b/src/components/moderation/LabelPreference.tsx
@@ -1,22 +1,21 @@
 import React from 'react'
 import {View} from 'react-native'
 import {InterpretedLabelValueDefinition, LabelPreference} from '@atproto/api'
-import {useLingui} from '@lingui/react'
 import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
+import {useLabelBehaviorDescription} from '#/lib/moderation/useLabelBehaviorDescription'
+import {getLabelStrings} from '#/lib/moderation/useLabelInfo'
 import {
   usePreferencesQuery,
   usePreferencesSetContentLabelMutation,
 } from '#/state/queries/preferences'
-import {useLabelBehaviorDescription} from '#/lib/moderation/useLabelBehaviorDescription'
-import {getLabelStrings} from '#/lib/moderation/useLabelInfo'
-
-import {useTheme, atoms as a, useBreakpoints} from '#/alf'
-import {Text} from '#/components/Typography'
+import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import * as ToggleButton from '#/components/forms/ToggleButton'
 import {InlineLink} from '#/components/Link'
+import {Text} from '#/components/Typography'
 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '../icons/CircleInfo'
-import * as ToggleButton from '#/components/forms/ToggleButton'
 
 export function Outer({children}: React.PropsWithChildren<{}>) {
   return (
diff --git a/src/lib/app-info.ts b/src/lib/app-info.ts
index 3f026d3fe..3071e031b 100644
--- a/src/lib/app-info.ts
+++ b/src/lib/app-info.ts
@@ -1,5 +1,9 @@
 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 IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
+export const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
+
+const UPDATES_CHANNEL = IS_TESTFLIGHT ? 'testflight' : 'production'
+export const appVersion = `${VersionNumber.appVersion} (${
+  VersionNumber.buildVersion
+}, ${IS_DEV ? 'development' : UPDATES_CHANNEL})`
diff --git a/src/lib/hooks/useOTAUpdates.ts b/src/lib/hooks/useOTAUpdates.ts
new file mode 100644
index 000000000..181f0b2c6
--- /dev/null
+++ b/src/lib/hooks/useOTAUpdates.ts
@@ -0,0 +1,142 @@
+import React from 'react'
+import {Alert, AppState, AppStateStatus} from 'react-native'
+import app from 'react-native-version-number'
+import {
+  checkForUpdateAsync,
+  fetchUpdateAsync,
+  isEnabled,
+  reloadAsync,
+  setExtraParamAsync,
+  useUpdates,
+} from 'expo-updates'
+
+import {logger} from '#/logger'
+import {IS_TESTFLIGHT} from 'lib/app-info'
+import {isIOS} from 'platform/detection'
+
+const MINIMUM_MINIMIZE_TIME = 15 * 60e3
+
+async function setExtraParams() {
+  await setExtraParamAsync(
+    isIOS ? 'ios-build-number' : 'android-build-number',
+    // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is.
+    // This just ensures it gets passed as a string
+    `${app.buildVersion}`,
+  )
+  await setExtraParamAsync(
+    'channel',
+    IS_TESTFLIGHT ? 'testflight' : 'production',
+  )
+}
+
+export function useOTAUpdates() {
+  const appState = React.useRef<AppStateStatus>('active')
+  const lastMinimize = React.useRef(0)
+  const ranInitialCheck = React.useRef(false)
+  const timeout = React.useRef<NodeJS.Timeout>()
+  const {isUpdatePending} = useUpdates()
+
+  const setCheckTimeout = React.useCallback(() => {
+    timeout.current = setTimeout(async () => {
+      try {
+        await setExtraParams()
+
+        logger.debug('Checking for update...')
+        const res = await checkForUpdateAsync()
+
+        if (res.isAvailable) {
+          logger.debug('Attempting to fetch update...')
+          await fetchUpdateAsync()
+        } else {
+          logger.debug('No update available.')
+        }
+      } catch (e) {
+        logger.warn('OTA Update Error', {error: `${e}`})
+      }
+    }, 10e3)
+  }, [])
+
+  const onIsTestFlight = React.useCallback(() => {
+    setTimeout(async () => {
+      try {
+        await setExtraParams()
+
+        const res = await checkForUpdateAsync()
+        if (res.isAvailable) {
+          await fetchUpdateAsync()
+
+          Alert.alert(
+            'Update Available',
+            'A new version of the app is available. Relaunch now?',
+            [
+              {
+                text: 'No',
+                style: 'cancel',
+              },
+              {
+                text: 'Relaunch',
+                style: 'default',
+                onPress: async () => {
+                  await reloadAsync()
+                },
+              },
+            ],
+          )
+        }
+      } catch (e: any) {
+        // No need to handle
+      }
+    }, 3e3)
+  }, [])
+
+  React.useEffect(() => {
+    // For Testflight users, we can prompt the user to update immediately whenever there's an available update. This
+    // is suspect however with the Apple App Store guidelines, so we don't want to prompt production users to update
+    // immediately.
+    if (IS_TESTFLIGHT) {
+      onIsTestFlight()
+      return
+    } else if (!isEnabled || __DEV__ || ranInitialCheck.current) {
+      // Development client shouldn't check for updates at all, so we skip that here.
+      return
+    }
+
+    setCheckTimeout()
+    ranInitialCheck.current = true
+  }, [onIsTestFlight, setCheckTimeout])
+
+  // After the app has been minimized for 30 minutes, we want to either A. install an update if one has become available
+  // or B check for an update again.
+  React.useEffect(() => {
+    if (!isEnabled) return
+
+    const subscription = AppState.addEventListener(
+      'change',
+      async nextAppState => {
+        if (
+          appState.current.match(/inactive|background/) &&
+          nextAppState === 'active'
+        ) {
+          // If it's been 15 minutes since the last "minimize", we should feel comfortable updating the client since
+          // chances are that there isn't anything important going on in the current session.
+          if (lastMinimize.current <= Date.now() - MINIMUM_MINIMIZE_TIME) {
+            if (isUpdatePending) {
+              await reloadAsync()
+            } else {
+              setCheckTimeout()
+            }
+          }
+        } else {
+          lastMinimize.current = Date.now()
+        }
+
+        appState.current = nextAppState
+      },
+    )
+
+    return () => {
+      clearTimeout(timeout.current)
+      subscription.remove()
+    }
+  }, [isUpdatePending, setCheckTimeout])
+}
diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx
index 7d991cc71..9d51a6197 100644
--- a/src/screens/Moderation/index.tsx
+++ b/src/screens/Moderation/index.tsx
@@ -1,51 +1,49 @@
 import React from 'react'
 import {View} from 'react-native'
-import {useFocusEffect} from '@react-navigation/native'
+import {useSafeAreaFrame} from 'react-native-safe-area-context'
 import {ComAtprotoLabelDefs} from '@atproto/api'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
 import {LABELS} from '@atproto/api'
-import {useSafeAreaFrame} from 'react-native-safe-area-context'
-
-import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types'
-import {CenteredView} from '#/view/com/util/Views'
-import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {useSetMinimalShellMode} from '#/state/shell'
-import {useSession} from '#/state/session'
-import {
-  useProfileQuery,
-  useProfileUpdateMutation,
-} from '#/state/queries/profile'
-import {ScrollView} from '#/view/com/util/Views'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useFocusEffect} from '@react-navigation/native'
 
+import {getLabelingServiceTitle} from '#/lib/moderation'
+import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
+import {logger} from '#/logger'
 import {
-  UsePreferencesQueryResponse,
   useMyLabelersQuery,
   usePreferencesQuery,
+  UsePreferencesQueryResponse,
   usePreferencesSetAdultContentMutation,
 } from '#/state/queries/preferences'
-
-import {getLabelingServiceTitle} from '#/lib/moderation'
-import {logger} from '#/logger'
-import {useTheme, atoms as a, useBreakpoints, ViewStyleProp} from '#/alf'
+import {
+  useProfileQuery,
+  useProfileUpdateMutation,
+} from '#/state/queries/profile'
+import {useSession} from '#/state/session'
+import {useSetMinimalShellMode} from '#/state/shell'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {ViewHeader} from '#/view/com/util/ViewHeader'
+import {CenteredView} from '#/view/com/util/Views'
+import {ScrollView} from '#/view/com/util/Views'
+import {atoms as a, useBreakpoints, useTheme, ViewStyleProp} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
+import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
 import {Divider} from '#/components/Divider'
+import * as Toggle from '#/components/forms/Toggle'
+import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
 import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
+import {Props as SVGIconProps} from '#/components/icons/common'
+import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
 import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
 import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
-import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
-import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
-import {Text} from '#/components/Typography'
-import * as Toggle from '#/components/forms/Toggle'
+import * as LabelingService from '#/components/LabelingServiceCard'
 import {InlineLink, Link} from '#/components/Link'
-import {Button, ButtonText} from '#/components/Button'
 import {Loader} from '#/components/Loader'
-import * as LabelingService from '#/components/LabelingServiceCard'
 import {GlobalLabelPreference} from '#/components/moderation/LabelPreference'
-import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
-import {Props as SVGIconProps} from '#/components/icons/common'
-import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
-import * as Dialog from '#/components/Dialog'
+import {Text} from '#/components/Typography'
 
 function ErrorState({error}: {error: string}) {
   const t = useTheme()
diff --git a/src/screens/Profile/Sections/Labels.tsx b/src/screens/Profile/Sections/Labels.tsx
index 2b2b99594..5ba8f00a5 100644
--- a/src/screens/Profile/Sections/Labels.tsx
+++ b/src/screens/Profile/Sections/Labels.tsx
@@ -1,30 +1,29 @@
 import React from 'react'
 import {View} from 'react-native'
+import {useSafeAreaFrame} from 'react-native-safe-area-context'
 import {
   AppBskyLabelerDefs,
-  ModerationOpts,
-  interpretLabelValueDefinitions,
   InterpretedLabelValueDefinition,
+  interpretLabelValueDefinitions,
+  ModerationOpts,
 } from '@atproto/api'
-import {Trans, msg} from '@lingui/macro'
+import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {useSafeAreaFrame} from 'react-native-safe-area-context'
 
-import {useScrollHandlers} from '#/lib/ScrollContext'
 import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
 import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation'
-import {ListRef} from '#/view/com/util/List'
-import {SectionRef} from './types'
+import {useScrollHandlers} from '#/lib/ScrollContext'
 import {isNative} from '#/platform/detection'
-
-import {useTheme, atoms as a} from '#/alf'
-import {Text} from '#/components/Typography'
-import {Loader} from '#/components/Loader'
-import {Divider} from '#/components/Divider'
+import {ListRef} from '#/view/com/util/List'
 import {CenteredView, ScrollView} from '#/view/com/util/Views'
-import {ErrorState} from '../ErrorState'
-import {LabelerLabelPreference} from '#/components/moderation/LabelPreference'
+import {atoms as a, useTheme} from '#/alf'
+import {Divider} from '#/components/Divider'
 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
+import {Loader} from '#/components/Loader'
+import {LabelerLabelPreference} from '#/components/moderation/LabelPreference'
+import {Text} from '#/components/Typography'
+import {ErrorState} from '../ErrorState'
+import {SectionRef} from './types'
 
 interface LabelsSectionProps {
   isLabelerLoading: boolean
diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx
index 3967678b4..790ce5ee9 100644
--- a/src/view/screens/Settings/index.tsx
+++ b/src/view/screens/Settings/index.tsx
@@ -3,72 +3,72 @@ import {
   ActivityIndicator,
   Linking,
   Platform,
-  StyleSheet,
   Pressable,
+  StyleSheet,
   TextStyle,
   TouchableOpacity,
   View,
   ViewStyle,
 } from 'react-native'
-import {useFocusEffect, useNavigation} from '@react-navigation/native'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
-import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
-import * as AppInfo from 'lib/app-info'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useCustomPalette} from 'lib/hooks/useCustomPalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {NavigationProp} from 'lib/routes/types'
-import {HandIcon, HashtagIcon} from 'lib/icons'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 import Clipboard from '@react-native-clipboard/clipboard'
-import {makeProfileLink} from 'lib/routes/links'
-import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
+import {useFocusEffect, useNavigation} from '@react-navigation/native'
+import {useQueryClient} from '@tanstack/react-query'
+
+import {isNative} from '#/platform/detection'
 import {useModalControls} from '#/state/modals'
-import {
-  useSetMinimalShellMode,
-  useThemePrefs,
-  useSetThemePrefs,
-  useOnboardingDispatch,
-} from '#/state/shell'
+import {clearLegacyStorage} from '#/state/persisted/legacy'
+// TODO import {useInviteCodesQuery} from '#/state/queries/invites'
+import {clear as clearStorage} from '#/state/persisted/store'
 import {
   useRequireAltTextEnabled,
   useSetRequireAltTextEnabled,
 } from '#/state/preferences'
-import {useSession, useSessionApi, SessionAccount} from '#/state/session'
-import {useProfileQuery} from '#/state/queries/profile'
-import {useClearPreferencesMutation} from '#/state/queries/preferences'
-// TODO import {useInviteCodesQuery} from '#/state/queries/invites'
-import {clear as clearStorage} from '#/state/persisted/store'
-import {clearLegacyStorage} from '#/state/persisted/legacy'
-import {STATUS_PAGE_URL} from 'lib/constants'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useQueryClient} from '@tanstack/react-query'
-import {useLoggedOutViewControls} from '#/state/shell/logged-out'
-import {useCloseAllActiveElements} from '#/state/util'
 import {
   useInAppBrowser,
   useSetInAppBrowser,
 } from '#/state/preferences/in-app-browser'
-import {isNative} from '#/platform/detection'
-import {useDialogControl} from '#/components/Dialog'
-
-import {s, colors} from 'lib/styles'
-import {ScrollView} from 'view/com/util/Views'
+import {useClearPreferencesMutation} from '#/state/queries/preferences'
+import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
+import {useProfileQuery} from '#/state/queries/profile'
+import {SessionAccount, useSession, useSessionApi} from '#/state/session'
+import {
+  useOnboardingDispatch,
+  useSetMinimalShellMode,
+  useSetThemePrefs,
+  useThemePrefs,
+} from '#/state/shell'
+import {useLoggedOutViewControls} from '#/state/shell/logged-out'
+import {useCloseAllActiveElements} from '#/state/util'
+import {useAnalytics} from 'lib/analytics/analytics'
+import * as AppInfo from 'lib/app-info'
+import {STATUS_PAGE_URL} from 'lib/constants'
+import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher'
+import {useCustomPalette} from 'lib/hooks/useCustomPalette'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {HandIcon, HashtagIcon} from 'lib/icons'
+import {makeProfileLink} from 'lib/routes/links'
+import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
+import {NavigationProp} from 'lib/routes/types'
+import {colors, s} from 'lib/styles'
+import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn'
+import {SelectableBtn} from 'view/com/util/forms/SelectableBtn'
+import {ToggleButton} from 'view/com/util/forms/ToggleButton'
 import {Link, TextLink} from 'view/com/util/Link'
+import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
 import {Text} from 'view/com/util/text/Text'
 import * as Toast from 'view/com/util/Toast'
 import {UserAvatar} from 'view/com/util/UserAvatar'
-import {ToggleButton} from 'view/com/util/forms/ToggleButton'
-import {SelectableBtn} from 'view/com/util/forms/SelectableBtn'
-import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn'
-import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
-import {ExportCarDialog} from './ExportCarDialog'
+import {ScrollView} from 'view/com/util/Views'
+import {useDialogControl} from '#/components/Dialog'
 import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
+import {ExportCarDialog} from './ExportCarDialog'
 
 function SettingsAccountCard({account}: {account: SessionAccount}) {
   const pal = usePalette('default')
@@ -890,9 +890,7 @@ export function SettingsScreen({}: Props) {
             accessibilityRole="button"
             onPress={onPressBuildInfo}>
             <Text type="sm" style={[styles.buildInfo, pal.textLight]}>
-              <Trans>
-                Build version {AppInfo.appVersion} {AppInfo.updateChannel}
-              </Trans>
+              <Trans>Version {AppInfo.appVersion}</Trans>
             </Text>
           </TouchableOpacity>
           <Text type="sm" style={[pal.textLight]}>