01Choosing how to build it
Use Expo with TypeScript, and build in the cloud with EAS Build. One code base gives you an Android APK and an iPhone build, Expo's push service handles both Google's and Apple's push systems for you, and the code that talks to the backend is shared with the version 2 website.
| Option | Language | One code base for both? | Push notifications | Fit for this project | Verdict |
|---|---|---|---|---|---|
| Expo (React Native) | TypeScript | Yes | Expo's service wraps both Google and Apple; one kind of token | Shares TypeScript, the API client and the React way of thinking with the website; cloud builds mean iPhone builds work before Xcode is even set up | Recommended |
| Flutter | Dart | Yes | Firebase plugin; you wire Apple push yourself | Great screens, but a second language and nothing shared with the website | Good second choice |
| Native (Kotlin + Swift) | Two | No, two apps | Platform tools directly | Best polish; double the work for a small app | Not for the first version |
| Progressive Web App | TypeScript | Yes, it is the website | Browser push works on Android, and on iPhone (16.4 or newer) once added to the home screen | Zero extra code; iPhone push is less reliable in the background | Ship this first as a stepping stone |
What "works on any mobile and any account" means here. The app never holds a BookMyShow token in its code. A user signs in to your backend (email link or Google), and the backend polls BookMyShow with no login. Seat checking, which needs a BookMyShow login, is done once per user inside the app through a web view of the normal BookMyShow login page. The resulting cookies go to the backend, exactly as the version 2 plan, section 6 describes.
Minimum versions. Expo SDK 52 or newer, Android 8.0 and iOS 15.1 as the oldest supported. Together they cover more than 95 % of phones in use in India in 2026.
02What to install on your computer
You can build the app from macOS, Linux or Windows. Android works fully on all three. For iPhone, the simulator and building on your own computer need a Mac, but Expo's cloud builds make iPhone apps from any computer, so a Windows or Linux user can still ship to iPhones and test on a real one.
| Tool | Why | Needed on | Check |
|---|---|---|---|
| Node.js 20 or newer | Runs Expo and the build tools | All | node --version |
| Expo CLI | Creates and runs the project | All (no install; use npx expo inside the project) |
npx expo --version |
| EAS CLI | Cloud builds, signing, store upload | All | eas whoami |
| Expo Go on your phone | Run the app while developing, without building | Your phone | Scan the QR code from npx expo start |
| Android Studio | Android tools and the virtual phone (emulator) | All | adb --version, emulator -list-avds |
| Java 17 | Needed by Android builds | All | java -version |
| Watchman | Notices file changes quickly while developing | macOS (optional on Linux, not needed on Windows) | watchman --version |
| Xcode (about 12 GB) | iPhone simulator, local iPhone builds | macOS only | xcodebuild -version |
| CocoaPods | iPhone packages for local builds | macOS only | pod --version |
Install them:
brew install watchman cocoapods
brew install --cask android-studio zulu@17
npm install -g eas-cli && eas login
# Xcode: install it from the Mac App Store, open it once and accept the licence, then:
sudo xcode-select -s /Applications/Xcode.appsudo apt install -y openjdk-17-jdk
sudo snap install android-studio --classic
npm install -g eas-cli && eas login
# Xcode and CocoaPods are macOS only. Use cloud builds for iPhone (section 8).winget install -e --id Microsoft.OpenJDK.17
winget install -e --id Google.AndroidStudio
npm install -g eas-cli; eas login
# Xcode and CocoaPods are macOS only. Use cloud builds for iPhone (section 8).Then open Android Studio once. In its SDK Manager, tick Android 14 (API 34), Android SDK Build-Tools, Platform-Tools and Emulator. In Device Manager, create one Pixel virtual phone.
Finally, tell your terminal where the Android tools are:
echo 'export ANDROID_HOME=$HOME/Library/Android/sdk' >> ~/.zshrc
echo 'export PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator' >> ~/.zshrc
source ~/.zshrc
adb --versionecho 'export ANDROID_HOME=$HOME/Android/Sdk' >> ~/.bashrc
echo 'export PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator' >> ~/.bashrc
source ~/.bashrc
adb --version$sdk = "$env:LOCALAPPDATA\Android\Sdk"
[Environment]::SetEnvironmentVariable("ANDROID_HOME", $sdk, "User")
$p = [Environment]::GetEnvironmentVariable("Path", "User")
[Environment]::SetEnvironmentVariable("Path", "$p;$sdk\platform-tools;$sdk\emulator", "User")
# Close PowerShell and open it again, then check:
adb --versionAccounts.
| Account | Cost | Needed for |
|---|---|---|
| Expo | Free (30 cloud builds a month) | Cloud builds, push service |
| Google Play Console | 25 USD once | Publishing in the Play Store; not needed for an APK you install yourself |
| Apple Developer Program | 99 USD a year | TestFlight and the App Store; also needed to run on a real iPhone for more than 7 days |
| Firebase project | Free | Google's push keys for Android (uploaded to Expo once) |
Follow Expo's own environment setup page for the exact current versions of Android Studio and Xcode. It has a separate tab for each system and is updated with every release.
03Project setup, step by step
About an hour from an empty folder to the app running on your own phone. The backend from the version 2 plan must be reachable over HTTPS before the sign-in screen works.
-
Open a terminal in the folder where you keep projects, then create the project (TypeScript, with tabs). This is the same on every system:
bashnpx create-expo-app@latest bms-alerts --template tabs cd bms-alerts -
Install the packages the app needs. Always use
npx expo installso the versions match your Expo release:bashnpx expo install expo-notifications expo-device expo-constants expo-secure-store expo-linking expo-web-browser react-native-webview @react-native-cookies/cookies npm install @tanstack/react-query zod -
Set the app's names in
app.json:json{ "expo": { "name": "BMS Alerts", "slug": "bms-alerts", "scheme": "bmsalerts", "ios": { "bundleIdentifier": "com.sriram.bmsalerts", "supportsTablet": false }, "android": { "package": "com.sriram.bmsalerts", "googleServicesFile": "./google-services.json" }, "plugins": ["expo-router", "expo-secure-store", ["expo-notifications", { "icon": "./assets/notification-icon.png" }]] } } -
Point the app at the backend with a line in
.env:EXPO_PUBLIC_API_URL=https://bms.yourdomain.in. Generate the typed API client from the backend's description:npx openapi-typescript https://bms.yourdomain.in/openapi.json -o lib/api.d.ts. -
Create the screens. Expo Router turns file names into screens:
textapp/ sign-in.tsx email link or Google connect-bms.tsx BookMyShow login in a web view (section 5) (tabs)/index.tsx dashboard of watches (tabs)/theatres.tsx theatre list with search (tabs)/settings.tsx push on/off, email, disconnect theatre/[code].tsx movies and shows at one theatre watch/new.tsx the watch form alert/[id].tsx one alert, with an Open in BookMyShow button lib/api.ts lib/auth.ts lib/push.ts -
Run it in Expo Go first:
npx expo start, scan the QR code with your phone. The theatre and movie screens work here. Push notifications and reading web view cookies do not, because Expo Go cannot include those native pieces. -
Make a "development build" for your phone and use that from now on. These commands are the same on every system:
basheas login eas init # links the project to your Expo account eas build:configure # writes eas.json with development, preview, production eas build --profile development --platform android # about 10 min in the cloud, gives an APK link npx expo start --dev-clientInstall the APK on your Android phone from the link, open it, and it connects to your computer over the same Wi-Fi. The iPhone version of this step needs the Apple Developer account (section 8).
Guides: Create a project, Expo Router, Development builds.
04Screens and flow
Two one-time screens (sign in, connect BookMyShow) lead to a three-tab app: Dashboard, Theatres, Settings. The everyday path is Theatres, then a theatre, then a new watch, then back to the dashboard. A push notification is the second way in: tapping it opens the alert screen, whose main button opens the BookMyShow seat page. Each box is one file under app/ from section 3.
05Talking to the backend and signing in
The app talks only to your FastAPI backend over HTTPS. It signs in to your backend, not to BookMyShow. The BookMyShow connection is a one-time login in a web view whose cookies are handed to the backend.
Signing in to your backend. Two ways, both handled by fastapi-users on the server:
- Email link: the user types an email, the backend sends a link like
bmsalerts://auth?token=.... Opening it launches the app (expo-linking), which swaps the link token for a sign-in token pair. - Google:
expo-web-browseropens the backend's/auth/google/authorizepage; when done it comes back to the samebmsalerts://authlink.
Keep the long-lived refresh token in expo-secure-store (the phone's locked storage), keep the short-lived access token in memory, and add the Authorization: Bearer header in one fetch wrapper in lib/api.ts. On a 401, refresh once, then sign out.
Connecting BookMyShow (any account). The screen connect-bms.tsx:
import { WebView } from 'react-native-webview';
import CookieManager from '@react-native-cookies/cookies';
<WebView
source={{ uri: 'https://in.bookmyshow.com/explore/home/hyderabad' }}
sharedCookiesEnabled
onNavigationStateChange={async () => {
const jar = await CookieManager.get('https://in.bookmyshow.com', true);
if (jar.ud?.value) { // this cookie exists only after login
await api.post('/api/bms/session', { cookies: { ud: jar.ud.value, bmsId: jar.bmsId?.value } });
router.replace('/(tabs)');
}
}}
/>The user sees the normal BookMyShow login (phone and OTP) inside the app, so it works for whoever is holding the phone, and nothing on that page is automated. The backend stores the cookies encrypted and uses them only to read seat maps for that user's watches. Add a Disconnect button on the settings tab that calls DELETE /api/bms/session and clears the web view's cookies with CookieManager.clearAll().
Keeping the app and the backend in step. The app and the website use the same generated client from openapi.json; regenerate it whenever the backend changes (npm run gen-api). With TanStack Query, keep theatre showtimes for 5 minutes and the theatre list for 24 hours, and refresh the dashboard whenever the app comes to the front.
Guides: expo-secure-store, Linking into your app, react-native-webview guide, @react-native-cookies/cookies.
06Push notifications
Use Expo's push service. The app registers one Expo push token with your backend, the backend sends to Expo, and Expo delivers through Google (FCM) on Android and Apple (APNs) on iPhone. The keys are set up once.
One-time key setup.
- Android: create a Firebase project, add an Android app with package
com.sriram.bmsalerts, downloadgoogle-services.jsoninto the project root (add it to.gitignore). Then in Firebase, Project settings, Service accounts, generate a private key and upload it witheas credentials(Android, "Google Service Account Key for FCM V1"). Guide: Expo: using FCM. - iPhone: with the Apple Developer account linked,
eas build --platform iosoffers to create the Apple push key for you; say yes. Nothing to download. - Backend:
pip install exponent_server_sdk. No keys needed for Expo's push service at this app's volume.
In the app, lib/push.ts.
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
Notifications.setNotificationHandler({
handleNotification: async () => ({ shouldShowBanner: true, shouldShowList: true, shouldPlaySound: true, shouldSetBadge: false }),
});
export async function registerForPush(api) {
if (!Device.isDevice) return; // simulators cannot receive push
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('alerts', {
name: 'Seat alerts', importance: Notifications.AndroidImportance.MAX, sound: 'default',
});
}
const { status } = await Notifications.requestPermissionsAsync();
if (status !== 'granted') return;
const projectId = Constants.expoConfig?.extra?.eas?.projectId;
const token = (await Notifications.getExpoPushTokenAsync({ projectId })).data; // ExponentPushToken[...]
await api.post('/api/push/register', { kind: 'expo', token });
}
// tapping a notification opens the alert screen
Notifications.addNotificationResponseReceivedListener(({ notification }) => {
const id = notification.request.content.data?.alertId;
if (id) router.push(`/alert/${id}`);
});On the backend (the worker's notify.py): build one message per push_device of the user with a title, a body, data={"alertId": id, "url": seat_layout_url}, channelId="alerts" and priority="high". Send with PushClient().publish_multiple. A minute later read the receipts and delete tokens that come back as DeviceNotRegistered. Guide: Sending notifications with Expo's push API.
Testing. Paste the token printed by the app into Expo's push tool and send a test. On Android, test with the app closed. On iPhone, a real phone with a development build is needed. The phone's operating system delivers the message, so the app does not have to be running.
Setup guide: Expo push notifications setup.
07Building and testing on Android
Android is the easier of the two: no paid account, it works the same from macOS, Linux and Windows, cloud builds produce an APK you can install on any phone, and a virtual Pixel on your computer covers daily work.
Daily development.
- Start a virtual phone from Android Studio (Device Manager, play button) or
emulator -avd Pixel_8_API_34, or plug in a phone with USB debugging on (adb deviceslists it). npx expo start --dev-client, pressato open the development build on the virtual or real phone. Changes show up instantly.- Push notifications only work on a real phone with Google Play services, or a virtual phone whose image says "Google Play".
Build profiles in eas.json.
{
"build": {
"development": { "developmentClient": true, "distribution": "internal", "android": { "buildType": "apk" } },
"preview": { "distribution": "internal", "android": { "buildType": "apk" } },
"production": { "android": { "buildType": "app-bundle" }, "autoIncrement": true }
}
}Builds.
eas build --profile preview --platform android # APK for friends and your own phone
eas build --profile production --platform android # AAB for the Play Store
eas build --profile preview --platform android --local # on your own computer; macOS or Linux only, needs Java 17 and the Android toolsThe first cloud build creates and keeps a signing key for you. Keep using EAS for signing so every build is signed the same way. A preview APK installs on any Android 8 or newer phone from the build link (allow "install unknown apps" for the browser once).
Checklist on a real phone.
- Sign in, connect BookMyShow in the web view, confirm the backend shows the session as
ok. - Create a watch for a show today; force a test alert from the backend (
POST /api/dev/alerts/test) and confirm the push arrives with the app closed and the screen locked. - Tap the notification: the alert screen opens, and "Open in BookMyShow" opens the BookMyShow app or site on the seat page.
- Battery: Android may kill background apps, but push delivery does not depend on the app running. On Xiaomi, Oppo and Samsung phones, still set the app to "Unrestricted" under Settings, Battery.
Guides: Build APKs for emulators and devices, Android emulator setup, Local builds.
08Building and testing on iPhone
iPhone needs the Apple Developer Program (99 USD a year) for anything beyond the simulator. With it, Expo builds and signs in the cloud, and TestFlight puts the app on your iPhone and on up to 10,000 testers' phones.
On Windows there is no iPhone simulator. Skip to "With the account" below: Expo builds the iPhone app in the cloud, and you install it on a real iPhone from a QR code. Everything in this section after the simulator works the same from Windows.
On Linux there is no iPhone simulator. Skip to "With the account" below: Expo builds the iPhone app in the cloud, and you install it on a real iPhone from a QR code. Everything in this section after the simulator works the same from Linux.
Without a paid account (Mac only). Install the full Xcode, then npx expo run:ios builds and opens the app in the iPhone simulator. Everything except push notifications and the camera works there. The web view BookMyShow login works in the simulator too, so the connect flow can be tested without a phone.
With the account.
- Enroll at developer.apple.com with your Apple ID (approval takes a day or two).
eas build --profile development --platform ios: Expo asks to log in with your Apple ID, registers the app, creates the certificates and the push key, and keeps them. Register your iPhone when asked (eas device:createsends a link to open on the phone).- Install the development build on the iPhone from the build page (a QR code), then
npx expo start --dev-clientas on Android. - For a build to share:
eas build --profile preview --platform ios(up to 100 registered phones) or go straight to TestFlight with the production profile (section 9).
Local iPhone builds (eas build --platform ios --local) need a Mac with Xcode, CocoaPods and the same certificates. Use them only when the cloud queue is slow.
iPhone-specific things to test.
- Notification permission is asked once. If refused, send the user to Settings with
Linking.openSettings(). - Push must be tested on a real iPhone; the simulator cannot receive it.
- A normal web link that opens the app (
https://bms.yourdomain.in/alert/123) needs anapple-app-site-associationfile on your backend's domain andassociatedDomainsinapp.json. The custom linkbmsalerts://works without any of that. - Keep
supportsTablet: falseto avoid iPad screenshots at review time.
Guides: Xcode and the simulator, iPhone signing with EAS, Internal distribution.
09Getting it onto phones and into the stores
Start with an APK and TestFlight for yourself and friends. Publish in the stores only if strangers should use it, because both store reviews will ask what the app does with BookMyShow data.
| Way | Who can install | What it takes | Command |
|---|---|---|---|
| APK link (Android) | Anyone with the link, any Android 8 or newer phone | Nothing beyond Expo | eas build --profile preview --platform android |
| Internal distribution (iPhone) | Up to 100 registered iPhones | Apple Developer account, each phone registered | eas device:create, then eas build --profile preview --platform ios |
| TestFlight | Up to 10,000 testers by email or a public link | Apple Developer account, an App Store Connect record, a short review of the first build | eas build --profile production --platform ios then eas submit --platform ios |
| Google Play internal testing | Up to 100 testers by email | Play Console account, an app record, an AAB upload | eas build --profile production --platform android then eas submit --platform android |
| App Store and Play Store | Everyone | Store listings, screenshots, a privacy policy page, data-safety form, review | Promote the tested build in each console |
Uploading with one command. Add a submit.production block to eas.json with ios.appleId, ios.ascAppId and android.serviceAccountKeyPath (a Play Console service-account JSON file). Then eas submit uploads the latest build without opening Xcode or the Play Console. Guide: EAS Submit.
Updates without a new store release. eas update sends JavaScript-only changes (screens, text, fixes) straight to installed apps at next launch. Changes to native pieces (a new package, a new permission) still need a new build. Guide: EAS Update.
What store review will ask for.
- A privacy policy page on your domain saying that BookMyShow login cookies are stored encrypted, used only to read seat availability for the user's own watches, and can be deleted in the app.
- Apple's data questionnaire and Google's data-safety form: email, push token and the BookMyShow session count as account data linked to the user.
- Apple requires "Sign in with Apple" if you offer Google sign-in (guideline 4.8). The email link flow alone avoids that.
- Trademark: do not use BookMyShow's name or logo in the app name or icon. "Seat Alerts" with a plain icon is safe.
Version numbers. Set autoIncrement: true in the production profile so each build gets a new build number, and raise version in app.json for releases users will notice.
10Links and tutorials
Expo's own documentation covers every step above. The rows are in the order a first build meets them.
Two community write-ups worth one read each: Capgo: Expo push notifications on the common delivery failures on real phones, and BrowserStack: Playwright storage state on the backend side of the login the app hands over.