A SwiftUI + Firebase iOS app that brings every everyday pet-care workflow — profiles, grooming, vet requests, and a marketplace — into one place.
- Overview
- Features
- Tech Stack
- Architecture
- Project Structure
- Data Model
- Getting Started
- Usage
- Testing
- Configuration
- Firestore Rules & Security
- Troubleshooting
- Known Limitations & Roadmap
- Contributing
- Acknowledgments
- License
Meowtropolis is a native iOS companion app for pet owners, built entirely in SwiftUI on top of Firebase. The goal is to consolidate the scattered parts of caring for a pet — keeping track of who they are, booking grooming and vet visits, and shopping for supplies — into a single, coherent mobile experience instead of juggling multiple apps or paper records.
The app is designed as a full-stack demo/reference implementation: it pairs a polished, animated SwiftUI front end with a real Firebase backend (Authentication + Cloud Firestore), a protocol-oriented service layer that keeps business logic testable and swappable, and a lightweight state layer that drives navigation and screen data. It ships with:
- Bilingual UI — every user-facing string is available in English (US) and Bangla, switchable at runtime and persisted per-user (see
AppLanguageinAppDesign.swift). - Two navigation modes — a standard customer experience (
DashboardView) and a separate admin experience (AdminDashboardView) gated by an admin-email allowlist inAppState. - Offline-friendly sample data — a bundled JSON catalog that auto-seeds Firestore on first run, so the marketplace is populated without any manual data entry.
- A dedicated UI kit — the
images/andPet Care - Pet Services App UI Kit (No Image)/directories contain the full set of reference screen designs the app was built from.
This repository contains the iOS app source, both test targets, Firebase data-contract references, and the design/demo assets used to build and validate the app.
- Sign up, log in, log out
- Password reset
- Session restore on app start
- User-friendly auth error handling
In depth: Authentication is abstracted behind an AuthService protocol (Services/AuthService.swift), implemented concretely by FirebaseAuthService on top of Firebase Auth. AppState (State/AppState.swift) owns the single source of truth for session state — it listens to Firebase's auth-state-changed stream, resolves the authenticated user's Firestore profile, and exposes isLoggedIn, currentUser, isProfileLoading, and profileErrorMessage to the rest of the app via @Published properties. Auth errors from Firebase (wrong password, user not found, invalid email, weak password, network errors, etc.) are translated into friendly, localized (English/Bangla) messages instead of raw SDK error text.
- Personal information update
- Profile image upload with optimization/compression
- Password change and account delete with reauthentication safeguards
- Local activity history with noise filtering for meaningful events
In depth: Profile photo uploads are compressed client-side before being persisted as Base64 (profileImageBase64 on the User model) to keep Firestore document sizes reasonable. Sensitive account actions — changing the account email and deleting the account — require the user to re-enter their current password, satisfying Firebase's reauthentication requirement for security-sensitive operations. UserHistoryService records a locally-scoped activity trail (e.g. "Logged in", "Updated personal information", "Changed password") per user, filtering out noisy/duplicate events so the account screen shows a meaningful history rather than a raw event log.
- Create, edit, list, and delete pet profiles
In depth: Each Pet (Models/DataModels.swift) is linked to its owner via userId, and stores name, breed, and an optional age. PetService handles all Firestore CRUD against the pets collection, keeping the view layer free of direct Firestore calls.
- Grooming booking flow
- Vet request flow
In depth: Booking records capture serviceType, an ISO-8601 date, and a status (pending, confirmed, completed, cancelled) via the BookingStatus enum. BookingService writes these to the bookings collection. Vet requests use a dedicated, simpler VetRequest model and VetService, since vet workflows don't need the full booking lifecycle. ReminderService schedules local notifications for upcoming bookings so users get a reminder without any server-side push infrastructure.
- Product listing and detail views
- Search and sort/filter controls
- Add-to-cart and cart quantity management
- Checkout flow with stock-safe order placement (no real payments)
- Cart recommendations and savings summary
- One-time Firestore auto-seeding from bundled product JSON when products collection is empty
In depth: ProductService follows a Firestore-first strategy with a local JSON fallback (LocalProductService reads SampleData/products.json from the app bundle) — so the marketplace still works if Firestore is temporarily unreachable. ProductSeedService checks once whether the products collection is empty and, if so, uploads the bundled catalog (17 sample products spanning categories like cat litter, food, and accessories) so a brand-new Firebase project has real data to browse immediately. A local one-time seed flag prevents re-uploading on every launch. OrderService places orders inside a Firestore transaction that atomically checks and decrements product stock, so two users can't oversell the same limited-stock item. CartState (an ObservableObject injected app-wide) owns cart contents, quantities, and derived totals/savings shown in CartRecommendationsView.
- Built-in pet blog section with featured cards, list view, and detail pages
- Google Places-backed nearby place search flow (loading, empty, error, retry states)
In depth: MapsService performs one-time setup of the Google Maps and Google Places SDKs using an API key read from Info.plist. PlacesService and LocationService handle place search requests/responses (Models/PlaceSearchRequest.swift, Models/PlaceSearchResponse.swift, Models/Place.swift) and device location, while MapState drives the loading/empty/error/retry UI states shown in MapView.
- Separate admin-only home screen (
AdminDashboardView), gated by an email allowlist inAppState - A demo admin account is auto-provisioned on first login attempt for convenience during evaluation/demo (see Usage)
- Full English (US) / Bangla bilingual support via the
AppLanguageenum, with the preference persisted both locally (UserDefaults) and on the user's Firestore profile (preferredLanguageCode)
| Layer | Technology |
|---|---|
| UI | Swift, SwiftUI |
| Backend | Firebase Authentication, Cloud Firestore |
| Location | Google Maps SDK, Google Places SDK |
| Local persistence | UserDefaults (language preference), on-device local notifications (booking reminders) |
| Reactive state | Combine / ObservableObject (AppState, CartState, MapState, MarketplaceState) |
| Testing | XCTest (unit + UI) |
Why this stack: SwiftUI + Firebase keeps the project fully native with no custom backend to host or operate — Authentication and Firestore cover the app's identity and data needs, while Google Maps/Places covers location discovery. The protocol-oriented service layer (AuthService, and concrete FirebaseAuthService) means Firebase could be swapped for another backend without touching the UI or state layers.
Architecture: a clean, layered structure — Models (Codable domain types), Services (Firebase/Maps/data access), State (observable orchestration), and Views (SwiftUI screens). UI stays in Views, side effects stay in Services, and flow orchestration stays in State.
Meowtropolis follows a straightforward four-layer separation, enforced by directory structure rather than a heavyweight framework:
Views → State → Services → Firebase / Google APIs
(UI) (orchestration) (side effects) (external systems)
↓
Models
(shared data shapes)
Models/— plainCodable, Sendablestructs (User,Pet,Booking,Product,Order,OrderItem,VetRequest,Place,PlaceSearchRequest,PlaceSearchResponse,CartItem) that define the data contracts shared between Firestore, services, and the UI.Firestore/ModelCoding.swiftcentralizes Firestore-specific encode/decode helpers, andFirestore/FirestoreCollectionLegacy.swiftretains legacy collection-name handling.Services/— one service per responsibility, each documented in-file:AuthService/FirebaseAuthService— authentication interface + Firebase implementationUserService— Firestore CRUD for user profilesPetService— Firestore CRUD for pet profilesBookingService— Firestore CRUD for grooming/service bookings, plus a manual smoke-test helperVetService— minimal Firestore CRUD for vet requestsProductService— Firestore-first product loading with local JSON fallback, plus manual validation helpersLocalProductService— loads the bundled product catalog fromSampleData/products.jsonFirestoreProductService— loads product data from FirestoreProductSeedService— one-time seeding of bundled products into FirestoreOrderService— stock-safe order creation via Firestore transactionsMapsService— one-time Google Maps/Places SDK setupPlacesService/LocationService— nearby-place search and device locationReminderService— local-notification scheduling for booking remindersUserHistoryService— local, filtered activity history per userFirestoreCollections— a single enum of Firestore collection name constants, used everywhere instead of hardcoded stringsBackendSmokeTests— simple, manual smoke tests for backend services aimed at beginner-friendly debugging
State/—ObservableObjectclasses that orchestrate screen-level data flow and are injected via@EnvironmentObject/@StateObject:AppState(session, profile, admin routing),CartState(cart contents/totals),MapState(nearby-places loading/empty/error/retry),MarketplaceState(product listing/search/filter state).Views/— SwiftUI screens grouped by area:Auth/(landing, login, signup, forgot password, OTP verification),Main/(dashboard, admin dashboard, pet profile, grooming, vet, marketplace, product detail, related products, cart, cart recommendations, checkout, account, blog, map, services catalog),Common/(design system inAppDesign.swift, onboarding, splash),Shared/(reusable primitives: cards, empty/error/loading states, placeholder images, spacing and text style constants).
Design principle: keep UI concerns in Views, side effects (network/Firebase/notifications) in Services, and cross-screen flow/state orchestration in State. Views should be able to render from State alone, and State should never talk to Firebase directly — it always goes through a Service.
Meowtropolis/
├── Meowtropolis/ # App target source
│ ├── MeowtropolisApp.swift # @main entry point, Firebase bootstrap
│ ├── ContentView.swift
│ ├── Firestore/
│ │ ├── ModelCoding.swift
│ │ └── FirestoreCollectionLegacy.swift
│ ├── Models/
│ │ ├── DataModels.swift # User, Pet, Booking, Product, Order, OrderItem
│ │ ├── CartItem.swift
│ │ ├── VetRequest.swift
│ │ ├── Place.swift
│ │ ├── PlaceSearchRequest.swift
│ │ └── PlaceSearchResponse.swift
│ ├── SampleData/
│ │ └── products.json # 17 bundled sample products
│ ├── Services/ # One file per backend/system responsibility
│ ├── State/ # AppState, CartState, MapState, MarketplaceState
│ └── Views/
│ ├── Auth/
│ ├── Common/
│ ├── Main/
│ └── Shared/
├── MeowtropolisTests/
│ └── MeowtropolisTests.swift
├── MeowtropolisUITests/
│ ├── MeowtropolisUITests.swift
│ └── MeowtropolisUITestsLaunchTests.swift
├── docs/
│ ├── erdiagram.xml # Entity-relationship diagram source
│ └── validate_asset_catalog.ps1 # Asset catalog validation script
├── images/ # 14 reference screen screenshots
├── Pet Care - Pet Services App UI Kit (No Image)/ # Source UI kit assets
├── firestore.rules # Development-baseline security rules
├── GoogleService-Info.plist.example # Sanitized Firebase config template
├── SECURITY.md
├── LICENSE
└── README.md
Firestore collection names are centralized in FirestoreCollections and used everywhere instead of hardcoded strings:
| Collection | Model | Key Fields |
|---|---|---|
users |
User |
id, name, email, preferredLanguageCode?, profileImageBase64?, role? |
pets |
Pet |
id, userId, name, breed, age? |
bookings |
Booking |
id, userId, petId, serviceType, date (ISO-8601), status (pending | confirmed | completed | cancelled) |
products |
Product |
id, name, price, category, imageURL, stock (defaults to 50 if absent) |
orders |
Order |
id, userId, items: [OrderItem], totalAmount, currencyCode, status, createdAt |
vetRequests |
VetRequest |
see Models/VetRequest.swift |
OrderItem snapshots the product data at time of purchase (productId, name, category, imageURL, unitPrice, quantity, lineTotal) so historical orders remain accurate even if a product's price or listing changes later.
Sample product record (from SampleData/products.json, used for local fallback and one-time Firestore seeding):
{
"id": "product_cat_001",
"name": "Black Sand Bentonite Cat Litter 10L",
"price": 620,
"category": "cat",
"imageURL": "img_black_sand_carbon_bentonite_cat_litter_10l_600x588",
"stock": 15
}An entity-relationship diagram source is available at docs/erdiagram.xml.
- macOS with a recent stable Xcode
- iOS Simulator or a physical iPhone
- A Firebase project with Authentication and Cloud Firestore enabled
- A Google Cloud project with Maps SDK for iOS and Places API enabled
- Clone the repository
git clone https://github.com/mayer-doa-coder/Meowtropolis.git cd Meowtropolis - Open the project in Xcode
open Meowtropolis.xcodeproj
- Add your Firebase config — download
GoogleService-Info.plistfrom your Firebase project's console (Project settings → General → Your apps) and drag it into theMeowtropolisapp target in Xcode. A template describing the expected fields is atGoogleService-Info.plist.example.- In Firebase Console, enable Authentication → Sign-in method → Email/Password.
- Enable Cloud Firestore and create a database (start in the default/native mode).
- Add your Google Maps/Places key — add a
GOOGLE_MAPS_API_KEYentry to the app target'sInfo.plistwith your key value (see Configuration). In Google Cloud Console, make sure both Maps SDK for iOS and Places API are enabled for that key's project, and restrict the key to your iOS bundle identifier once things work. - Set your signing team in Xcode (Signing & Capabilities) if you plan to run on a physical device.
- Build and run on a simulator or device (
⌘R).
Once configured, launch the app to land on the splash/onboarding flow, then sign up or log in. From the dashboard you can:
Dashboard → Pet Profile → add/edit/remove pets
→ Grooming / Vet → book appointments or submit vet requests
→ Marketplace → browse, add to cart, checkout
→ Pet Blog → read featured articles
→ Map → find nearby pet-friendly places
The marketplace auto-seeds sample products into Firestore the first time you open it on a fresh project (see Services/ProductSeedService.swift), so there's no manual data entry needed to try it out.
For quick evaluation without hand-provisioning an admin account, AppState recognizes a built-in demo admin login (admin@meowtropolis.com). Logging in with those demo credentials on a fresh Firebase project auto-provisions the account and its role: "admin" Firestore profile on first attempt, then routes into AdminDashboardView instead of the regular customer DashboardView. For any real deployment, remove or replace this demo shortcut and manage admin access exclusively through your own allowlist/role system.
Use the language selector (persisted via AppLanguage/UserDefaults and mirrored to the user's Firestore profile) to switch the entire UI between English (US) and Bangla at runtime — no restart required.
Two test targets are included:
MeowtropolisTests— unit tests (MeowtropolisTests.swift)MeowtropolisUITests— UI tests, including a dedicated launch-performance test (MeowtropolisUITestsLaunchTests.swift)
Run either from Xcode's Test navigator, or from the command line:
xcodebuild test \
-project Meowtropolis.xcodeproj \
-scheme Meowtropolis \
-destination 'platform=iOS Simulator,name=iPhone 15'For quick, beginner-friendly manual verification of backend wiring (without a full XCTest run), see Services/BackendSmokeTests.swift and the smoke-test helper in Services/BookingService.swift.
| Key | Where | Purpose |
|---|---|---|
GoogleService-Info.plist |
App target root | Firebase project credentials (Auth + Firestore) |
GOOGLE_MAPS_API_KEY |
Info.plist |
Enables Google Maps SDK and Places API lookups |
appLanguageCode |
UserDefaults (key defined by AppLanguage.storageKey) |
Persists the selected UI language (en-US or bn-BD) across launches |
Firestore's development-baseline rule (firestore.rules) allows read/write to any authenticated user — tighten this before shipping to production.
Admin access is controlled by a hardcoded email allowlist (adminEmails) inside State/AppState.swift. For production use, replace this with a proper role/claims-based authorization mechanism instead of an in-app allowlist.
The repository ships with a development-baseline Firestore rule set (firestore.rules):
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
This means any authenticated user can read or write any document — convenient for local development and demos, but not suitable for production. Before shipping, scope rules per-collection (e.g. users can only write their own users/{uid} document, orders are only readable by their owner, products are admin-write / public-read, etc.).
A few practical security notes for contributors:
- Never commit a real
GoogleService-Info.plist— it's already excluded via.gitignore; useGoogleService-Info.plist.exampleas the template for the fields Xcode/Firebase expect. - Treat any Google/Firebase API key that ends up in git history as compromised — rotate or restrict it (by bundle ID / API scope) in Google Cloud Console rather than assuming deletion from a later commit is sufficient.
- See
SECURITY.mdfor how to report a vulnerability.
- Ensure user is still authenticated.
- Large images are automatically resized/compressed before save.
- If Firestore errors still occur, verify network and Firestore permissions.
- Open marketplace once while signed in to trigger one-time auto-seeding.
- If one manual product already exists, seeding is skipped by design.
- Confirm API key exists and required APIs are enabled.
- Confirm key restrictions allow your iOS bundle/app setup.
- Verify GoogleService-Info.plist belongs to the same Firebase project you are inspecting in console.
- Admin routing requires both
isAdmin(email present in theadminEmailsallowlist inAppState) andprefersAdminHome(set when logging in through the demo-admin path) to be true — logging in with a non-allowlisted email will always route to the regular customer dashboard.
- Confirm the app has notification permission granted on the device/simulator;
ReminderServiceschedules local notifications but cannot deliver them without OS-level permission.
- Checkout does not process real payments —
OrderServiceplaces stock-safe orders in Firestore, but there is no payment gateway integration; this is a demo/prototype checkout flow. - Firestore rules are a development baseline, not production-hardened (see Firestore Rules & Security).
- Admin access uses a hardcoded email allowlist rather than custom claims/roles — fine for a demo, not for multi-admin production use.
- Demo admin auto-provisioning exists for convenience during evaluation and should be removed before a public production release.
Contributions are welcome. To propose a change:
- Fork the repo and create a feature branch
- Keep changes scoped — UI in
Views, side effects inServices, orchestration inState - Add or update tests under
MeowtropolisTests/MeowtropolisUITestswhere relevant - Open a pull request with a clear description of what changed and why
Please don't commit real Firebase credentials or API keys — use the .example template and your own local config.
- UI reference designs sourced from the "Pet Care - Pet Services App UI Kit" (see
images/andPet Care - Pet Services App UI Kit (No Image)/). - Built with Firebase (Authentication, Cloud Firestore) and Google Maps Platform (Maps SDK, Places API).
Released under the MIT License.