Roadly Mobile is a Flutter monorepo containing separate customer and driver applications backed by shared, tested Roadly packages.
roadly_customer: vehicle rentals and customer ride bookings.roadly_driver: dispatch, live trips, earnings, documents and safety.roadly_catalog: internal visual contract for the Roadly design system.- The existing Angular application remains the Roadly administration portal.
- Flutter 3.47 or newer
- Dart 3.13 or newer
- Android Studio and an Android SDK for Android development
- Xcode on macOS for iOS development
flutter pub get
dart run melos run format
dart run melos run analyze
dart run melos run testRun an application from its directory:
cd apps/roadly_catalog
flutter run -d chromeFor final visual QA, run the component catalog from the workspace root:
flutter run -t apps/roadly_catalog/lib/main.dartIts Visual QA controls switch theme, EN/MS/ES/PT locale, 100%–200% text
scale and phone/tablet constraints without rebuilding. Follow the complete
docs/release-gate.md checklist before producing a
store candidate.
Roadly accepts non-secret build configuration through Dart defines:
| Define | Android emulator value | Purpose |
|---|---|---|
ROADLY_API_URL |
http://10.0.2.2:5000 |
Connects the emulator to the backend running on the host computer |
GOOGLE_WEB_CLIENT_ID |
Web OAuth client ID | Requests a Google ID token intended for the Roadly backend |
Use http://localhost:5000 for an iOS simulator. Use the development
computer's LAN IP, such as http://192.168.1.20:5000, for a physical device.
The computer and physical device must be on the same network, and the firewall
must allow the backend port.
Client IDs are public identifiers and may be supplied through Dart defines. Client secrets, provider signing keys, and backend credentials must never be placed in Flutter source code, mobile configuration, or Git.
Roadly Customer uses the official Flutter google_sign_in package to obtain a
Google ID token. The token is sent to the Roadly backend, where Better Auth
verifies its signature, issuer, audience, and expiry before creating the Roadly
session. The driver app intentionally remains email-only.
All OAuth clients below must belong to the same Google Cloud project.
- Open Google Cloud Console and select or create the Roadly project.
- Open Google Auth Platform > Branding.
- Configure the app name, support email, and developer contact email.
- Open Audience, select External, and add developer Google accounts as test users while the app is in testing mode.
- Open Clients to create the OAuth clients described below.
Create a client with these values:
Application type: Web application
Name: Roadly Web and Backend
Authorized JavaScript origin: http://localhost:4200
Authorized redirect URI: http://localhost:5000/api/v1/auth/session/callback/google
Copy the resulting Web client ID and client secret into the backend .env:
GOOGLE_CLIENT_ID=your-web-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-web-client-secretThe Web client ID is also passed to the Flutter customer app as
GOOGLE_WEB_CLIENT_ID. The client secret remains backend-only.
From PowerShell or the Android Studio terminal:
cd .\apps\roadly_customer\android
.\gradlew.bat signingReportFind the debug variant and copy its SHA1 value. The equivalent direct
keytool command is:
keytool -list -v `
-keystore "$env:USERPROFILE\.android\debug.keystore" `
-alias androiddebugkey `
-storepass android `
-keypass androidRelease and Google Play builds use different signing certificates. Create the corresponding OAuth credential with the release or Play App Signing SHA-1 before distributing the app.
Create another Google Auth Platform client:
Application type: Android
Name: Roadly Customer Android Debug
Package name: com.roadly.roadly_customer
SHA-1 certificate fingerprint: value from signingReport
Copy its client ID to the backend .env:
GOOGLE_ANDROID_CLIENT_ID=your-android-client-id.apps.googleusercontent.comThe Android package name and SHA-1 must match the build being launched. An incorrect package, fingerprint, or Web server client ID can appear as a configuration error or, on some Android versions, as a canceled sign-in.
The iOS client is not required for Android development. When iOS work begins, create a client with:
Application type: iOS
Name: Roadly Customer iOS
Bundle ID: com.roadly.roadlyCustomer
Then add the result to the backend .env:
GOOGLE_IOS_CLIENT_ID=your-ios-client-id.apps.googleusercontent.comThe iOS Runner must also receive the Google iOS client and reversed URL-scheme configuration before Google sign-in can run on iOS. Leave the backend value empty instead of using placeholder text until that client exists.
For current Android development, the relevant backend values are:
BACKEND_URL=http://localhost:5000
FRONTEND_URL=http://localhost:4200
GOOGLE_CLIENT_ID=your-web-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-web-client-secret
GOOGLE_ANDROID_CLIENT_ID=your-android-client-id.apps.googleusercontent.com
GOOGLE_IOS_CLIENT_ID=Restart the Node.js backend after editing .env. Hot reload of the Flutter app
does not reload backend authentication configuration.
-
Open Run > Edit Configurations.
-
Select or create the Roadly Customer Flutter configuration.
-
Set Dart entrypoint to:
<roadly-mobile-root>\apps\roadly_customer\lib\main.dartReplace
<roadly-mobile-root>with the absolute path where this repository is cloned. -
Put the following on one line in Additional run args:
--dart-define=ROADLY_API_URL=http://10.0.2.2:5000 --dart-define=GOOGLE_WEB_CLIENT_ID=your-web-client-id.apps.googleusercontent.com -
Select the Android emulator, click Apply, and then click OK.
-
Stop any currently running app and launch it again. Hot reload does not update compile-time Dart defines.
Do not put these values in the run configuration's Environment variables field. They are Flutter command-line build defines.
From the monorepo root:
flutter run `
-t apps/roadly_customer/lib/main.dart `
--dart-define=ROADLY_API_URL=http://10.0.2.2:5000 `
--dart-define=GOOGLE_WEB_CLIENT_ID=your-web-client-id.apps.googleusercontent.comThe driver app does not need a Google client define:
flutter run `
-t apps/roadly_driver/lib/main.dart `
--dart-define=ROADLY_API_URL=http://10.0.2.2:5000If account selection closes without signing in, confirm all of the following:
- The mobile Web client ID exactly matches backend
GOOGLE_CLIENT_ID. - The Android OAuth client uses
com.roadly.roadly_customer. - The Android OAuth client contains the SHA-1 for the active build variant.
- The Google account is listed under test users while OAuth publishing status is Testing.
- The backend was restarted after
.envchanges. - The Flutter app was fully stopped and relaunched after changing Dart defines.
- The emulator can reach
http://10.0.2.2:5000. - Placeholder strings such as
replace-with-google-ios-client-idare not left in active backend configuration.
Google Cloud changes can take several minutes to propagate.
The applications use feature-first MVVM with repositories and services:
feature/
├── data/
│ ├── models/
│ ├── repositories/
│ └── services/
├── domain/
│ ├── entities/
│ └── use_cases/
└── presentation/
├── screens/
├── view_models/
└── widgets/
Shared package responsibilities:
| Package | Responsibility |
|---|---|
roadly_api |
HTTP contracts, serialization and structured API failures |
roadly_auth |
Better Auth mobile session and role contracts |
roadly_core |
Environment, result and cross-cutting primitives |
roadly_design_system |
Themes, tokens and approved UI components |
roadly_localization |
Typed EN, MS, ES and PT translations |
roadly_maps |
GPS permission, native geocoding and private coordinate-picker contracts |
roadly_notifications |
Customer and driver mobile push contracts |
roadly_realtime |
Authenticated realtime connection contracts |
roadly_support |
Shared customer and driver human-support workflow |
roadly_testing |
Shared fixtures and test helpers |
Feature screens must use components exported by roadly_design_system.
Do not independently restyle Material buttons, fields, selects, cards, dialogs
or status treatments inside a feature. If a component is missing, add and
approve it in roadly_catalog first.
All application forms use reactive_forms through Roadly form components.
Validation appears below its field, invalid submissions are disabled, and an
in-flight action cannot be submitted twice.
Every shared component must cover light and dark themes, loading, disabled, focus, validation, translated text and accessible text scaling where relevant.
Completed foundation:
- Customer, driver and component-catalog applications
- Dart workspace and Melos configuration
- Riverpod and GoRouter application shells
- Shared package boundaries
- Roadly light and dark themes
- Roadly spacing and colour tokens
- Initial button, icon button, badge, card, reactive input, select and state components
- Typed English, Malay, Spanish and Portuguese catalogs
- Initial package and widget tests
- Better Auth signed Bearer sessions alongside the existing Angular cookies
- Secure native session storage using the platform keychain or keystore
- Shared HTTP transport with structured API failures and authorization headers
- Role-aware customer and driver routing with session restoration and sign-out
- Shared typed-reactive sign-in UI with validation and password visibility
- Customer-only native Google ID-token sign-in through Better Auth, including secure token persistence and phone recovery when Google has no phone number
- Authenticated Socket.IO transport for customer and driver apps using the stored Better Auth Bearer session, WebSocket-only native transport, reconnect lifecycle states, session-expiry sign-out, and a resync signal for recovering domain state missed while offline
- Live customer vehicle discovery backed by Roadly's server-paginated catalog, with pull-to-refresh, incremental loading, vehicle imagery, availability, pricing, ratings, and localized empty/error/detail states
- Live driver operations backed by the existing profile, availability, assignment, accept, and reject endpoints, including required structured rejection reasons, duplicate-action protection, and realtime refetches
- Customer rental checkout with server availability, pricing, booking, cancellation, Stripe checkout, and combined Activity history
- Customer Kuala Lumpur rides with typed fare quotes, cash/card request flow, card authorization, active-ride recovery, cancellation, realtime refresh, and ride history
- Customer ride pickup and drop-off selection using landmark/street search, current device location, a private coordinate picker, localized permission recovery, and KL offline fallbacks. Selected coordinates pass through the existing signed server quote contract without client-side fare calculation.
- Rental and ride Stripe checkout now persist a non-secret pending resource,
return through the registered
roadly://payment/...Android/iOS scheme, and refresh authenticated server state on return, resume, or app restart. Redirect parameters never mark a payment successful; Stripe webhooks and the refreshed booking/ride records remain authoritative. - Driver trip lifecycle controls backed by the server state machine, including restart-safe offer acceptance and required cash-receipt confirmation
- Driver Activity, earnings and payout views using server pagination and server-calculated settlement values
- Driver performance, compliance-document submission and safety incident reporting through typed APIs and reactive Roadly forms
- Persisted EN/MS/ES/PT language, MYR/USD/EUR/GBP/SGD/AUD display-currency,
and system/light/dark appearance preferences shared by customer and driver
apps through
roadly_coreandroadly_design_system. - Customer catalog, rental availability, rental booking, and ride quotes send the selected display currency to the backend. Conversion uses Roadly's live-rate provider with configured fallback rates; settlement and historical transaction currencies remain unchanged.
- Customer and driver apps share one account-secured Support Centre with typed reactive request/reply forms, related rental and ride context, resolved-chat protection, reopening, pull-to-refresh and localized inbox states.
- Customer Home now surfaces live fleet, rental and active-ride information, quick booking actions and human support. Driver Drive provides immediate Activity, Earnings and Support shortcuts without changing its shift or trip state machine.
Remaining implementation phases are saved in
docs/implementation-checkpoint.md:
- Complete the final visual, accessibility, regression and store-release gate.
The customer and driver apps share the Roadly launcher identity: Roadly Night,
Lime and Teal with the fleet mark kept inside Android's adaptive-icon safe
zone. The same mark appears on a Roadly Night native splash while Flutter loads,
including Android 12+ system splash support. Source artwork is stored in
assets/branding/; generated Android and iOS assets are committed so a normal
app build does not require an icon generator.
After changing the SVG source, regenerate every platform size from the repository root:
.\tool\generate_launcher_icons.ps1The script uses installed Chrome or Edge to rasterize the SVG master and does not alter the internal catalog app.
Customer and driver sign-in screens share the recovery forms from packages/roadly_auth. Requests always show the same check-your-email state, new passwords must meet Roadly's strength rules, and a successful reset invalidates existing sessions. The customer app registers roadly://auth/reset-password; the driver app registers roadly-driver://auth/reset-password on Android and iOS.
For local testing without Resend, submit the mobile forgot-password form and copy the callback URL printed by the backend terminal. Test Android links directly with:
adb shell am start -a android.intent.action.VIEW -d "roadly://auth/reset-password?token=TOKEN"
adb shell am start -a android.intent.action.VIEW -d "roadly-driver://auth/reset-password?token=TOKEN"Production email delivery requires backend RESEND_API_KEY and EMAIL_FROM values. Store releases should replace custom schemes with verified Android App Links and iOS Universal Links when the production Roadly domain is available.
The backend .env controls the HTTPS/HTTP bridge used by Stripe:
# Android emulator
MOBILE_PAYMENT_RETURN_URL=http://10.0.2.2:5000/api/v1/payments/mobile-returnUse the deployed HTTPS backend URL in production. For a physical device during
local development, use a reachable LAN IP or HTTPS development tunnel. Android
and iOS register roadly://payment; no Stripe secret or session identifier is
stored in the URL or trusted by the app.
You can exercise the Android deep link without Stripe:
adb shell am start -a android.intent.action.VIEW -d "roadly://payment/booking/success?id=1"The signed-in customer must own the referenced booking. Roadly navigates to
Activity and reloads the authenticated booking API. Use
roadly://payment/ride/success?id=1 for a ride; cancellation uses
cancelled instead of success.
The customer app asks for location only after the user selects Use my current
location. Android declares coarse and fine foreground location; iOS declares
NSLocationWhenInUseUsageDescription. Background location is not requested.
Address search and reverse geocoding use the operating system's native
geocoder. The Roadly coordinate preview does not load third-party map tiles or
send a precise coordinate to a tile host. A production road-map provider can
later implement the roadly_maps gateway after its privacy, billing and key
restrictions are approved; the ride API and signed fare contract will not need
to change.
See docs/mobile-api-readiness.md for the
backend audit and required changes before authentication implementation.