Mobile app (Flutter)
esimScan has a companion customer mobile app built with Flutter — one codebase for Android and iOS. Buyers browse destinations, get AI plan recommendations, pay through your existing Stripe/PayPal checkout, install the eSIM by QR, and manage their orders, support tickets and account from the phone.
Sold separately
The mobile app is a separate CodeCanyon item (eSimScan App — Flutter eSIM Store App). This guide covers configuring, building and publishing it. The app is a client for the store you already run — the mobile API it talks to ships with esimScan itself, so there is no backend work to do.
What the app does
| Area | Screens |
|---|---|
| First run | Intro carousel, sign in, register, forgot password |
| Browse | Home (popular packages, complete plans), destination search, destination detail with every plan |
| Buy | Opens your existing web checkout in a secure in-app browser |
| My eSIMs | Purchased eSIMs, QR install, manual SM-DP+ / activation code, top-ups, live status |
| AI Advisor | Free-text trip description → plans from your own catalog |
| Support | Ticket list, threaded replies, new ticket |
| Account | Profile, edit details, change password, order history, delete account |
| Settings | Light / dark / system theme, terms & privacy links |
Requirements
- Flutter SDK 3.44+ and Dart 3.12+
- Android: Android Studio + SDK — builds for Android 6.0+ (minSdk 23)
- iOS: macOS with Xcode 15+ — builds for iOS 13.0+
- A running esimScan store on a public HTTPS URL
- Apple Developer and/or Google Play accounts to publish
1. Prepare the backend
The mobile API is part of esimScan and enabled by default under /api/v1. It authenticates with Laravel Sanctum bearer tokens issued to a CustomerAccount (the same storefront buyer account).
Run migrations once so the token table exists:
php artisan migrate
Check it responds:
curl https://yourstore.com/api/v1/health
# {"ok":true,"service":"esimscan-mobile-api","version":"v1"}
If that returns 404, your web server is not passing /api/* through to Laravel — see Troubleshooting.
2. Point the app at your store
The API base URL is a compile-time define, so you never edit code to re-target the app:
flutter pub get
flutter run --dart-define=API_BASE_URL=https://yourstore.com/api/v1
| Target | API_BASE_URL |
|---|---|
| Android emulator → your machine | http://10.0.2.2:8000/api/v1 (with php artisan serve) |
| iOS simulator → your machine | http://127.0.0.1:8000/api/v1 |
| Physical device on the same Wi-Fi | http://<your-LAN-ip>:8000/api/v1 |
| Local HTTPS with a self-signed cert | https://esimscan.test/api/v1 plus --dart-define=ALLOW_BAD_CERTS=true |
| Production | https://yourstore.com/api/v1 |
Never ship ALLOW_BAD_CERTS
It disables TLS certificate validation and exists only so you can test against a local self-signed certificate. Leave it out of every build you distribute.
The app also fetches /api/v1/config on launch for the store name, terms and privacy URLs and feature flags — so those follow your store without a rebuild.
3. Rebrand it
Everything visual lives in two places.
Colours and type — lib/core/theme/app_theme.dart:
static const Color brand = Color(0xFF1A62E8); // primary
static const Color brandDark = Color(0xFF0E3374); // navy
static const Color brandLight = Color(0xFF4487F3);
static const Color accent = Color(0xFFFFC107); // highlight
Logo, icon and splash — replace the files in assets/images/ (icon.png, logo_black.png, logo_white.png), then regenerate:
dart run flutter_launcher_icons
dart run flutter_native_splash:create
Rename the app itself in pubspec.yaml (name, description), android/app/build.gradle.kts (applicationId), and the iOS bundle identifier in Xcode. The default identifier is com.esimscan.app.
4. Build for release
# Android — Play Store
flutter build appbundle --release --dart-define=API_BASE_URL=https://yourstore.com/api/v1
# Android — direct download / sideload
flutter build apk --release --split-per-abi \
--dart-define=API_BASE_URL=https://yourstore.com/api/v1
# iOS — App Store
flutter build ipa --dart-define=API_BASE_URL=https://yourstore.com/api/v1
Add your own signing key
Out of the box android/app/build.gradle.kts signs release builds with Flutter's debug keystore, so the project builds immediately. Google Play rejects debug-signed uploads. Create a keystore and wire up a real signingConfig before publishing.
Payments: no in-app purchase
Tapping Buy calls POST /api/v1/checkout, which returns a web checkout URL. The app opens it in a secure in-app browser — SFSafariViewController on iOS, Chrome Custom Tabs on Android — reusing the storefront's existing Stripe or PayPal flow and the same provisioning pipeline.
eSIM connectivity is a real-world service, which both Apple and Google permit to be sold outside in-app purchase (the model Airalo and Holafly use). So there is no native IAP and no 30% store commission, and no payment credentials are ever handled inside the app.
The completed order links back to the buyer by email, so it appears under My eSIMs and Order history on their next refresh.
Store compliance
Already handled in the app:
- In-app account deletion (Account → Delete account →
DELETE /api/v1/me) — required by both stores - Terms of Service and Privacy Policy links, served from your backend
- Only the
INTERNETpermission — no camera, photos, location or contacts, so no runtime prompts and nothing extra to declare - Secure token storage — iOS Keychain / Android EncryptedSharedPreferences
- No third-party ad or tracking SDKs
Still yours to do before submitting:
- Point
API_BASE_URLat production HTTPS - Configure release signing (Android keystore, iOS provisioning profile)
- Host a public Privacy Policy URL (the app links to
/privacyon your store) - Answer Apple App Privacy / Google Data safety (data collected: name and email, for account and purchases; not used for tracking)
Project layout
Feature-first, layered by responsibility:
lib/
main.dart # bootstrap + ProviderScope
app.dart # MaterialApp.router + theme
core/
config/ # AppConfig (compile-time defines)
theme/ # AppTheme + AppColors (light/dark)
network/ # ApiClient (Dio) + ApiException
storage/ # TokenStorage (Keychain / Keystore)
router/ # go_router + auth-aware redirect
models/ · widgets/ · utils/
features/
onboarding/ auth/ catalog/ checkout/ esims/
advisor/ support/ profile/ settings/ shell/
Every feature has data/ (repository), domain/ (models), application/ (Riverpod providers) and presentation/ (screens). State is Riverpod, navigation is go_router, networking is Dio.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Splash screen never finishes | The app cannot reach API_BASE_URL. Test the URL with curl from the same network; on an Android emulator use 10.0.2.2, not localhost. |
/api/v1/health returns 404 | The web server isn't routing /api/* to Laravel. Confirm the try_files … /index.php?$query_string rule and clear caches with php artisan route:clear. |
| Login fails with a valid password | Migrations not run — the personal_access_tokens table is missing. Run php artisan migrate. |
| Certificate errors on a local domain | Add --dart-define=ALLOW_BAD_CERTS=true for local testing only. |
| AI Advisor says it is unavailable | No AI provider configured, or demo_mode is on. See the AI assistant guide. |
| "My eSIMs" is empty after a purchase | The order is still provisioning, or it was paid with a different email than the account. See fulfillment. |
| Too many requests | The API allows 90 requests/minute per token; the AI Advisor is capped at 15/minute. |