Vibe code to app stores

Android & iOS on macOS

17 min read

A step-by-step guide for shipping a vibe-coded React Native or Expo app to the Google Play Store and/or the Apple App Store from a Mac — covering Xcode and Android Studio setup, prebuild, signing, and the full submission process for each store.

If you're using Lunadeck, the build and signing steps are handled for you automatically. This guide is for building and publishing locally, or for understanding what Lunadeck does under the hood.

What you'll need before starting:

  • A Mac running macOS Sonoma 14 or later (Sequoia 15 required for Xcode 26)
  • At least 30 GB of free disk space (Xcode ~12 GB, Android Studio ~10 GB, plus SDKs and simulators)
  • At least 8 GB of RAM (16 GB recommended)
  • A credit/debit card ($25 one-time for Google Play; $99/year for Apple Developer Program)
  • Your exported React Native / Expo project (ZIP from your AI coding tool, or a git clone)
  • About 2–4 hours for initial setup, then 1–4 weeks for publishing

Part 1: Install the Required Tools

You need: Xcode (iOS builds), Android Studio (Android builds), Node.js, Java (JDK 17), and Watchman (required by React Native's Metro bundler on macOS). Homebrew handles most of this.

1.1 Install Homebrew

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

After installation, follow the printed instructions to add Homebrew to your PATH. On Apple Silicon:

echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"

Verify:

brew --version
brew install curl wget unzip git

1.2 Install Xcode (iOS builds only)

Step 1: Install from the Mac App Store — search for "Xcode" and install it (~12 GB download). Or download from https://developer.apple.com/xcode/.

Step 2: Install command line tools:

xcode-select --install
sudo xcodebuild -license accept

Verify:

xcode-select -p
# Should output: /Applications/Xcode.app/Contents/Developer

Step 3: Open Xcode once and let it install additional components (simulators, etc.) before continuing.

1.3 Install Node.js (v18+)

Option A: Using nvm (recommended)

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.zshrc
nvm install 22
nvm use 22
node --version   # Should show v22.x.x

Option B: Using Homebrew

brew install node@22
node --version

1.4 Install Watchman

Watchman is required by React Native's Metro bundler on macOS to watch files for changes:

brew install watchman

1.5 Install Java (JDK 17) — Android only

brew install openjdk@17
 
# Symlink so macOS can find it
sudo ln -sfn $(brew --prefix openjdk@17)/libexec/openjdk.jdk \
  /Library/Java/JavaVirtualMachines/openjdk-17.jdk
 
java -version

Add JAVA_HOME to your ~/.zshrc:

export JAVA_HOME=$(/usr/libexec/java_home -v 17)

Reload:

source ~/.zshrc
java -version

1.6 Install Android Studio — Android only

Step 1: Download the macOS .dmg from https://developer.android.com/studio, drag it to Applications, and launch it. On first launch, run through the setup wizard (choose Standard, let it download the SDK and emulator).

Step 2: Install SDK components — go to Tools > SDK Manager:

  • SDK Platforms: check Android 14 (API 34) or newer
  • SDK Tools: check Android SDK Build-Tools, Android SDK Command-line Tools, Android Emulator, Android SDK Platform-Tools

Step 3: Set environment variables — add to your ~/.zshrc:

export ANDROID_HOME="$HOME/Library/Android/sdk"
export ANDROID_SDK_ROOT="$HOME/Library/Android/sdk"
export PATH="$PATH:$ANDROID_HOME/platform-tools"
export PATH="$PATH:$ANDROID_HOME/tools"
export PATH="$PATH:$ANDROID_HOME/tools/bin"
export PATH="$PATH:$ANDROID_HOME/cmdline-tools/latest/bin"

Then reload and verify:

source ~/.zshrc
adb --version

Apple Silicon note: The Android emulator runs natively on ARM. When creating virtual devices, pick ARM-based system images (not x86).


Part 2: Export and Prepare Your Project

2.1 Get your project files

If your AI tool provides a ZIP export, download and unzip it:

mkdir ~/my-app
cd ~/my-app
unzip ~/Downloads/your-project.zip -d .

If you're working from a git repository:

git clone https://github.com/yourname/your-app.git ~/my-app
cd ~/my-app

2.2 Install dependencies

cd ~/my-app
npm install

Common issues with AI-generated exports:

  • Missing .env variables — create a .env file with any required API keys
  • Dependency conflicts — delete node_modules and package-lock.json, then re-run npm install
  • TypeScript errors — fix the specific file and line number the error points to

2.3 Confirm your project type

Open package.json and look at "dependencies":

  • Contains "expo"Expo managed workflow — follow Part 3A
  • Contains "react-native" but not "expo"Bare React Native — follow Part 3B

Part 3A: Expo Managed Workflow

Skip to Part 3B if your project is bare React Native.

3A.1 Check your app.json

Open app.json and verify the "expo" section contains at minimum:

{
  "expo": {
    "name": "My App",
    "slug": "my-app",
    "android": {
      "package": "com.yourname.myapp"
    },
    "ios": {
      "bundleIdentifier": "com.yourname.myapp"
    }
  }
}

The android.package and ios.bundleIdentifier values are permanent. Once you publish to a store, you cannot change them. Use a reverse-domain format: com.yourname.appname. They are typically the same value.

If either field is missing, add them now.

3A.2 Run prebuild

Expo prebuild reads your app.json and generates both the native android/ and ios/ project directories:

# Both platforms at once:
npx expo prebuild
 
# Or individual platforms:
npx expo prebuild --platform android
npx expo prebuild --platform ios

If android/ or ios/ already exist, prebuild will ask if you want to overwrite them. Answer yes unless you've made manual changes — manual changes are not preserved through prebuild.

3A.3 Install iOS dependencies

Expo uses CocoaPods or Swift Package Manager to manage iOS native dependencies. For CocoaPods (the default for many projects):

cd ios
pod install
cd ..

If the project uses Swift Package Manager, pod install is not required.


Part 3B: Bare React Native

Skip this if your project is Expo-managed.

3B.1 Verify the native directories

Your project should already have android/ and ios/ directories:

ls android/ ios/

If either is missing, your project may actually be Expo — check package.json dependencies and follow Part 3A.

3B.2 Install iOS dependencies

cd ios
pod install
cd ..

Part 4: Test Your App

4.1 Test on Android

Open two separate terminal windows:

Terminal 1 — Metro bundler:

cd ~/my-app
npx react-native start

Terminal 2:

cd ~/my-app
npx react-native run-android

Emulator: In Android Studio > Device Manager, create a virtual device (Pixel 7, API 34+, pick the ARM image on Apple Silicon). Click Run (▶).

Physical device: Enable Developer Options (Settings > About Phone > tap Build Number 7 times), enable USB Debugging, connect via USB.

4.2 Test on iOS

Simulator:

npx react-native run-ios

Or open Xcode:

npx expo run:ios   # Expo projects
# or:
open ios/YourApp.xcworkspace   # bare RN (use .xcworkspace, not .xcodeproj)

Select a simulator (e.g., "iPhone 16") from the device dropdown in Xcode and press Cmd + R.

Physical device: Connect your iPhone. In Xcode, select the "App" target > Signing & Capabilities > select your Apple team. A free Apple ID works for testing (builds expire every 7 days; the paid Developer Program removes this limit and is required for App Store distribution).

4.3 Common issues

Metro bundler not starting — start it explicitly: npx react-native start

"SDK location not found" (Android)ANDROID_HOME is not set. Follow Part 1.6 Step 3 and run source ~/.zshrc.

App crashes immediately (Android) — check Logcat in Android Studio (View > Tool Windows > Logcat).

Red error screen — a JavaScript error. Read the message — it names the file and line. Fix it and press r in the Metro terminal to reload.

iOS build fails with "No signing certificate" — select your Apple Developer team in Xcode > Signing & Capabilities. A free Apple ID works for testing.

iOS Simulator missing — in Xcode, go to Settings > Platforms and download the iOS Simulator runtime.

"pod: command not found" — install CocoaPods:

brew install cocoapods

Part 5: Prepare for Release

5.1 Set your app icon and splash screen

Expo projects: set paths in app.json and re-run prebuild:

{
  "expo": {
    "icon": "./assets/icon.png",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#FFFFFF"
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#FFFFFF"
      }
    },
    "ios": {
      "icon": "./assets/icon.png"
    }
  }
}
  • icon.png must be at least 1024×1024 px, no transparency, no rounded corners (Apple applies rounding automatically)
  • splash.png should be at least 2048×2048 px

After updating app.json:

npx expo prebuild
cd ios && pod install && cd ..

Bare React Native: use Android Studio's File > New > Image Asset wizard for Android; replace files in ios/YourApp/Images.xcassets for iOS.

5.2 Update version numbers

Android — in android/app/build.gradle:

versionCode 1       // Integer, must increase with every Play Store upload
versionName "1.0"   // Human-readable string shown to users

iOS — in Xcode (App target > General tab):

  • Version: user-visible version number (e.g., 1.0.0)
  • Build: incrementing number per upload (e.g., 1)

Or edit ios/App/Info.plist directly:

<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleVersion</key>
<string>1</string>

For Expo projects you can also set these in app.json:

{
  "expo": {
    "version": "1.0.0",
    "android": { "versionCode": 1 },
    "ios": { "buildNumber": "1" }
  }
}

5.3 Configure permissions

Android — in android/app/src/main/AndroidManifest.xml:

<!-- Internet access — required for most apps -->
<uses-permission android:name="android.permission.INTERNET" />
 
<!-- Add only what your app actually uses: -->
<!-- <uses-permission android:name="android.permission.CAMERA" /> -->
<!-- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> -->
<!-- <uses-permission android:name="android.permission.RECORD_AUDIO" /> -->

iOS — in ios/YourApp/Info.plist, add usage description strings for each permission. Apple rejects apps with missing or vague descriptions:

<!-- Add only for permissions your app actually uses: -->
<key>NSCameraUsageDescription</key>
<string>This app uses the camera to take photos for your profile.</string>
 
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app uses your location to show nearby results.</string>
 
<key>NSMicrophoneUsageDescription</key>
<string>This app uses the microphone for voice messages.</string>
 
<key>NSPhotoLibraryUsageDescription</key>
<string>This app accesses your photo library to let you choose images.</string>

Part 6: Sign and Build the Android Release Bundle

6.1 Generate a signing key

keytool -genkey -v -keystore ~/my-app-release.keystore \
  -alias my-app-key \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000

Back up this keystore file and both passwords. Losing it means you can never update your app on Google Play. Store a copy in a password manager and on external storage.

6.2 Configure Gradle signing

Create android/keystore.properties (do NOT commit this to git — add it to .gitignore):

storeFile=/Users/YOUR_USERNAME/my-app-release.keystore
storePassword=your_keystore_password
keyAlias=my-app-key
keyPassword=your_key_password

Edit android/app/build.gradle — add above the android { block:

def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

Inside the android { block:

android {
    // ... existing config ...
 
    signingConfigs {
        release {
            storeFile file(keystoreProperties['storeFile'])
            storePassword keystoreProperties['storePassword']
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
        }
    }
 
    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

6.3 Build the release bundle

cd ~/my-app/android
./gradlew bundleRelease

Output: android/app/build/outputs/bundle/release/app-release.aab


Part 7: Sign and Build the iOS Release Archive

iOS code signing requires an Apple Developer Program membership ($99/year).

7.1 Enroll in the Apple Developer Program

  1. Go to https://developer.apple.com/programs/
  2. Click "Enroll" and sign in with your Apple ID
  3. Choose Individual or Organization (Individual shows your legal name as seller; Organization requires a D-U-N-S number)
  4. Pay the $99/year fee
  5. Wait for identity verification (a few hours to a few days)

7.2 Configure signing in Xcode

open ios/YourApp.xcworkspace   # Use .xcworkspace, not .xcodeproj
  1. Select the main app target in the project navigator
  2. Go to Signing & Capabilities
  3. Check "Automatically manage signing" and select your Apple Developer team

Xcode creates the provisioning profile and signing certificate automatically.

7.3 Build the archive

Option A: Xcode GUI (easier)

  1. Set the device target to "Any iOS Device (arm64)" in the dropdown — not a simulator
  2. Go to Product > Archive
  3. When the Organizer window opens, select your archive and click Distribute App
  4. Choose App Store Connect > Upload and follow the prompts

Option B: Command line

cd ~/my-app
 
# Build the archive
xcodebuild clean archive \
  -workspace ios/YourApp.xcworkspace \
  -scheme YourApp \
  -configuration Release \
  -archivePath ~/my-app.xcarchive \
  -destination 'generic/platform=iOS'

Create ExportOptions.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>method</key>
    <string>app-store</string>
    <key>uploadSymbols</key>
    <true/>
</dict>
</plist>

Export the IPA:

xcodebuild -exportArchive \
  -archivePath ~/my-app.xcarchive \
  -exportPath ~/my-app-ipa/ \
  -exportOptionsPlist ExportOptions.plist

7.4 Upload to App Store Connect

  • From Xcode Organizer: available after using Product > Archive — click Distribute App
  • Transporter app: free on the Mac App Store — drag and drop your IPA
  • Command line: xcrun altool --upload-app --type ios --file ~/my-app-ipa/YourApp.ipa --apiKey YOUR_KEY_ID --apiIssuer YOUR_ISSUER_ID (API key created in App Store Connect under Users and Access > Integrations)

Part 8: Developer Accounts and Store Listings

8.1 Google Play Developer Account

  1. Go to https://play.google.com/console and sign in
  2. Choose Personal or Organization (Personal is fine for indie developers)
  3. Pay the one-time $25 USD fee and complete identity verification
  4. Wait 24–48 hours for activation

Personal accounts created after November 2023 require device verification. Install the Google Play Console mobile app on an Android device, sign in, and follow the steps.

8.2 App Store Connect

  1. Go to https://appstoreconnect.apple.com and sign in
  2. Click + > New App
  3. Fill in: Platform (iOS), Name, Primary language, Bundle ID (must match ios.bundleIdentifier in app.json or your Xcode project), and an internal SKU
  4. Click Create

Required fields for your app page:

  • Description — short and full
  • Screenshots — at minimum for 6.7" (iPhone 15 Pro Max) and 6.1" (iPhone 15 Pro); capture from the Simulator
  • App icon — 1024×1024 px, no transparency, no rounded corners
  • Category
  • Privacy policy URL — mandatory; a simple Notion or GitHub Pages page works
  • Age rating — fill out the content questionnaire

Part 9: Google Play — The Closed Testing Requirement

New personal developer accounts cannot publish directly to production. Google requires:

  1. At least 12 testers opted in to a closed test track
  2. Those testers remain opted in for 14 consecutive days
  3. Then you can apply for production access

Set up closed testing

  1. In Play Console, click Create app and work through all required sections (store listing, content rating, target audience, privacy policy)
  2. Go to Testing > Closed testing, create a tester email list with 12+ Gmail addresses
  3. Click Create new release, accept Play App Signing, upload your .aab, add release notes, click Start rollout
  4. Share the generated opt-in link with your testers — they must click it on an Android device and install the app

After 14 days with all testers opted in, go to the app Dashboard and click Apply for production access.


Part 10: App Store — Review Process

Apple's review is faster than Google's testing requirement (most reviews complete within 24 hours), but the bar is higher for certain types of apps.

App Store review for React Native apps

Unlike Capacitor (which wraps a WebView), React Native renders native components — so you won't run into the Guideline 4.2 WebView rejection issue. Apple reviews React Native apps much like any other native app. The main requirements:

  • Your app must work correctly and not crash during review
  • Usage description strings in Info.plist must be present for every permission your app uses
  • A privacy policy URL is required if your app collects any user data
  • If your app has login, include demo credentials in the App Review Information section

TestFlight (beta testing)

Before submitting to production, use TestFlight:

  1. Upload a build to App Store Connect
  2. Go to the TestFlight tab
  3. Internal testers (up to 100 App Store Connect team members): available almost immediately, no review needed
  4. External testers (up to 10,000 via email or public link): the first build of each version requires a brief beta review (~24 hours)

TestFlight builds expire after 90 days.

Submitting for production review

  1. In App Store Connect, select your build
  2. Fill out App Review Information — provide demo credentials if your app has login
  3. Click Submit for Review

Apple reviews ~90% of submissions within 24 hours.

If your app gets rejected

Common fixes:

  • Missing privacy policy: add a URL to a hosted privacy policy
  • Missing usage descriptions: add the NS...UsageDescription keys to Info.plist
  • Crashes during review: fix the crash — reviewer crash logs appear in the Resolution Center
  • App doesn't work as described: make sure your store listing accurately describes what the app does

Part 11: Publish to Production

Google Play

Once you have production access:

  1. Go to Production > Create new release
  2. Upload your .aab, add release notes, select distribution countries
  3. Click Start rollout to production

App Store

Once Apple approves your submission, choose:

  • Immediate release — goes live as soon as approved (default)
  • Manual release — you choose when to publish
  • Scheduled release — set a specific date

Part 12: Updating Your App

Expo projects:

# 1. Make your code changes
 
# 2. If you changed app.json or added/removed native packages:
npx expo prebuild
cd ios && pod install && cd ..
 
# 3. Bump version numbers:
#    Android: versionCode in android/app/build.gradle
#    iOS: Build number in Xcode or Info.plist
 
# Android release
cd android
./gradlew bundleRelease
# Upload new .aab to Google Play Console
 
# iOS release
# Open Xcode > Product > Archive > Distribute
# Or via command line: xcodebuild archive + exportArchive

Bare React Native:

# 1. Make your code changes
# 2. Bump version numbers
# 3. Build Android: cd android && ./gradlew bundleRelease
# 4. Build iOS: Product > Archive in Xcode

Quick Reference

# === EXPO: ONE-TIME SETUP ===
npm install
npx expo prebuild
cd ios && pod install && cd ..
 
# === TESTING ===
npx react-native start          # Metro bundler
npx react-native run-android    # Android
npx react-native run-ios        # iOS Simulator
 
# === ANDROID RELEASE ===
cd android && ./gradlew bundleRelease
# Output: android/app/build/outputs/bundle/release/app-release.aab
 
# === iOS RELEASE (Xcode GUI) ===
open ios/YourApp.xcworkspace
# Product > Archive > Distribute App > App Store Connect > Upload
 
# === iOS RELEASE (command line) ===
xcodebuild clean archive \
  -workspace ios/YourApp.xcworkspace \
  -scheme YourApp \
  -archivePath ~/my-app.xcarchive \
  -destination 'generic/platform=iOS'
xcodebuild -exportArchive \
  -archivePath ~/my-app.xcarchive \
  -exportPath ~/my-app-ipa/ \
  -exportOptionsPlist ExportOptions.plist

Troubleshooting

"SDK location not found" (Android)ANDROID_HOME is not set. Follow Part 1.6 Step 3 and run source ~/.zshrc.

"JAVA_HOME is not set" (Android) — add to ~/.zshrc:

export JAVA_HOME=$(/usr/libexec/java_home -v 17)

Then run source ~/.zshrc.

Gradle fails with "Could not determine java version" — multiple Java versions installed. Use /usr/libexec/java_home -V to list them and confirm JAVA_HOME points to JDK 17.

Xcode build fails with "No signing certificate" — select your Apple Developer team in Xcode > Signing & Capabilities. The paid Developer Program ($99/year) is required for distribution builds.

"pod: command not found"brew install cocoapods

pod install fails — check the error. Common causes: CocoaPods spec repo out of date (pod repo update), an incompatible plugin version, or a missing Xcode command line tools install.

npx expo prebuild fails — check for missing android.package / ios.bundleIdentifier in app.json, run npm install first, and check for dependency version conflicts.

iOS Simulator is slow — on Apple Silicon it should be fast. Try Device > Erase All Content and Settings in the Simulator menu and close other heavy apps (Xcode + Android Studio eat RAM quickly).

Red error screen in the app — a JavaScript error. Read the message and press r in the Metro terminal to reload after fixing it.

"Command PhaseScriptExecution failed" in Xcode — a build script failed. Expand the error in the Xcode build log to see the actual message.

"The request was denied by service delegate" when archiving — provisioning profile or certificate is invalid. In Xcode, toggle "Automatically manage signing" off and back on, or go to Settings > Accounts > select your team > Download Manual Profiles.


Realistic Expectations

The Google Play timeline is long. The 14-day testing requirement plus review means 3–4 weeks from start to live. The App Store is faster (a few days if you pass first try) but rejections add cycles.

The costs are different. Google Play: $25 one-time. Apple: $99 per year, every year. If your Apple Developer membership lapses, your apps are removed from the App Store within 24 hours.

AI-generated React Native code often has issues the AI couldn't test. Red screens, missing native modules, and platform-specific bugs are common. Budget time for debugging — it's normal.

React Native apps get easier App Store approval than WebView wrappers. Because your app renders native components, you won't face the Guideline 4.2 WebView rejection that Capacitor apps commonly encounter. However, your app still needs to be functional and comply with all other guidelines.

iOS and Android behave differently. A feature that works perfectly on Android may look wrong or crash on iOS. Test on both platforms before submitting to either store.