ParkEZ is a complete smart parking management ecosystem that connects drivers searching for parking, owners listing their parking spaces, watchmen managing on-ground operations, and administrators overseeing the entire platform. Built with Flutter and Firebase, it ships as four tightly integrated apps that share a single backend.
Customers can discover nearby parking spaces on the map, view photos, check availability, and book slots in real time.
Phone OTP, Google Sign-In, and Apple Sign-In through Firebase Authentication.
Built-in wallet for owners, customer payments, refunds on cancellation, and a complete transaction ledger.
Booking, check-in, check-out, and admin announcements pushed via Firebase Cloud Messaging (FCM).
Manage owners, watchmen, customers, parking spaces, bookings, reviews, and platform-wide settings.
Firestore, Firebase Storage, and FCM — a fully managed backend with no separate server to maintain.
Before starting the setup, make sure you have the following tools and accounts ready.
ParkEZ relies on Firebase (Auth, Firestore, Storage, Cloud Messaging, Hosting) and Google Maps Platform. Both are third-party services with their own pricing: Firebase's free Spark plan and Google Maps' monthly free credit cover development and small deployments, but production usage beyond the free tiers is billed by Google to your own account. Payment gateways (Stripe, Razorpay, PayPal) also charge per-transaction fees. These costs are not included in the item price — review Firebase pricing and Google Maps pricing before going live.
| Software | Version | Purpose |
|---|---|---|
| Flutter SDK | 3.22 or later (stable) | Framework for building all four apps |
| Dart SDK | Bundled with Flutter | Programming language for Flutter |
| Android Studio | Latest | IDE & Android SDK / Emulator |
| Xcode (macOS only) | Latest | iOS development & simulator |
| Node.js & npm | LTS version | Firebase CLI & Firestore database import |
| Firebase CLI | Latest | Deploy and manage Firebase services |
| Git | Latest | Version control |
| VS Code / Android Studio | Latest | Code editor with Flutter / Dart plugins |
The ParkEZ package ships as a single repository containing four Flutter projects and a shared documentation folder.
End-user app for drivers to discover, book, and pay for parking spaces, manage their wallet, view booking history, and rate parkings.
For parking-space owners to onboard their lots, set pricing, manage availability, view earnings, and withdraw to their bank.
For on-ground staff assigned by owners to verify vehicles, scan booking QR codes, perform check-in / check-out, and handle disputes.
Web dashboard for administrators to manage owners, watchmen, customers, parkings, bookings, payments, reviews, notifications, and settings.
ParkEZ/
+-- admin_panel/ # Flutter Web - Admin Dashboard
+-- customer/ # Flutter Mobile - Customer App
+-- owner/ # Flutter Mobile - Owner App
+-- watchman/ # Flutter Mobile - Watchman App
+-- android/ # Shared Android scripts (if any)
+-- cloud_functions/ # Firebase Cloud Functions - server-side payment operations
+-- documentation/ # This documentation
+-- database/ # Firestore collection import / export scripts
+-- firestore_index/ # firestore.indexes.json + rules
+-- README.md
All four apps connect to the same Firebase project. Each app is registered as a separate platform within that project (Android / iOS / Web) so they share Firestore, Authentication, Storage, and Cloud Messaging.
New to Flutter or Firebase? Don't worry. This page explains the whole setup in plain words. Follow the steps from top to bottom — each one links to a detailed page with exact commands, screenshots, and copy buttons on every command.
Prefer to watch instead of read? This short walkthrough shows the complete setup from start to finish — downloading the package, creating the Firebase project, importing the database, connecting the Admin Panel, and running the mobile apps.
Following this guide carefully, the complete end-to-end setup for all four apps typically takes 2–3 hours depending on your familiarity with Flutter and Firebase.
This is the complete, ordered installation walkthrough. Each step links to the detailed section covering it — do them in this order; later steps depend on earlier ones.
Every common failure we know of is listed in Troubleshooting with its fix.
ParkEZ targets Flutter stable. Follow the steps below for your operating system.
flutter doctor
flutter doctor
export PATH="$PATH:$HOME/development/flutter/bin"
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch
sudo gem install cocoapods
flutter doctor
ParkEZ supports three authentication methods used across the Customer, Owner, and Watchman apps. Enable each one in the Firebase Console.
Apple Sign-In requires an active Apple Developer account ($99/year). It is mandatory for iOS apps that offer third-party login options.
Import the pre-built ParkEZ Firestore collections (users, owners, watchmen, parkings, bookings, transactions, settings, etc.) to populate your database with the required structure.
ParkEZ ships with production-grade security rules (cloud_functions/firestore.rules, also included in firestore_index.zip). They scope every collection: customers only manage their own profile, wallet history, and bookings; owners only their own parkings and staff; watchmen only their assigned parking; admins manage everything. Wallet balances and payment secrets are locked so they can only be touched by the Cloud Functions (Admin SDK) and the Admin Panel — never directly by an app user.
Option A (recommended): the rules deploy automatically together with the Cloud Functions in the Cloud Functions step (firebase deploy --only functions,firestore) — nothing to do here.
Option B (manual): in Firestore Database → Rules, delete everything in the editor, paste the following, and click Publish:
rules_version = '2';
// ParkEZ production security rules.
//
// Highlights:
// - Nothing is writable without authentication; most reads require sign-in.
// - Wallet balances (walletAmount) can NEVER be changed from a client app:
// all balance mutations go through the Cloud Functions (Admin SDK).
// - wallet_transaction documents are read-only for their owner and can only
// be created by the Cloud Functions / admin panel.
// - Payment gateway secret keys live in settings/payment_secrets, which only
// admins can read; the apps read only the sanitized settings/payment doc.
// - Users/owners/watchmen only see and manage what belongs to them; the
// admin panel (users present in the `admin` collection) manages everything.
service cloud.firestore {
match /databases/{database}/documents {
function isAuthed() {
return request.auth != null;
}
function isSelf(id) {
return isAuthed() && request.auth.uid == id;
}
function isAdmin() {
return isAuthed() && exists(/databases/$(database)/documents/admin/$(request.auth.uid));
}
// The client may not change the wallet balance of an existing document.
function walletUntouched() {
return !request.resource.data.diff(resource.data).affectedKeys().hasAny(['walletAmount']);
}
// New profile documents must start with an empty wallet.
function walletStartsEmpty() {
return !('walletAmount' in request.resource.data)
|| request.resource.data.walletAmount in ['0', '0.0', 0, 0.0];
}
function isWatchmanFor(parkingId) {
return isAuthed()
&& exists(/databases/$(database)/documents/watchman/$(request.auth.uid))
&& get(/databases/$(database)/documents/watchman/$(request.auth.uid)).data.assignedParkingId == parkingId;
}
// ── Admin panel users ────────────────────────────────────────────────
match /admin/{adminId} {
allow read, write: if isAdmin();
}
// ── Profiles (wallet balance locked for clients) ─────────────────────
match /customers/{userId} {
allow read: if isAuthed();
allow create: if isAdmin() || (isSelf(userId) && walletStartsEmpty());
allow update: if isAdmin() || (isSelf(userId) && walletUntouched());
allow delete: if isAdmin() || isSelf(userId);
match /{subCollection}/{docId} {
allow read: if isAuthed();
allow write: if isAdmin() || isSelf(userId);
}
}
match /owners/{ownerId} {
allow read: if isAuthed();
allow create: if isAdmin() || (isSelf(ownerId) && walletStartsEmpty());
allow update: if isAdmin() || (isSelf(ownerId) && walletUntouched());
allow delete: if isAdmin() || isSelf(ownerId);
match /{subCollection}/{docId} {
allow read: if isAuthed();
allow write: if isAdmin() || isSelf(ownerId);
}
}
match /watchman/{watchmanId} {
allow read: if isAuthed();
// A watchman edits their own profile; the owner who employs them (and
// admins) manage the account.
allow create: if isAdmin() || (isAuthed() && request.resource.data.ownerId == request.auth.uid);
allow update: if isAdmin() || isSelf(watchmanId)
|| (isAuthed() && resource.data.ownerId == request.auth.uid);
allow delete: if isAdmin() || (isAuthed() && resource.data.ownerId == request.auth.uid);
}
// ── Parking & slots ──────────────────────────────────────────────────
match /parking/{parkingId} {
allow read: if true; // public listings (browse before login)
allow create: if isAdmin() || (isAuthed() && request.resource.data.ownerId == request.auth.uid);
allow update: if isAdmin()
|| (isAuthed() && resource.data.ownerId == request.auth.uid)
|| isWatchmanFor(parkingId)
// customers may only bump the review counters after rating
|| (isAuthed() && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['reviewSum', 'reviewCount']));
allow delete: if isAdmin() || (isAuthed() && resource.data.ownerId == request.auth.uid);
match /{subCollection}/{docId} {
allow read: if true;
// slot status changes during booking / check-in / check-out
allow write: if isAuthed();
}
}
// ── Bookings (participants only) ─────────────────────────────────────
match /bookings/{bookingId} {
allow read: if isAdmin()
|| (isAuthed() && (resource.data.userId == request.auth.uid || resource.data.ownerId == request.auth.uid))
|| isWatchmanFor(resource.data.parkingId);
allow create: if isAdmin() || (isAuthed() && request.resource.data.userId == request.auth.uid);
allow update: if isAdmin()
|| (isAuthed() && (resource.data.userId == request.auth.uid || resource.data.ownerId == request.auth.uid))
|| isWatchmanFor(resource.data.parkingId);
allow delete: if isAdmin();
}
// ── Money (Cloud Functions / Admin SDK only) ─────────────────────────
match /wallet_transaction/{txnId} {
allow read: if isAdmin() || (isAuthed() && resource.data.userId == request.auth.uid);
allow write: if isAdmin(); // apps never write these; Cloud Functions bypass rules
}
match /transaction_log/{logId} {
allow read: if isAdmin() || (isAuthed() && resource.data.userId == request.auth.uid);
allow create: if isAdmin() || (isAuthed() && request.resource.data.userId == request.auth.uid);
allow update, delete: if isAdmin();
}
// ── Settings ─────────────────────────────────────────────────────────
// Gateway secret keys: admins only. The apps and Cloud Functions never
// need to read this document from the client side.
match /settings/payment_secrets {
allow read, write: if isAdmin();
}
match /settings/{settingId} {
// Everything except the secrets document is public app config
// (branding, landing page content, sanitized payment config, …).
allow read: if settingId != 'payment_secrets';
allow write: if isAdmin();
}
// ── Admin-managed content (public read) ──────────────────────────────
match /currencies/{docId} { allow read: if true; allow write: if isAdmin(); }
match /languages/{docId} { allow read: if true; allow write: if isAdmin(); }
match /country_tax/{docId} { allow read: if true; allow write: if isAdmin(); }
match /onboarding_screen/{docId} { allow read: if true; allow write: if isAdmin(); }
match /parking_facilities/{docId} { allow read: if true; allow write: if isAdmin(); }
match /coupons/{docId} { allow read: if isAuthed(); allow write: if isAdmin(); }
match /email_template/{docId} { allow read: if isAuthed(); allow write: if isAdmin(); }
match /documents/{docId} { allow read: if isAuthed(); allow write: if isAdmin(); }
match /role_permissions/{docId} { allow read: if isAdmin(); allow write: if isAdmin(); }
match /broadcast_notifications/{docId} { allow read: if isAuthed(); allow write: if isAdmin(); }
// ── Reviews ──────────────────────────────────────────────────────────
match /review_customer/{reviewId} {
allow read: if true;
allow create: if isAuthed();
allow update, delete: if isAdmin() || (isAuthed() && resource.data.customerId == request.auth.uid);
}
// ── Notifications & chat ─────────────────────────────────────────────
// A notification's recipient is identified by the role-specific id field
// (customerId / ownerId / watchmanId). Only the recipient — and admins —
// can read, update (e.g. mark as read), or delete it. Any signed-in user
// may create one (apps notify counterparties about booking events).
match /notification/{notificationId} {
function isRecipient() {
return isAuthed()
&& (resource.data.get('customerId', '') == request.auth.uid
|| resource.data.get('ownerId', '') == request.auth.uid
|| resource.data.get('watchmanId', '') == request.auth.uid);
}
allow read: if isAdmin() || isRecipient();
allow create: if isAuthed();
allow update, delete: if isAdmin() || isRecipient();
}
// Chat rooms are private to their participants: every room document must
// store a `participants` array of user ids. Only those users — and
// admins — can read or write the room and its messages, and a message
// can only be created or edited by its own author (senderId).
match /chat_rooms/{roomId} {
function isRoomParticipant() {
return isAuthed() && request.auth.uid in resource.data.get('participants', []);
}
allow read: if isAdmin() || isRoomParticipant();
allow create: if isAdmin()
|| (isAuthed() && request.auth.uid in request.resource.data.get('participants', []));
allow update: if isAdmin()
|| (isRoomParticipant()
&& request.resource.data.get('participants', []) == resource.data.get('participants', []));
allow delete: if isAdmin();
match /messages/{messageId} {
function isParticipant() {
return isAuthed()
&& request.auth.uid in get(/databases/$(database)/documents/chat_rooms/$(roomId)).data.get('participants', []);
}
allow read: if isAdmin() || isParticipant();
allow create: if isAdmin()
|| (isParticipant() && request.resource.data.get('senderId', '') == request.auth.uid);
allow update, delete: if isAdmin()
|| (isParticipant() && resource.data.get('senderId', '') == request.auth.uid);
}
}
// ── Everything else: admins only ─────────────────────────────────────
match /{document=**} {
allow read, write: if isAdmin();
}
}
}
Do not publish allow read, write: if true — not even “temporarily for testing”. Open rules let anyone read or modify your entire database, including wallets and payment configuration. The database import in the next steps does not need open rules: it uses a service account (Admin SDK), which bypasses security rules.
If any app shows empty screens and the console logs permission-denied, the rules were not published (or an old version is live) — re-check this step, or run firebase deploy --only firestore from the cloud_functions folder. Also make sure you are signed in inside the app: almost all data requires an authenticated user.
node -v
npm -v
cd path/to/extracted/folder
npm install firestore-export-import
node import
Once the import completes, your Firestore database will have all required ParkEZ collections including customers, owners, watchmen, parkings, bookings, transactions, reviews, notifications, and settings.
cd path/to/firestore_index
firebase init
You're about to initialize a Firebase project in this directory:
C:\path\to\your\firestore_index
? Are you ready to proceed? (Y/n)
(*) Firestore: Configure security rules and indexes files for Firestore
Use arrow keys to move, Space to select, Enter to confirm.
firebase deploy --only firestore:indexes
=== Deploying to 'parkez-xxxxx'...
i firestore: reading indexes from firestore.indexes.json...
i cloud.firestore: checking firestore.rules for compilation errors...
+ cloud.firestore: rules file firestore.rules compiled successfully
i firestore: deploying indexes...
+ firestore: deployed indexes in firestore.indexes.json successfully
+ Deploy complete!
Project Console: https://console.firebase.google.com/project/parkez-xxxxx/overview
ParkEZ uses Firebase Storage to host parking photos, profile pictures, owner KYC documents, and watchman ID proofs.
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /parkings/{parkingId}/{allFiles=**} {
allow read: if true;
allow write: if request.auth != null;
}
match /users/{userId}/{allFiles=**} {
allow read: if request.auth != null;
allow write: if request.auth != null && request.auth.uid == userId;
}
}
}
For production, tighten read access on user-uploaded KYC documents to admins only. See the optional rules block in storage.rules shipped with the project.
Browsers block the Admin Panel and Landing Page from loading Storage images unless the bucket sends CORS headers — without this step, images upload fine from mobile but appear broken/blank on the web.
echo '[{ "origin": ["*"], "method": ["GET"], "maxAgeSeconds": 3600, "responseHeader": ["Content-Type"] }]' > cors-config.json
gsutil cors set cors-config.json gs://YOUR_BUCKET_NAME
Full walkthrough with screenshots of every Cloud Shell step: CORS Configuration. If Admin Panel images ever show broken after deploy, this step is the first thing to re-check (see Troubleshooting).
CORS (Cross-Origin Resource Sharing) must be configured on your Firebase Storage bucket so the Admin Panel can load parking images and uploaded documents.
Welcome to Cloud Shell! Type "help" to get started.
Your Cloud Platform project in this session is set to parkez-xxxxx.
echo '[{ "origin": ["*"], "method": ["GET"], "maxAgeSeconds": 3600, "responseHeader": ["Content-Type"] }]' > cors-config.json
gsutil cors set cors-config.json gs://YOUR_BUCKET_NAME
All secret-key payment operations — Stripe, PayPal, Flutterwave, Paystack, Xendit, Midtrans, Mercado Pago, and PayFast — are processed server-side by Firebase Cloud Functions included in the cloud_functions/ folder of the package. The mobile apps call these functions instead of contacting the gateways directly, so the gateway secret keys never leave your server and are never shipped inside the APK / IPA. Deploying them is a one-time step — these gateways will not work until you do. (Razorpay needs no function: its checkout uses only the public key id.)
The functions deployed are: stripeCreatePaymentIntent, paypalCreateOrder, paypalAuthorizeOrder, flutterwaveInitiatePayment, flutterwaveVerifyTransaction, paystackInitializeTransaction, paystackVerifyTransaction, xenditCreateInvoice, xenditCheckInvoice, midtransCreatePaymentLink, mercadopagoCreatePreference, and payfastGetPaymentHtml. They read the gateway keys from the settings/payment Firestore document — the same one managed from Admin Panel → Settings → Payment Settings — so after deployment there is no extra configuration: whatever keys you save in the Admin Panel are used automatically.
npm install -g firebase-tools
firebase login
cd cloud_functions
firebase use --add
cd functions
npm install
cd ..
firebase deploy --only functions,firestore
When the command finishes with Deploy complete!, all functions are live. You can verify them under Firebase Console → Build → Functions. The mobile apps discover them automatically through their Firebase configuration (google-services.json / GoogleService-Info.plist) — nothing to change in the app code.
The ParkEZ Admin Panel is a Flutter Web application located in /admin_panel. The sections below walk through configuring and deploying it.
The Admin Panel runs entirely on Flutter Web and talks directly to Firestore, Firebase Auth, Storage, and FCM. No backend server is required.
<head>
...
<title>ParkEZ Admin</title>
...
</head>
{
"name": "ParkEZ Admin",
"short_name": "ParkEZ Admin",
...
}
npm install -g firebase-tools
firebase login
firebase projects:create
$ dart pub global activate flutterfire_cli
$ flutterfire configure --project=YOUR_PROJECT_ID
This registers per-platform apps with Firebase and adds lib/firebase_options.dart to your project.
dart pub global activate flutterfire_cli
flutterfire configure --project=YOUR_PROJECT_ID
? Which platforms should your configuration support?
( ) android
( ) ios
( ) macos
(*) web <-- Select ONLY this
( ) windows
Use arrow keys and Space to select web only. Press Enter to confirm.
flutterfire configure only generates lib/firebase_options.dart. Two web files also contain a firebaseConfig object that must point to your Firebase project (used for web push notifications):
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places"></script>
The Admin Panel uses Google Maps for the Parkings module (showing all parking locations on a single map) and for the Owners "live view" screen.
The favicon represents the Admin Panel in the browser tab.
Use online tools like favicon.io or realfavicongenerator.net to generate the full favicon set from a single ParkEZ logo image.
ParkEZ is designed so you can rebrand the entire platform without touching any code. Log in to the Admin Panel and open Settings → App Settings — the name, colors, and every logo/icon for all four apps are managed from this one screen. Click Save at the bottom to apply. (See the annotated App Settings screenshot.)
App Settings has a dedicated primary color field for each app — enter a HEX code (e.g. #5D54F2) or use the color swatch, then press Change:
The new color is stored in Firebase and each app applies it on next launch — no rebuild required. Light and dark mode are handled automatically from your chosen color.
Every image is uploaded from the same screen using its Change / Upload button (PNG or JPG, square recommended):
| Field | Where it appears |
|---|---|
| Favicon | Browser tab icon |
| App Icon (Light) / App Icon (Dark) | Mobile app logo for light & dark themes |
| Admin Panel Logo | Logo in the admin sidebar/header |
| Landing Page Logo | Logo shown on the marketing landing page |
The values above override everything at runtime, so most buyers never need code changes. If you also want to change the fallback colors compiled into the apps (used before the panel value loads), edit primary4 in each app's lib/app/utils/app_colors.dart and rebuild.
Run all commands from inside the admin_panel/ folder.
flutter clean
flutter pub get
flutter build web --release
Compiling lib/main.dart for the Web...
✓ Built build\web 24.7s
admin_panel/build/web/
Run all commands from inside the admin_panel/ folder.
npm install -g firebase-tools
firebase login
firebase init
? Are you ready to proceed? (Y/n) y
? Which Firebase features do you want to set up?
(*) Hosting: Configure files for Firebase Hosting
=== Project Setup
? Please select an option: Use an existing project
i Using project parkez-xxxxx
=== Hosting Setup
? What do you want to use as your public directory? build/web
? Configure as a single-page app (rewrite all urls to /index.html)? Yes
? Set up automatic builds and deploys with GitHub? No
flutter build web --release
firebase deploy --only hosting
=== Deploying to 'parkez-xxxxx'...
i deploying hosting
i hosting[parkez-xxxxx]: beginning deploy...
i hosting[parkez-xxxxx]: found 489 files in build/web
+ hosting[parkez-xxxxx]: file upload complete
+ hosting[parkez-xxxxx]: version finalized
+ hosting[parkez-xxxxx]: release complete
+ Deploy complete!
Project Console: https://console.firebase.google.com/project/parkez-xxxxx/overview
Hosting URL: https://parkez-xxxxx.web.app
Your ParkEZ Admin Panel is live! Copy the Hosting URL (e.g., https://parkez-xxxxx.web.app) to access it.
After the Admin Panel build is uploaded (see Deploy on Server or Deploy on Firebase), map your domain to it and secure it with HTTPS. Follow the path that matches your hosting.
Firebase Hosting includes free, auto-renewing SSL. Once the domain shows Connected, HTTPS works with no extra steps.
Type: A Host: admin (or @) Value: 203.0.113.10 TTL: 3600
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d admin.yourdomain.com
server {
listen 80;
server_name admin.yourdomain.com;
return 301 https://$host$request_uri; # force HTTPS
}
server {
listen 443 ssl;
server_name admin.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/admin.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/admin.yourdomain.com/privkey.pem;
root /var/www/parkez-admin; # your build/web folder
index index.html;
location / {
try_files $uri $uri/ /index.html; # SPA fallback
}
}
sudo apt install certbot python3-certbot-apache
sudo certbot --apache -d admin.yourdomain.com
RewriteEngine On
# Force HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# SPA fallback (all routes -> index.html)
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.html [L]
Certbot installs a renewal timer automatically. Verify it with sudo certbot renew --dry-run.
The landing page is a lightweight static site (plain HTML/JS in landing_page/) — it can be hosted anywhere. Typical setup with the Admin Panel on a subdomain:
| Site | Suggested domain | What to upload |
|---|---|---|
| Landing Page | yourdomain.com (root) | Contents of landing_page/ |
| Admin Panel | admin.yourdomain.com | Contents of admin_panel/build/web/ |
A blank page after deploy is almost always a missing SPA rewrite (all routes → index.html) or a stale browser cache. Apply the rewrite rules above and hard-refresh.
The steps below apply to all three mobile apps shipped with ParkEZ: customer/, owner/, and watchman/. Repeat each step for each app folder, using a different package name / bundle ID per app.
Run these steps separately for each mobile app folder (customer/, owner/, watchman/).
defaultConfig {
applicationId "com.parkez.customer"
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.parkez.customer">
</manifest>
<application
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:label="ParkEZ"
...>
</application>
<string name="app_name">ParkEZ</string>
<key>CFBundleDisplayName</key>
<string>ParkEZ</string>
<application
android:icon="@mipmap/ic_launcher"
...>
</application>
Use appicon.co or the flutter_launcher_icons package to automatically generate all required icon sizes from a single ParkEZ logo image.
Repeat these steps once per app folder (customer/, owner/, watchman/) so each gets its own lib/firebase_options.dart while pointing to the same Firebase project.
npm install -g firebase-tools
firebase login
$ dart pub global activate flutterfire_cli
$ flutterfire configure --project=YOUR_PROJECT_ID
Run both inside the app folder. Then click Next → Continue to console.
cd customer
dart pub global activate flutterfire_cli
flutterfire configure --project=YOUR_PROJECT_ID
? Which platforms should your configuration support?
(*) android <-- Select this
(*) ios <-- Select this
( ) macos
( ) web
( ) windows
Use arrow keys and Space to select android and ios. Press Enter.
flutter pub get
flutterfire configure
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(MyApp());
}
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="YOUR_API_KEY"/>
import GoogleMaps
GMSServices.provideAPIKey("YOUR_API_KEY")
SHA keys are required for Firebase services like Google Sign-In and Phone Authentication on Android. Generate and register keys for each of the three mobile apps (customer, owner, watchman).
keytool -list -v -keystore "%USERPROFILE%\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
A JKS (Java KeyStore) holds the private key that signs Android release builds. Google Play requires every APK/AAB to be signed with the same key for every update — create it once for each ParkEZ app and keep it safe.
Uses keytool from the JDK (bundled with Android Studio / Flutter). Run from your terminal.
Repeat these steps for every Android app you ship: customer/, owner/, watchman/. The Admin Panel is web-only and doesn't need a JKS.
Run this in the terminal to create the JKS for Android signing only. Replace (Project_name) with the app name (e.g. parkez-customer) — the file lands in the current folder.
keytool -genkey -v -keystore (Project_name).jks -keyalg RSA -keysize 2048 -validity 10000 -alias (Project_name)
Enter keystore password: ********
Re-enter new password: ********
What is your first and last name?
[Unknown]: Your Name
What is the name of your organizational unit?
[Unknown]: ParkEZ
...
Is CN=Your Name, OU=ParkEZ, ... correct?
[no]: yes
Enter key password for <(Project_name)>
(RETURN if same as keystore password):
# android/key.properties
storePassword=YOUR_KEYSTORE_PASSWORD
keyPassword=YOUR_KEY_PASSWORD
keyAlias=(Project_name)
storeFile=(Project_name).jks
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
flutter build apk --release
# or
flutter build appbundle --release
Run all commands from inside the specific app folder (customer/, owner/, or watchman/).
flutter clean
flutter pub get
flutter build apk --release
build/app/outputs/flutter-apk/app-release.apk
flutter clean
flutter pub get
flutter build appbundle --release
build/app/outputs/bundle/release/app-release.aab
cd path/to/ParkEZ/customer # or owner, watchman, admin_panel
flutter run
Everything needed to run and publish the Customer, Owner, and Watchman apps on iOS. Repeat the steps for each app folder. A Mac with Xcode 15+ and an Apple Developer account ($99/year) are required.
Each app ships with ios/Runner/Runner.entitlements pre-configured for Push Notifications (aps-environment) and Sign in with Apple. You only need to enable both capabilities on each App ID in the Apple Developer portal, then refresh provisioning profiles in Xcode.
Usage descriptions are already included per app — camera & photo library (all apps), location (Customer, Owner, Watchman), plus background remote-notification mode. Edit the strings in Info.plist if you want your own wording; App Review reads them.
flutter build ipa --release
Or archive from Xcode: Product → Archive. When distributing, you may see “Upload Symbols Failed — dSYM for RazorpayStandard.framework” — this is a harmless Razorpay limitation; untick “Upload your app's symbols” in the distribute dialog (see Troubleshooting).
ParkEZ uses Firebase Cloud Messaging (FCM) to deliver booking, check-in, and admin notifications across all four apps.
| Field | What to Enter |
|---|---|
| Notification Sender Key | The Sender ID from the Cloud Messaging tab |
| Client ID for Google Login | The Web client ID from Authentication → Google |
| Upload JSON File | The Service Account JSON downloaded from Firebase |
You can also enter your Google Maps Key and platform-wide settings (currency, commission %, support phone/email) under Settings → General Settings in the Admin Panel.
The Landing Page is a standalone marketing site under landing_page/. Edit all copy and images from the admin panel (Settings → Landing Page). Content loads from Firestore at runtime — no redeploy needed.
landing_page/
index.html Main marketing page
privacy-policy.html Privacy Policy (reads settings/constant.privacyPolicy)
terms.html Terms & Conditions (reads settings/constant.termsAndConditions)
app.js Hydrates index.html from Firestore
legal.js Hydrates both legal pages from Firestore
styles.css Shared stylesheet (indigo / lavender theme)
assets/ Logo, illustrations, step images and feature SVGs
README.md Quick start notes
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_STORAGE_BUCKET",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID",
measurementId: "YOUR_MEASUREMENT_ID"
};
Click "Add app" → the Web icon (</>) → enter a nickname (e.g. parkez-landing) → "Register app". The firebaseConfig will appear.
Open the admin panel → Settings → Landing Page. You can edit:
Click Save. Changes show on the next page load — no deploy needed.
privacy-policy.html and terms.html read from settings/constant — the same document the mobile apps use. Edit them in the admin's Settings → Policy Settings screen.
One source of truth — the apps and website always show the same legal text.
Two ways to preview the page:
cd landing_page
python -m http.server 8000
# or
npx serve .
Then open http://localhost:8000/.
Open DevTools → Console. A [ParkEZ] Loaded document log lists every field that was read — use it to verify your Firestore data loaded.
The landing page only reads from Firestore, so add these rules so visitors can see it without signing in:
match /settings/landing_page { allow read: if true; }
match /settings/constant { allow read: if true; }
Writes are still gated behind the Admin Panel's auth-required rules — the landing page never writes anything.
It's a plain static site. Deploy to any static host:
npm install -g firebase-tools
cd landing_page
firebase login
firebase init hosting
# - Use existing project → your Firebase project
# - Public directory: . (this current folder)
# - Configure as SPA? → No
# - Set up automatic builds? → No
# - Overwrite index.html? → No
firebase deploy --only hosting
You'll get a live URL like https://your-project.web.app.
Upload the whole landing_page/ folder to Netlify, Vercel, GitHub Pages, AWS S3 + CloudFront, or your own server. No build step needed.
Multi-language support spans the whole system. The Admin Panel is where you register languages, and the mobile apps (Customer, Owner, Watchman) show their UI in whichever language the user picks. It uses GetX and has three pieces that must stay in sync:
Adding a language in the Admin Panel only adds it to the list. The strings aren't downloaded from Firestore. If you add code de in the panel but the app has no matching app_de.dart, selecting it shows raw English keys. Both steps are required.
| Field | What to Enter | Example |
|---|---|---|
| Language name | Display name shown in the app's language list | English / हिंदी / العربية / Français / Português |
| Code | ISO language code — must match exactly the key in the app's strings file | en / hi / ar / fr / pt |
| Active | Whether users can see and pick this language | true / false |
| Default | Fallback for first-time users (only one allowed) | true / false |
// Firestore: languages/{autoId}
{
"id": "LzSABjMohyW3MA0CaxVH",
"name": "English",
"code": "en",
"active": true,
"defaultLanguage": true
}
Every app keeps its translations under a lang/ folder and registers them in LocalizationService. The structure is the same across all apps:
customer/ (also owner/, watchman/)
lib/
lang/
app_en.dart English strings -> const Map enUS
app_hi.dart Hindi strings -> const Map hiIN
app_ar.dart Arabic strings -> const Map lnAr
app_fr.dart French strings -> const Map frFR
app_pt.dart Portuguese strings -> const Map ptBR
services/
localization_service.dart Registers the maps with GetX
admin_panel/ (note the different path)
lib/
app/
lang/
app_en.dart, app_ar.dart, app_hi.dart, app_fr.dart, app_pt.dart
services/
localization_service.dart
Each file is a Dart const Map<String, String> of key → translated value. The key is the English text; the value is the translation.
// lib/lang/app_en.dart
const Map<String, String> enUS = {
"Skip" : "Skip",
"Welcome" : "Welcome",
"Home" : "Home",
"Bookings" : "Bookings",
"Wallet" : "Wallet",
"Profile" : "Profile",
"Search a place or address" : "Search a place or address",
"Book This Spot" : "Book This Spot",
"Spot Reserved" : "Spot Reserved",
// ... hundreds more key-value pairs
};
// lib/lang/app_ar.dart -> SAME keys, translated values
const Map<String, String> lnAr = {
"Skip" : "تخطي",
"Welcome" : "أهلاً",
"Home" : "الرئيسية",
"Bookings" : "الحجوزات",
"Wallet" : "المحفظة",
"Profile" : "الملف الشخصي",
"Book This Spot" : "احجز هذا الموقع",
// ... same keys, Arabic translations
};
Every language map must use the same keys. The key is what the UI looks up. If a language is missing a key, GetX shows the raw key text for that one string. Add new keys to every app_xx.dart file.
Variable naming convention across all apps:
| Language | Code | Variable | File |
|---|---|---|---|
| English | en | enUS | app_en.dart |
| Hindi | hi | hiIN | app_hi.dart |
| Arabic | ar | lnAr | app_ar.dart |
| French | fr | frFR | app_fr.dart |
| Portuguese | pt | ptBR | app_pt.dart |
| Spanish | es | esES | app_es.dart |
lib/services/localization_service.dart extends GetX's Translations class and maps each code to its strings map. The code here must match the Code from the Admin Panel (Step 1).
// lib/services/localization_service.dart
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import '../lang/app_en.dart';
import '../lang/app_hi.dart';
import '../lang/app_ar.dart';
import '../lang/app_fr.dart';
import '../lang/app_pt.dart';
class LocalizationService extends Translations {
static const locale = Locale('en', 'US'); // default locale
static final locales = [
const Locale('en'),
const Locale('hi'),
const Locale('ar'),
const Locale('fr'),
const Locale('pt'),
];
@override
Map<String, Map<String, String>> get keys => {
'en': enUS, // from app_en.dart
'hi': hiIN, // from app_hi.dart
'ar': lnAr, // from app_ar.dart
'fr': frFR, // from app_fr.dart
'pt': ptBR, // from app_pt.dart
};
void changeLocale(String lang) {
Get.updateLocale(Locale(lang)); // switches the active language at runtime
}
}
This file exists in all four apps:
In each app's main.dart, pass the translations and default/fallback locale to GetMaterialApp:
GetMaterialApp(
translations: LocalizationService(), // the maps from Step 4
locale: LocalizationService.locale, // Locale('en', 'US')
fallbackLocale: LocalizationService.locale, // used if a key/locale is missing
// ...
);
Call .tr on the key anywhere. GetX looks it up in the active language map and returns the translation.
// Simple string
Text("Home".tr)
AppButton(text: "Book This Spot".tr)
// Parameterized string -> .trParams replaces @placeholders
Text("spots_open".trParams({ "count": "12" }))
// active locale = ar -> resolves against lnAr map
// 1) switch the active language immediately
LocalizationService().changeLocale(selectedLanguage.code); // Get.updateLocale(...)
// 2) remember the choice for next launch (SharedPreferences)
Preferences.setString(
Preferences.languageCodeKey, // key: "languageCodeKey"
jsonEncode(selectedLanguage.toJson()), // value: full LanguageModel as JSON
);
// SplashScreenController
Future<void> redirectScreen() async {
final savedLang = Preferences.getString(Preferences.languageCodeKey);
if (savedLang == "null" || savedLang.isEmpty) {
Get.offAll(() => const LanguageScreenView(isFirstTime: true));
return;
} else {
final lang = LanguageModel.fromJson(jsonDecode(savedLang));
LocalizationService().changeLocale(lang.code ?? "en");
}
// ... continue to main screen
}
Controller & preference file locations:
| App | Language Screen Controller | Preferences File |
|---|---|---|
| customer | customer/lib/app/modules/language_screen/controllers/language_screen_controller.dart | customer/lib/utils/preferences.dart |
| owner | owner/lib/app/modules/language_screen/controllers/language_screen_controller.dart | owner/lib/utils/preferences.dart |
| watchman | watchman/lib/app/modules/language_screen/controllers/language_screen_controller.dart | watchman/lib/utils/preferences.dart |
| admin_panel | admin_panel/lib/app/modules/language/controllers/language_controller.dart | admin_panel/lib/app/services/shared_preferences/app_preference.dart |
☐ Add the language (Name + Code) in Admin Panel →
Settings → Languages.
☐ Create app_<code>.dart in all four apps
(admin_panel, customer, owner, watchman)
with every key translated.
☐ Import it + add the Locale and keys
entry in each app's localization_service.dart.
☐ Make sure the Code in the panel === the code key in
LocalizationService (e.g. fr).
☐ Remember: admin_panel uses lib/app/lang/, all others use
lib/lang/.
☐ Rebuild and run — the new language appears and translates the UI.
This chapter collects everything you can customize in ParkEZ in one place — what can be changed from the Admin Panel with no code at all, and what is changed in the source with a simple find-and-edit. Each topic links to its detailed step-by-step section.
| What to change | How |
|---|---|
| App display name (Android & iOS) | Changing Application Name — android:label, strings.xml, CFBundleDisplayName. |
| Admin Panel name & browser tab title | Changing Application Name (Admin Panel) and Admin Panel → Settings → App Settings → Brand Name. |
| Package name / bundle id (rebranding) | Changing Package Name in Flutter — applicationId + PRODUCT_BUNDLE_IDENTIFIER. Afterwards re-download google-services.json (see Troubleshooting). |
| App logo, launcher icons & splash | Changing Application Logo — Android mipmaps, iOS AppIcon set, and assets/images/logo.png per app. |
| Admin Panel favicon & web icons | Change the Favicon — favicon.png + web/icons/. |
| Landing page brand, hero & links | Admin Panel → Landing Page (CMS — no rebuild needed) — see Landing Page Setup. |
All apps use the bundled Figtree font family. To swap in your own font:
A full rebrand — name, package id, logo, colors, font, currency — touches only the files listed above and requires no code restructuring.
A visual reference for every screen across the ParkEZ apps — these are actual, high-quality screenshots captured from the running Customer, Owner, Watchman, and Admin applications. Click any image to view it full-screen.

















































































Every configuration screen in the Admin Panel, with a numbered legend explaining each control. The numbers read top-to-bottom against the screenshot.










ParkEZ is serverless: there is no custom REST API to host. All four apps talk directly to Firebase — Firestore for data, Firebase Auth for identity, Storage for images and Cloud Messaging for push. This section explains how the backend is structured and how to extend it.
Customer / Owner / Watchman (Flutter) Admin Panel (Flutter Web)
| | | |
+------------+------------+-----------+-----------+
|
Firebase Auth (identity: email, phone, Google, Apple)
Cloud Firestore (all application data)
Firebase Storage (profile images, parking photos, documents)
Cloud Messaging (push notifications)
| Collection | Purpose |
|---|---|
| customers / owners / watchman | User profiles per role (FCM token, wallet balance, status). |
| parking | Parking lots — location, price, floors, facilities, owner id. |
| slots | Individual slots per parking / floor with availability. |
| bookings | Reservations — customer, parking, slot, times, amount, status. |
| wallet_transaction / transaction_log | Wallet top-ups, payments and refunds. |
| review_customer | Ratings and review text per parking. |
| coupons / country_tax / currencies | Pricing helpers managed from the Admin Panel. |
| settings | Global runtime config read by every app at startup (see Configuration Guide). |
| notification / broadcast_notifications | Per-user and admin-broadcast notifications. |
| email_template | Transactional email templates edited in the Admin Panel. |
| onboarding_screen / languages / parking_facilities | App content managed from the Admin Panel. |
| admin / role_permissions / documents | Admin users, role-based access and owner KYC documents. |
Firestore and Storage rules ship with the package (see Storage Setup). If you add collections, extend the rules too — data is protected by rules, not by the API keys.
Flutter has no .env file by default — configuration lives in a few well-defined places. This table is the single reference for every value you must supply and exactly where it goes.
| Value | Where to put it |
|---|---|
| Firebase config (all apps) | lib/firebase_options.dart — generated by flutterfire configure. Never edit by hand. |
| Firebase Android config | android/app/google-services.json (download from Firebase Console per app). |
| Firebase iOS config | ios/Runner/GoogleService-Info.plist (download per app). |
| Google Maps key (Android) | android/app/src/main/AndroidManifest.xml → com.google.android.geo.API_KEY meta-data (customer & owner). |
| Google Maps key (iOS) | ios/Runner/AppDelegate.swift → GMSServices.provideAPIKey("…") (customer & owner). |
| Google Sign-In redirect (iOS) | ios/Runner/Info.plist → CFBundleURLSchemes = the REVERSED_CLIENT_ID from GoogleService-Info.plist. |
| Release signing (Android) | android/key.properties + your .jks keystore — see Generate JKS File. |
| Admin/landing web Firebase config | admin_panel/web/index.html, admin_panel/web/firebase-messaging-sw.js, landing_page/app.js + legal.js (the firebaseConfig object). |
| Setting | Admin Panel screen |
|---|---|
| App name, default currency, country code, branding | Settings → App Settings |
| Google Maps key used at runtime + map type | Settings → Map Settings |
| Payment gateways (Stripe / Razorpay / PayPal keys, sandbox toggle) | Settings → Payment Settings |
| FCM sender key + Google Login client id + service-account JSON | Settings → Notification Settings |
| SMTP host / port / credentials for transactional email | Settings → SMTP / Email |
| Taxes, currencies, email templates, policies | Settings → respective screens |
See the annotated screenshots of every settings screen for a field-by-field walkthrough.
Restrict your Google Maps key (by package name / bundle id / HTTP referrer) in Google Cloud Console, and protect data with Firestore Security Rules. Firebase web API keys are identifiers, not secrets — but payment gateway secret keys must only ever be entered in the Admin Panel, never committed to source.
All secret-key payment operations and every wallet/balance mutation run in Firebase Cloud Functions — the secret keys and wallet writes never happen on a device. The functions read the gateway keys from the admin-only settings/payment_secrets Firestore document, which the Admin Panel maintains automatically: whatever you save in Admin Panel → Settings → Payment Settings is picked up by the functions, while the apps only see the sanitized settings/payment document. See Cloud Functions for the one-time deployment steps — online payments will not work until the functions and security rules are deployed.
Solutions to common issues you might hit while setting up ParkEZ.
export PATH="$PATH":"$HOME/.pub-cache/bin"
flutter_localizations pins intl to an exact version. In pubspec.yaml set intl: 0.20.2 (no caret), then run flutter pub get.
The APNS token arrives asynchronously after launch. Ensure Push Notifications capability is enabled on the App ID, an APNS key (.p8) is uploaded to Firebase → Cloud Messaging, and test on a real device — the iOS Simulator cannot receive an APNS token.
Razorpay ships closed-source binaries without dSYMs; the App Store Connect warning is harmless. Untick “Upload your app's symbols” in the Xcode distribute dialog (or set uploadSymbols: false in ExportOptions.plist) to silence it.
In ios/ run pod repo update then pod install. Ensure the Podfile platform is platform :ios, '15.0' (required by current Maps/Firebase pods).
Run the app with flutter run --verbose and read the first red error line — it almost always names the exact file, key, or permission at fault. For anything unresolved, contact support with that log (see Support).
Yes. ParkEZ is intentionally split so each role (Customer, Owner, Watchman, Admin) ships as a focused product, can be branded / priced independently, and can be released to stores on its own schedule. All four share one Firebase backend.
Yes. Xcode is required for building iOS apps and runs only on macOS. You can develop and test the Android and Web (Admin Panel) versions on Windows or Linux without a Mac.
The free Spark plan is enough to get started. If you plan to use Phone OTP authentication heavily or expect large Storage usage, upgrade to the Blaze plan (pay-as-you-go); Blaze still includes a generous free usage tier.
ParkEZ is built around Firebase (Firestore, Auth, Storage, and FCM). Switching to a custom backend is possible but requires significant changes — mainly replacing the repository / service layer in each app.
Rebuild the modified app with flutter build apk, flutter build appbundle, or flutter build web and redeploy / upload to the relevant store or hosting target.
The Admin Panel → Settings → General Settings screen lets you set a platform commission percent. The Customer and Owner apps read this value when settling each booking and split the amount between the platform wallet and the owner wallet via secure Firestore transactions.
Owners accumulate balance in the in-app wallet. They can submit a Withdraw Request from the Owner App; admins approve / reject it from the Admin Panel, then mark it as paid after the bank transfer.
The item price covers the source code only. Firebase and Google Maps Platform are external Google services billed to your own account once you exceed their free tiers; payment gateways charge per-transaction fees; and app store accounts (Google Play one-time, Apple Developer yearly) are separate. See the cost warning in Prerequisites.
Yes. Changing the app name, package / bundle id, logo, colours, and default currency is covered step-by-step in Mobile Apps Setup and Admin Panel → App Settings. No code restructuring is required.
Add a new language map file in each app's lib/app/lang/, register it, and enable the language from the Admin Panel. The full walkthrough is in Localization (Multi-Language).
Stripe, Razorpay, PayPal, in-app Wallet, and Cash on Delivery. Each gateway is toggled and configured (keys, sandbox/live) from Admin Panel → Payment Settings — no rebuild needed.
Yes — the package ships full, unobfuscated Dart source for all four apps using a consistent GetX module structure (view / controller / binding per screen), a central data layer, and typed models. See the Developer Guide for how to extend it.
Refer to the support details provided with your ParkEZ purchase (email / WhatsApp on the product page).
Want to pull future ParkEZ releases into your customised project without losing your changes? Keep the project in Git and merge each release in as a branch. Below are the prep notes and the limits of our support.
Manual upgrades need solid Flutter and Firebase experience. Back up and test in a staging Firebase project first. We aren't liable for damage from manual upgrades — when stuck, check the official Flutter and Firebase docs.
Per Envato policy, installation, customisation, and upgrade help aren't covered by free item support — only bugs in the delivered code are.
The steps below are informational, to help you self-serve.
Don't open support tickets for upgrade issues — they're out of scope.
GitHub Desktop lets you keep your customisations on a private branch and merge each release in. Follow the steps in order.
Pull the ParkEZ v1.1 package and unzip it to a temporary folder — do not extract it over your existing project.
Open GitHub Desktop's History tab and walk through each commit from the v1.1 merge.
For every change, open the file in your editor next to the matching file in your real project.
Apply each change to your real project, one at a time, keeping the customisations you already had.
To skip Git, replace each app's lib/ folder with the one from the new release. Faster, but any custom code inside lib/ is lost — diff before overwriting.
Do not overwrite firebase_options.dart. Keep your existing Firebase config.
flutter clean
flutter pub get
On macOS, also run pod install --repo-update inside customer/ios (and the other iOS app folders).
This overwrites every file under lib/. Custom changes are lost unless re-applied. If you've heavily customised lib/, use the GitHub Desktop method above — it merges instead of replacing.
Support follows the Envato Item Support Policy:
| Covered | Not covered |
|---|---|
| Bugs and defects in the delivered code | Installation / server / store-publishing services |
| Questions about how a shipped feature works | Customisation or adding new features |
| Help locating the right setting or file | Issues caused by modified code or third-party plugins |