Customer App
Sellino Food — order from nearby restaurants, pay and track the rider
Developed By: BugBuild Labs
Introduction
The Customer App is the ordering side of Sellino Food: find restaurants that can actually deliver to you, build a bag with sizes and add-ons, pay, and watch the kitchen and the rider work.
It is one Flutter codebase for Android and iOS, talking to the same Laravel backend as the web storefront over /api/v10 — so an order placed on the phone and one placed on the site are the same order.
The app follows the backend's mode. In a food edition it boots straight into Food Home; in a mixed install it shows a Food tab beside the shop. You never build a separate app for that.
Requirements
- Flutter SDK (stable channel) with Dart —
flutter doctorshould pass. - Android Studio (Android SDK 21+) and/or Xcode for iOS builds (macOS only).
- A running Sellino Food backend reachable over HTTPS — set up first via Quick Start.
- An approved staff login on that backend (cashier, branch manager or admin).
- For push notifications: a Firebase project — see Common Setup.
Package & Source Code
The download's Main_Files/CustomerAppSourceCode/ folder holds the complete Flutter project (next to WebSourceCode and the other two app folders). Open it in Android Studio or VS Code and run flutter pub get.
lib/core/— environment config, API client, theme, shared widgets and controllers.lib/features/— one folder per screen area:food(home, restaurant page, item sheet, tracking),auth,address,cart,checkout,orders,wallet,rewards,coupons,reviews,chat,notifications,profile,settings.assets/branding/— app icon sources used by the launcher-icon generator.
App Configuration
- Open the
CustomerAppSourceCodefolder in your IDE and runflutter pub get. - Set the API base URL in
lib/core/env/prod_env.dart(release builds) orlib/core/env/dev_env.dart(debug builds) — image URLs, websocket scheme and broadcasting auth URL are derived from it automatically. - Paste the App API key (
kAppApiKey) from Admin → Settings → API Security in the same file. Wrong or missing key → 401 on every screen. - If you use real-time features, set
kReverbAppKeyto the backend's effective Reverb key (Settings → Realtime, elseREVERB_APP_KEYfrom.env). - Configure push notification keys if used — see Common Setup.
// lib/core/env/prod_env.dart → used by RELEASE builds
class ProdEnv implements Env {
static const String kBaseUrl = 'https://yourdomain.com/api/v10'; // your API root
static const String kAppApiKey = 'PASTE-KEY-FROM-ADMIN-PANEL'; // Settings → API Security
static const String kReverbAppKey = 'sellino-app-key'; // backend .env REVERB_APP_KEY
}
// lib/core/env/dev_env.dart → used by DEBUG / PROFILE builds (your local backend)
// imageBaseUrl, reverbScheme and broadcastingAuthUrl are all derived from kBaseUrl.
Tip: always use HTTPS — plain HTTP is blocked by default on Android/iOS. Built-in guard: a release build refuses to start if the key is still empty or the URL still points at a dev host (localhost, .test…), so a forgotten swap fails on your desk, not at the counter.
Never regenerate the App API key after release unless it leaked — installed apps stop working until users update. See Web → Security → App API Key.
App Name & Icon
- Name: change the display name in
android/app/src/main/AndroidManifest.xml(android:label) and iOSInfo.plist(CFBundleDisplayName). - Package / bundle id: set your own unique id (Android
applicationIdinandroid/app/build.gradle; iOS bundle identifier in Xcode) before publishing. - Icon: replace the sources in
assets/branding/and regenerate launcher icons (flutter_launcher_icons).
Build, Signing & Release
Before either platform: confirm the release build points at your production backend — lib/core/env/prod_env.dart. The app's built-in guard throws on first launch if the key is empty or the URL is still a dev host, so a forgotten swap fails on your desk.
Android — release signing
1. Generate a keystore. Run this once and keep the file safe — Google Play ties your app to it forever; lose it and you cannot publish updates under the same listing.
keytool -genkey -v -keystore ~/sellino-food-release.jks \ -keyalg RSA -keysize 2048 -validity 10000 -alias sellino-food # keytool ships with the JDK. On Windows it is: # "%JAVA_HOME%\bin\keytool" -genkey -v -keystore %USERPROFILE%\sellino-food-release.jks ^ # -keyalg RSA -keysize 2048 -validity 10000 -alias sellino-food
It asks for a keystore password, a key password and your name/organisation details. Note both passwords down — the next step needs them.
2. Create android/key.properties (same folder as android/app/), with exactly these four keys:
storePassword=your_keystore_password keyPassword=your_key_password keyAlias=sellino-food storeFile=/Users/you/sellino-food-release.jks # storeFile — absolute path, or a path relative to the android/ folder. # Windows: use forward slashes, e.g. C:/Users/you/sellino-food-release.jks
Never commit key.properties or the .jks file to source control, and never ship them to anyone. They are the identity of your published app.
3. The Gradle wiring is already done. android/app/build.gradle.kts reads that file and applies it — nothing to edit, shown here so you know what happens:
// Loads key.properties if the buyer created it
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
signingConfigs {
create("release") {
if (keystorePropertiesFile.exists()) {
keyAlias = keystoreProperties["keyAlias"] as String?
keyPassword = keystoreProperties["keyPassword"] as String?
storeFile = (keystoreProperties["storeFile"] as String?)?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String?
}
}
}
buildTypes {
release {
// Your keystore when key.properties exists; debug signing otherwise,
// so `flutter run --release` still works before you set one up.
signingConfig = if (keystorePropertiesFile.exists())
signingConfigs.getByName("release")
else
signingConfigs.getByName("debug")
}
}
Play Store builds must not be debug-signed. If key.properties is missing, a release build now stops with a clear error instead of silently signing with the debug key (which Google Play rejects); create the file and build again. Create the file before your first release.
4. Build.
flutter build appbundle --release --obfuscate --split-debug-info=build/symbols # .aab → Google Play flutter build apk --release --obfuscate --split-debug-info=build/symbols # .apk → direct install / other stores # --obfuscate hides Dart symbol names in the binary; keep build/symbols safe — it is what # turns an obfuscated crash log back into readable stack traces (flutter symbolize). # Verify what signed it (should NOT say "Android Debug"): keytool -printcert -jarfile build/app/outputs/bundle/release/app-release.aab
Also set your own applicationId in android/app/build.gradle.kts before publishing — the package ships as com.sellino.customer and every listing needs a unique id you own.
iOS — signing & distribution
iOS builds need macOS with Xcode and a paid Apple Developer Program membership (US$99/year — a third-party cost, not included).
- Bundle identifier. Open
ios/Runner.xcworkspacein Xcode (the workspace, not the project). Select the Runner target → General → set your own reverse-domain Bundle Identifier (e.g.com.yourbrand.pos). It must match the App ID you register with Apple, and should mirror the AndroidapplicationId. - Signing team. Xcode → Settings → Accounts → add your Apple ID. Then on the Runner target → Signing & Capabilities → tick Automatically manage signing and pick your Team. Xcode then creates the development certificate and provisioning profile for you — this is the recommended route.
- Manual signing (only if your organisation requires it): create a Distribution Certificate and an App Store provisioning profile at developer.apple.com → Certificates, Identifiers & Profiles, download both, double-click to install, then untick automatic signing and select the profile.
- Register the App ID at Identifiers using the same bundle id. If you use push, enable the Push Notifications capability here and generate the APNs key — see Common Setup.
- Create the app record in App Store Connect → My Apps → + → pick the bundle id, set name, primary language and SKU.
- Version & build number. Set them in
pubspec.yaml(version: 1.0.0+1— the part after+is the build number). Every upload to App Store Connect needs a build number higher than the last. - Archive and upload.
flutter build ipa --release --obfuscate --split-debug-info=build/symbols # then either open the generated .xcarchive in Xcode → Distribute App, # or upload build/ios/ipa/*.ipa with Transporter (free, Mac App Store)
In Xcode the equivalent is Product → Archive → Distribute App → App Store Connect. - TestFlight → review. The build appears in App Store Connect after processing (a few minutes to an hour). Test it via TestFlight, fill in the store listing, privacy details and screenshots, then submit for review.
Apple's review requires a reachable Privacy Policy URL and an in-app account deletion path. Both ship with the app — the sign-in screen links the policy (edited in Website → Pages) and Settings carries account deletion.
Store accounts (Google Play one-time US$25 / Apple Developer US$99 per year) are third-party services and are not included with this item.
For Developers — API Structure & Data Model
The app is a pure API client: it holds no business logic of its own and writes nothing the web panel cannot read. Everything goes through /api/v10/* on your Laravel backend.
Every request carries these headers
Accept: application/json X-App-Key: <the App API key from Settings → API Security> // rejected with 401 if wrong Authorization: Bearer <jwt> // authenticated calls only X-Branch-Id: <active branch id> // the multi-branch scope Accept-Language: en | bn | ar | he // drives translated responses
X-Branch-Id is the heart of the app. Stock, counters, registers, orders and reports are all scoped to it server-side, so switching branch in the UI changes what every subsequent endpoint returns. It is restored from secure storage before the first request on launch.
Response envelope
Every endpoint answers in the same shape, so the app has one parser and one error path:
{ "status": true, "message": "Request successfully executed", "data": { … } }
{ "status": false, "message": "Human readable error", "data": {} }
// Paginated lists put the rows in data.items alongside:
// current_page · last_page · per_page · total
Endpoint families
Routes live in routes/api/v10/ on the backend, one file per area. The app's back office uses the staff group (routes/api/v10/staff/, ~34 files) — each route behind auth:api, auth.staff and its own hasPermission:* gate:
auth pos cash-register counter orders products inventory warehouses branches categories brands attributes customers suppliers members accounts reports returns replacements coupon campaigns flash-sell users panel-features activity-logs archive
The same hasPermission:* names drive the app's drawer — see Web → Users & Roles. A role missing order_read gets no Orders menu item and a 403 if it calls the endpoint directly.
Core data model
| Entity | Relationship |
|---|---|
| Branch | The top-level scope. Owns warehouses, counters and staff assignments; every stock row, register and order belongs to one. |
| Warehouse → Rack | Stock lives per warehouse; racks locate it inside one. The cart's rack picker reads these. |
| Counter → Cash Register | A counter is the physical till; a register is one shift on it (open → sell → close). One open register per counter, one per user. |
| Product → Variant | Products carry attributes; the sellable unit is the variant (own SKU, barcode, price, stock). |
| Stock → Cost layers | On-hand quantity per variant per warehouse, backed by FIFO cost layers that purchases create and sales consume. |
| Order → Items → Payments | A sale, its lines, and one or more payments (cash / bank / MFS split), tied to the register it was rung on. |
| Journal → Transactions | Every money movement posts a balanced journal of ledger lines against Chart of Accounts heads — sales, expenses, pickups, purchases, returns. |
| User → Role → Permissions | Roles bundle permissions; users may also carry per-user overrides. This is what gates both menu and API. |
Auth & token lifecycle
- Login returns a JWT plus its expiry; the app stores it with
flutter_secure_storage(Keychain on iOS, EncryptedSharedPreferences on Android) — never in plain preferences. - An interceptor refreshes the token through
POST /login/refreshbefore expiry, and retries once on a401; concurrent calls share one refresh instead of racing. - Auth endpoints are rate limited server-side (
throttlemiddleware) against brute force.
Extending it
- Add a backend route under
routes/api/v10/staff/with itshasPermission:*gate, then a matchingservice+controllerin the app's feature folder — the folder layout underlib/features/mirrors these route files closely. lib/core/network/api_endpoints.dartholds every path in one place;dio_client.dartowns headers, refresh and error mapping.- Full endpoint-by-endpoint reference:
docs/api-v10-endpoints.mdin the backend source.
Login & Onboarding
The app opens on a single field — email or phone — and adapts to what the backend has switched on.
- Password or OTP — an existing account asks for its password; a new number gets a one-time code. The code length is read from the server, so 4-digit and 6-digit installs both work with no app change.
- Social login — Google and Facebook appear only when configured in the panel.
- Continue as guest — browse restaurants and build a cart without an account; sign-in is asked for at checkout.
- Language & theme — every language you enabled in the panel, including RTL, plus light and dark mode.
Deliver To & Map Pin
Food is location-first: until the app knows where the customer is, it cannot say which kitchens can reach them.
- Deliver to bar — tap it for a sheet with current location, pick on map and the customer's saved addresses.
- Pick on map — an OpenStreetMap picker; no map API key to buy or configure.
- Saved addresses — each one carries its own pin, so choosing "Home" instantly re-sorts the whole restaurant list.
- No pin yet? The app asks rather than guessing — a wrong guess shows restaurants that cannot deliver.
Food Home & Discovery
The home screen is assembled from the same blocks you arrange in the panel, so what you publish is what the app shows.
- Restaurant cards — logo, name, cuisines, rating, ETA, delivery fee, distance and an Open / Closed pill.
- Cuisine chips and food tags (veg, spicy, halal, bestseller) as one-tap filters.
- Sort by nearest, top rated or fastest; Open now hides closed kitchens.
- Popular dishes — order straight from the rail without opening a restaurant; the tag chips above it (Veg, Spicy…) narrow the rail, and several chips together mean "all of them".
- Deals strip — every active coupon with a tap-to-copy code, Trending this week ranked by real orders, and Order again — a reorder rail from the customer's own history (signed-in customers only).
- Search covers restaurants and dishes together, with a Restaurants group in the results.
Restaurant Page
- Header — banner with a bottom scrim, an identity card with the logo overlapping its edge, name, cuisines and an Open / Closed status pill, then one stat row: rating · ETA · fee and distance · minimum order.
- Popular rail — the restaurant's most-ordered dishes as a swipe rail above the menu; tag chips filter the rail and the menu together.
- Sticky category tabs with an in-menu search box.
- Info — address with a navigate button, phone with a call button, the weekly opening hours sheet and the description.
- Follow the restaurant to find it again from the account tab.
- Sold out dishes stay visible but greyed, so customers see the full menu.
Item Sheet & Add-ons
Tapping a dish opens the sheet where the order is actually assembled.
- Sizes — Regular / Family / Half / Full, each with its own price.
- Add-on groups — required groups must be answered; min/max rules render as radios or capped checkboxes, and the rules are enforced on the server too.
- Note for the kitchen — "no onion, extra spicy" travels to the KOT.
- Quantity, with the button showing the live total as choices change.
- One restaurant per cart — adding from another restaurant asks to replace the cart first.
Cart & Checkout
- Cart — the restaurant at the top, add-ons nested under their dish, quantities editable inline, plus a cutlery toggle and an order note.
- Delivery address — pick a pinned address or drop a new pin; distance and fee update immediately.
- Fee breakdown — base fee, per-km × distance, packaging, free-delivery flag and the total, with warnings for below-minimum, out-of-range and no-pin.
- Rider tip — your preset buttons plus a custom amount.
- Promo code, loyalty points and wallet.
- Schedule for later and the payment methods you enabled — cash on delivery, wallet or an online gateway.
Order Tracking
- Stepper — Placed → Accepted → Preparing → Ready → Picked up → Delivered, updating as the kitchen works.
- Live map — the rider's position while the order is on the road.
- Call and chat — reach the restaurant or the rider without leaving the screen.
- Delivery OTP — the code the customer reads out to the rider.
- Push notifications at every step, deep-linking straight back to this screen.
Account
- Orders — history, invoice, re-order and per-dish reviews.
- Wallet — balance, top-ups, transactions and refunds landing instantly.
- Points & membership — earn on delivered orders, redeem at checkout.
- Coupons — what is available and what has been used.
- Addresses with their map pins, favourites, notifications and chat.
- Settings — language, theme, password, and account deletion where required by the stores.