diff --git a/.github/consumer/App.tsx b/.github/consumer/App.tsx
new file mode 100644
index 00000000..4643dc81
--- /dev/null
+++ b/.github/consumer/App.tsx
@@ -0,0 +1,60 @@
+// Copied over a fresh React Native Community CLI app by the `consumer-cli` CI
+// job, which installs the packed tarball instead of the workspace source.
+import * as React from 'react';
+import { StyleSheet, Text, View } from 'react-native';
+import TabView, {
+ SceneMap,
+ useBottomTabBarHeight,
+ type AppleIcon,
+} from 'react-native-bottom-tabs';
+
+function HomeScreen() {
+ const tabBarHeight = useBottomTabBarHeight();
+ return (
+
+ Tab bar height: {tabBarHeight}
+
+ );
+}
+
+function SettingsScreen() {
+ return (
+
+ Settings
+
+ );
+}
+
+const renderScene = SceneMap({
+ home: HomeScreen,
+ settings: SettingsScreen,
+});
+
+const settingsIcon: AppleIcon = { sfSymbol: 'gear' };
+
+export default function App() {
+ const [index, setIndex] = React.useState(0);
+ const [routes] = React.useState([
+ { key: 'home', title: 'Home', focusedIcon: { sfSymbol: 'house' } },
+ {
+ key: 'settings',
+ title: 'Settings',
+ focusedIcon: settingsIcon,
+ badge: '3',
+ },
+ ]);
+
+ return (
+
+ );
+}
+
+const styles = StyleSheet.create({
+ screen: { flex: 1, alignItems: 'center', justifyContent: 'center' },
+});
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index afe3d690..c311899d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,6 +11,10 @@ on:
types:
- checks_requested
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
lint:
runs-on: ubuntu-latest
@@ -27,6 +31,9 @@ jobs:
- name: Lint files
run: yarn lint
+ - name: Test files
+ run: yarn test
+
- name: Typecheck files
run: yarn typecheck
@@ -52,6 +59,23 @@ jobs:
- name: Build package
run: yarn build
+ # npm pack matches the file list that changeset publish (npm publish) ships.
+ - name: Pack packages
+ run: |
+ mkdir -p "$RUNNER_TEMP/packages"
+ npm pack --pack-destination "$RUNNER_TEMP/packages" \
+ ./packages/react-native-bottom-tabs \
+ ./packages/react-navigation \
+ ./packages/expo-template
+
+ - name: Upload packages
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
+ with:
+ name: packages
+ path: ${{ runner.temp }}/packages
+ if-no-files-found: error
+ retention-days: 7
+
build-android:
runs-on: ubuntu-latest
env:
@@ -181,3 +205,206 @@ jobs:
ARCHS=arm64 \
CODE_SIGNING_ALLOWED=NO \
build
+
+ # Creates an app from the packed Expo template and installs the packed
+ # packages into it, as `create-expo-app --template @bottom-tabs/expo-template`
+ # users do, so nothing resolves through the workspace or src/.
+ consumer-expo:
+ name: consumer-expo (${{ matrix.platform }})
+ needs: build-library
+ runs-on: ${{ matrix.runner }}
+ timeout-minutes: 60
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - platform: ios
+ runner: macos-26
+ - platform: android
+ runner: ubuntu-latest
+
+ env:
+ APP_DIR: ${{ github.workspace }}/../consumer
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+
+ - name: Setup Node.js
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
+ with:
+ node-version-file: .nvmrc
+
+ - name: Download packages
+ uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
+ with:
+ name: packages
+ path: ${{ runner.temp }}/packages
+
+ - name: Create app from the Expo template
+ run: |
+ npx --yes create-expo-app@latest "$APP_DIR" --template "$RUNNER_TEMP"/packages/bottom-tabs-expo-template-*.tgz --no-install
+
+ # The template pins the published versions; the tarballs replace them.
+ - name: Install packages from tarballs
+ working-directory: ${{ env.APP_DIR }}
+ run: |
+ npm install "$RUNNER_TEMP"/packages/react-native-bottom-tabs-*.tgz "$RUNNER_TEMP"/packages/bottom-tabs-react-navigation-*.tgz
+
+ - name: Typecheck
+ working-directory: ${{ env.APP_DIR }}
+ run: npx tsc --noEmit
+
+ - name: Bundle JavaScript
+ working-directory: ${{ env.APP_DIR }}
+ run: npx expo export --platform ${{ matrix.platform }}
+
+ - name: Install JDK
+ if: matrix.platform == 'android'
+ uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
+ with:
+ distribution: 'zulu'
+ java-version: '17'
+
+ - name: Setup Gradle
+ if: matrix.platform == 'android'
+ uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
+
+ - name: Finalize Android SDK
+ if: matrix.platform == 'android'
+ run: |
+ /bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null"
+
+ # Runs the library's config plugin from the tarball's app.plugin.js.
+ - name: Prebuild
+ working-directory: ${{ env.APP_DIR }}
+ run: npx expo prebuild --platform ${{ matrix.platform }}
+
+ # BottomNavigationView crashes on launch without a Material theme.
+ - name: Check the config plugin set the Material 3 theme
+ if: matrix.platform == 'android'
+ working-directory: ${{ env.APP_DIR }}
+ run: |
+ grep 'parent="Theme.Material3.DayNight.NoActionBar"' android/app/src/main/res/values/styles.xml
+
+ - name: Build for iOS
+ if: matrix.platform == 'ios'
+ working-directory: ${{ env.APP_DIR }}
+ run: |
+ xcodebuild -workspace ios/consumer.xcworkspace -scheme consumer -configuration Release -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -derivedDataPath ios/build ARCHS=arm64 CODE_SIGNING_ALLOWED=NO -quiet
+
+ - name: Build for Android
+ if: matrix.platform == 'android'
+ working-directory: ${{ env.APP_DIR }}/android
+ env:
+ JAVA_OPTS: '-XX:MaxHeapSize=6g'
+ run: |
+ ./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a --no-daemon --console=plain
+
+ # Installs the packed library into a fresh React Native Community CLI app on
+ # the example's React Native version, as bare React Native users do.
+ consumer-cli:
+ name: consumer-cli (${{ matrix.platform }})
+ needs: build-library
+ runs-on: ${{ matrix.runner }}
+ timeout-minutes: 60
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ # Only Xcode 27 compiles the `#if compiler(>=6.4)` iOS 27 SDK code;
+ # consumer-expo builds with Xcode 26.
+ - platform: ios
+ runner: xcode-27
+ - platform: android
+ runner: ubuntu-latest
+
+ env:
+ APP_DIR: ${{ github.workspace }}/../consumer
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+
+ - name: Setup Node.js
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
+ with:
+ node-version-file: .nvmrc
+
+ - name: Download packages
+ uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
+ with:
+ name: packages
+ path: ${{ runner.temp }}/packages
+
+ - name: Create app on the example's React Native version
+ run: |
+ version=$(node -p "require('./apps/example/package.json').dependencies['react-native'].replace(/^\D+/, '')")
+ npx --yes @react-native-community/cli@latest init Consumer --version "$version" --directory "$APP_DIR" --skip-install --skip-git-init
+
+ - name: Install library from tarball
+ working-directory: ${{ env.APP_DIR }}
+ run: |
+ cp "$GITHUB_WORKSPACE/.github/consumer/App.tsx" App.tsx
+ npm install "$RUNNER_TEMP"/packages/react-native-bottom-tabs-*.tgz
+
+ - name: Typecheck
+ working-directory: ${{ env.APP_DIR }}
+ run: npx tsc --noEmit
+
+ - name: Bundle JavaScript
+ working-directory: ${{ env.APP_DIR }}
+ run: |
+ npx react-native bundle --entry-file index.js --platform ${{ matrix.platform }} --dev false --bundle-output "$RUNNER_TEMP/index.bundle"
+
+ - name: Install pods
+ if: matrix.platform == 'ios'
+ working-directory: ${{ env.APP_DIR }}
+ run: |
+ bundle install
+ cd ios && bundle exec pod install
+
+ - name: Build for iOS
+ if: matrix.platform == 'ios'
+ working-directory: ${{ env.APP_DIR }}
+ run: |
+ xcodebuild -workspace ios/Consumer.xcworkspace -scheme Consumer -configuration Release -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -derivedDataPath ios/build ARCHS=arm64 CODE_SIGNING_ALLOWED=NO -quiet
+
+ - name: Install JDK
+ if: matrix.platform == 'android'
+ uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
+ with:
+ distribution: 'zulu'
+ java-version: '17'
+
+ - name: Setup Gradle
+ if: matrix.platform == 'android'
+ uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
+
+ - name: Finalize Android SDK
+ if: matrix.platform == 'android'
+ run: |
+ /bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null"
+
+ # Inherits the Material 3 theme as the Quick Start tells CLI users to, and
+ # turns on AGP 9's built-in Kotlin, which the template still opts out of
+ # and AGP 10 makes mandatory. consumer-expo (AGP 8) covers the library
+ # applying kotlin-android itself.
+ - name: Configure Android app
+ if: matrix.platform == 'android'
+ working-directory: ${{ env.APP_DIR }}/android
+ run: |
+ sed -i 's/Theme.AppCompat.DayNight.NoActionBar/Theme.Material3.DayNight.NoActionBar/' app/src/main/res/values/styles.xml
+ grep 'parent="Theme.Material3.DayNight.NoActionBar"' app/src/main/res/values/styles.xml
+ sed -i '/^android.builtInKotlin=false/d; /^android.newDsl=false/d' gradle.properties
+ sed -i '/apply plugin: "org.jetbrains.kotlin.android"/d' app/build.gradle
+
+ - name: Build for Android
+ if: matrix.platform == 'android'
+ working-directory: ${{ env.APP_DIR }}/android
+ env:
+ JAVA_OPTS: '-XX:MaxHeapSize=6g'
+ run: |
+ ./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a --no-daemon --console=plain