Skip to content

Setup guides · React Native

Google Play closed testing for React Native apps

For bare React Native and React Native CLI projects: create an upload key, build a signed bundle with Gradle, set up the closed track and keep 12 testers opted in for 14 days. Using Expo with EAS Build? Read the Expo guide instead.

Build
./gradlew bundleRelease
Version code
android/app/build.gradle → versionCode
Target API
targetSdkVersion = 36

Build a release app bundle

React Native packs your JavaScript into the native Android build, so the release .aab comes out of Gradle. Set up signing first (next section), then:

  1. Check the package name

    applicationId in android/app/build.gradle is permanent once you upload to Google Play. Make sure it's the one you want.

  2. Build

    From the project root run npx react-native build-android --mode=release, or call Gradle directly as below. Both run the bundleRelease task and bundle the JavaScript into the app.

  3. Find the file

    The bundle is written to android/app/build/outputs/bundle/release/app-release.aab.

  4. Test the release build

    Uninstall the debug app, then install the release variant on a real device with the second command below. Release-only crashes show up here, not in Play Console.

Terminal
cd android
./gradlew bundleRelease
Terminal
npm run android -- --mode="release"

Don't set org.gradle.configureondemand=true in gradle.properties. It can make the release build skip bundling JavaScript and assets, and testers get an app that can't start.

Upload key and Play App Signing

Google Play requires Play App Signing for new apps: Google keeps the app signing key, and you sign uploads with an upload key. Generate one with keytool, which ships with the JDK:

Terminal
keytool -genkeypair -v -storetype PKCS12 -keystore my-upload-key.keystore \
  -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000

Move the file to android/app/ and put the credentials in ~/.gradle/gradle.properties so they never end up in git:

~/.gradle/gradle.properties
MYAPP_UPLOAD_STORE_FILE=my-upload-key.keystore
MYAPP_UPLOAD_KEY_ALIAS=my-key-alias
MYAPP_UPLOAD_STORE_PASSWORD=*****
MYAPP_UPLOAD_KEY_PASSWORD=*****

Then add a release signing config in android/app/build.gradle and use it in the release build type. The template's release build type signs with the debug key until you change this:

android/app/build.gradle
android {
    signingConfigs {
        release {
            if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) {
                storeFile file(MYAPP_UPLOAD_STORE_FILE)
                storePassword MYAPP_UPLOAD_STORE_PASSWORD
                keyAlias MYAPP_UPLOAD_KEY_ALIAS
                keyPassword MYAPP_UPLOAD_KEY_PASSWORD
            }
        }
    }
    buildTypes {
        release {
            // ...
            signingConfig signingConfigs.release
        }
    }
}

Version code and target API level

Each upload needs a higher versionCode than everything you have uploaded before. In a bare project it lives in android/app/build.gradle; versionName is the label users see:

android/app/build.gradle
android {
    defaultConfig {
        versionCode 2
        versionName "1.0.1"
    }
}

Since 31 August 2026, new apps and updates must target Android 16 (API level 36). The SDK levels sit in the ext block of android/build.gradle (compileSdkVersion, targetSdkVersion). New React Native releases raise them. On an older version, upgrading React Native is usually safer than raising targetSdkVersion alone, because your native libraries have to be compatible too.

React Native pitfalls

  • Debug-signed bundle. Forget to switch the release build type to signingConfigs.release and Play Console rejects the upload as debug-signed.
  • R8/ProGuard surprises. With enableProguardInReleaseBuilds = true, libraries that rely on reflection can crash only in release. Add their keep rules to android/app/proguard-rules.pro and test the release build on a device.
  • Unreadable crash reports. Hermes, the default engine, runs precompiled bytecode, so release stack traces point into the compiled bundle. Keep the source maps from each build you upload so you can symbolicate what testers send.
  • The app looks for Metro. A release build that tries to reach the development server means the JavaScript wasn't bundled. Check the configureondemand setting and build again.
  • Native modules and 16 KB pages. Libraries with native .so files must support 16 KB memory pages. Play Console warns about it, and from 1 February 2027 updates without support can't be released. Update them before the test starts.
  • Lost keystore. Back up the keystore and its passwords. You can request an upload key reset in Play Console, but it takes time you don't want to lose in the middle of a test.

Symbolicate crashes from testers

A JavaScript crash in a release build reaches you as a Hermes bytecode position such as p@1:132161, which says nothing on its own. React Native generates source maps for Android release builds by default; what matters is keeping the right map for every bundle you upload.

  1. Archive the source map

    After each release build, copy the map from android/app/build/generated/sourcemaps/react/release/ and label it with the version code. It must match the exact commit of the crashing build: even small code changes move the offsets.

  2. Run metro-symbolicate

    Save the tester's stack trace to a file, or read it straight from adb logcat, and pipe it through metro-symbolicate with that map. It turns bytecode offsets back into file names, lines and function names.

  3. Check native frames in Play Console

    Crashes in Java or Kotlin code from native modules appear under Monitor and improve → Android vitals → Crashes & ANRs. With R8 enabled, the mapping file inside the .aab makes them readable without an extra upload.

Terminal
# From a saved stack trace
npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.map < stacktrace.txt

# From a connected device
adb logcat -d | npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.map

If metro-symbolicate exits at once without output, it didn't get any input: pass the trace through a pipe or a redirect, not by typing it into the terminal.

Play Console in short: from closed test to production

Once your React Native bundle is built and signed, the rest happens in Play Console and works the same for every framework. This is the checklist; the full Play Console walkthrough covers every menu and screen.

  • Finish app setup on the Dashboard: privacy policy, app access, ads, content rating, target audience, data safety and store listing.
  • Create a closed testing release under Test and release → Testing → Closed testing: choose your testers' countries, upload the .aab and send it for review.
  • Add testers with a Google Group on the Testers tab and share the opt-in link. It works once the track shows Published.
  • Keep at least 12 testers opted in for 14 days in a row. Internal testing doesn't count, and someone who leaves and rejoins starts their 14 days again.
  • Ship at least one update during the test and write down what testers found and what you changed.
  • Apply for production from the Dashboard and answer the three sections about your test, your app and your readiness. The decision is Google's; nobody can guarantee approval.

Plan for drop-outs: recruit more than 12 testers so one person leaving doesn't push back your timeline. See the full Play Console steps.

The full Play Console walkthrough

Closed track, Google Group and opt-in link, the 12 testers / 14 days rule and the production application, step by step on one page.

Open the Play Console steps

Ship updates during the test

For React Native: raise versionCode (and versionName), build again and upload the new bundle to the same closed track.

Terminal
cd android
./gradlew bundleRelease

Uninstall the debug app before installing the release variant with npm run android -- --mode="release"; Android won't replace an app signed with a different key. Repeat that check before every upload, because release-only problems such as missing keep rules never show up in debug.

Over-the-air JavaScript updates are a gray area many teams ask about. Google Play's Device and Network Abuse policy says an app may not update itself outside Google Play, but the restriction doesn't apply to code that runs in a virtual machine or an interpreter, such as JavaScript. Native code, new permissions and native modules always need a new bundle through Google Play.

  • Keep the closed track as the main path. A new versionCode on the closed track reaches every opted-in tester through Google Play and shows up in your release history.
  • Keep native dependencies still. Upgrading React Native or a native module mid-test changes far more than the bug you're fixing. Do it before the test or after production access.
  • Keep one map per release. Every rebuild produces a new source map. Store it with the version code, or the next crash report from that version can't be decoded.

Frequently asked questions

Do I need Hermes for Google Play?

No, but it's React Native's default engine and the one most apps ship with. It precompiles JavaScript to bytecode, which speeds up startup and is also why release stack traces need a source map to be readable.

Which file do I upload to Play Console?

The bundle at android/app/build/outputs/bundle/release/app-release.aab. npx react-native build-android --mode=release runs Gradle's bundleRelease task and packs all of your JavaScript into it.

Why does my layout slide under the status bar after raising targetSdk?

Apps that target Android 15 (API level 35) or higher are drawn edge to edge. react-native-safe-area-context already handles this; without it, add safe-area insets to every screen before testers see it.

Does React Native support 16 KB memory pages?

Yes, since React Native 0.77. Older versions, and native modules that ship their own .so files, may not. Upgrade before the test starts rather than in the middle of it.

Sources

Technical details were checked against the official documentation below. Menus and defaults change between versions; when in doubt, the official page wins.

New to the 12 testers rule?

The general guide explains Google's requirement, how a TestBunny campaign runs day by day and what we can and can't guarantee.

Read the general guide

Need 12 real testers for 14 days?

Add your app and closed testing link, pick Starter (15 testers) or Pro (25 testers), and follow every tester's activity day by day in your panel.

TestBunny isn't affiliated with Google. Testers earn credits, never cash. Nobody can guarantee Google's production access decision, and we don't.