Sports nutrition e-commerce app built with Kotlin Multiplatform and Compose Multiplatform.
The repository includes:
- Android and iOS apps,
- seller/admin flows inside the mobile app,
- a Firebase backend (Auth, Firestore, Storage, Analytics, Crashlytics, and Functions),
- tooling for seed data, docs generation, and Android release automation.
- Kotlin Multiplatform, Compose Multiplatform, and Material 3
- MVVM architecture, repositories, and DI with Koin
- Firebase Auth, Firestore, Storage, Analytics, Crashlytics, and Functions
- Ktor, Kotlin Coroutines, and Flow
- Dokka, GitHub Actions, and Firebase CLI
Modular structure by domain and feature:
:androidApp-> Android host app, splash screen, permissions, and Android-only integrations:composeApp-> shared Compose app entry point exported to Android and iOSiosApp-> iOS host project (Xcode) and iOS lifecycle integrations:core-> domain models and shared navigation/request-state primitives:domain-> repository contracts and use cases:data-> Firebase-backed repository implementations:di-> Koin wiring and initialization:shared-> shared UI, theming, i18n, analytics, preferences, and platform helpers:navigation-> routes, stack rules, and feature wiring:feature:auth-> login/register, Google Sign-In, forgot password, email verification:feature:home+ submodules -> home shell, catalog, categories, search, cart, checkout, and store pickup point selection:feature:details-> product detail and guided add-to-cart flow:feature:orders-> authenticated customer order history and order detail:feature:blog-> localized educational video/blog content:feature:contact-> support/contact flow:feature:profile,:feature:settings-> profile and account settings:feature:admin_panel,:feature:admin_panel:manage_product-> seller product management:feature:payment_completed-> post-checkout result screen:dbseed-> Firestore seed data and maintenance utilitiesFirebase/functions-> Cloud Functions for checkout, push acknowledgements, stock sync, product i18n sync, hard delete, auth-provider lookup, and order email documents
Useful verification commands:
./gradlew :androidApp:assembleDebug
./gradlew quickAndroidTests
./gradlew androidCi
./gradlew allTestWithReport- Email/password and Google Sign-In authentication.
- Product catalog with tokenized search and EN/ES localization.
- Guided add-to-cart flow from the product details screen.
- Cart and checkout with customer profile validation and two delivery methods: home delivery (requires full address) and store pickup (pickup point selection with map).
- My Orders for authenticated customers: order history list, empty state, and order detail.
- Backend-managed checkout for both PayPal and cash on delivery. Order creation is server-side
only; the client never writes to the
orderscollection. - Seller/admin product management with stock validation and soft deactivation rules.
- Role model (
user,seller,admin) incustomers/{uid}/privateData/roles. - Push notifications with topic subscription and delivery/open acknowledgement on Android and iOS.
- Blog/video content and contact support flow.
The app is designed to remain functional in low-connectivity or fully offline conditions.
- Firestore offline persistence — enabled at app startup via
configureFirebasePersistence()before any Firestore operation runs. Cache size is set to 100 MB. - Coil3 disk cache — image cache limited to 50 MB so the catalog renders from disk when the network is unavailable.
NetworkMonitoris a KMP interface in:domainwith a singleFlow<Boolean>property (isOnline). Platform implementations live in:data:- Android:
AndroidNetworkMonitor— backed byConnectivityManager. - iOS:
IosNetworkMonitor— backed byNWPathMonitor.
- Android:
ConnectivityViewModelin:sharedexposes bothisOnlineandhasResolvedConnectivity. The UI stays in a safe unresolved state until the first platform snapshot arrives, which avoids false positives on iOS startup.OfflineChipis a dismissible bottom chip in:sharedthat appears after connectivity has been resolved and the device is offline. It is hoisted intoNavGraph, so every screen inherits it automatically.- Critical flows that cannot complete offline are guarded at action level:
CheckoutScreenblocks PayPal and cash-on-delivery actions.ManageProductScreenblocks save, delete, upload-image and delete-image actions.
| Operation | Offline behaviour |
|---|---|
| Catalog browsing | Reads from Firestore cache — fully functional |
| Product details | Reads from Firestore cache — fully functional |
| Order history | Reads from Firestore cache — fully functional |
| Checkout (cash on delivery) | Blocked by action guards — requires connectivity |
| Checkout (PayPal) | Blocked by action guards — requires connectivity |
| Seller product upload/edit | Blocked by action guards — requires connectivity |
| Write operations (cart, profile) | Queued by Firebase SDK and synced when back online |
Non-critical reads (catalog, order history) work entirely from the local Firestore cache. Critical writes that involve backend validation — checkout and admin actions — are blocked with a clear message and an invitation to retry when connectivity is restored.
- Order creation is handled by Firebase Cloud Functions, not directly by the mobile client.
Pay on deliverycalls a backend endpoint that validates the authenticated user, cart content, stock, and total amount before creating the order.- PayPal checkout is split into backend order creation and backend capture confirmation. The mobile app only opens the approval URL and sends the approved token back to the backend.
- Firestore rules block direct client-side writes to
orders, so only trusted backend code can create final order documents. - PayPal credentials are stored in Firebase Functions secrets, never in the app binary or repository source files.
- The drawer includes a dedicated My Orders entry for authenticated customers.
- The orders screen streams the current user's orders in real time and sorts them by most recent first.
- Empty-state and error-state UI are handled in the screen itself, so the user always gets feedback when there are no orders or when loading fails.
- Tapping an order opens a detail screen that shows the order reference, date, admin status, total amount, and all purchased items.
- The detail screen loads product metadata separately so item rows can show product thumbnails and names; if a product no longer exists, the UI falls back to the product id.
- Order status chips reflect the backend status values (
processing,completed,cancelled) and use the shared theme palette so they adapt to light and dark mode.
- JDK 21
- Android Studio (latest stable recommended)
- Xcode 16+ (for iOS)
- Node.js 24+ (for
Firebase/functions) - Firebase CLI (
npm i -g firebase-tools) if you deploy backend resources
- Open the repository once in Android Studio so
local.propertiespoints to your Android SDK. - If you build iOS locally, set
TEAM_IDiniosApp/Configuration/Config.xcconfig. - If you use the Firestore seed utilities, place your Firebase Admin SDK JSON at
dbseed/serviceAccountKey.json. - If you want automatic order emails, install/configure a mail sender for the Firestore
mailcollection (for example the Firebase Trigger Email extension). - If you want automatic product i18n completion in Cloud Functions, enable the Google Cloud Translation API in the Firebase/GCP project.
- If you want the pickup-point map on Android, fill the root
secrets.propertiesfile withMAPS_API_KEY=.... The repository shipslocal.defaults.propertiesas a safe fallback and the Android build reads the real key through the Secrets Gradle Plugin.
Android build from the project root:
./gradlew :androidApp:assembleDebugRun from Android Studio when you need an emulator/device install, or use the generated APK/AAB from Gradle tasks.
For iOS:
- Set
TEAM_IDiniosApp/Configuration/Config.xcconfig. - Open
iosApp/iosApp.xcodeprojin Xcode. - Run the
iosAppscheme on a simulator or device.
Fast Android suite (no iOS, ideal for daily iteration):
./gradlew quickAndroidTestsGenerates the Android-only HTML report at build/reports/tests/platform/android/index.html.
Android CI suite used by GitHub Actions:
./gradlew androidCiFull Android+iOS suite with HTML dashboard:
./gradlew allTestWithReportGenerates a cross-platform dashboard at build/reports/tests/crossPlatform/index.html.
Initial seed data lives in dbseed/seeds/products.json, dbseed/seeds/customers.json, and
dbseed/seeds/orders.json.
Seed Firestore from the repository root:
./gradlew :dbseed:runUpdate only localized product fields for existing documents:
./gradlew :dbseed:updateProductsI18nSeed only the pickup-point sample data:
./gradlew :dbseed:seedPickupPointsNotes:
:dbseed:runclears and recreates thecustomers,products,orders, andpickup_pointscollections.:dbseed:seedPickupPointsonly clears and recreatespickup_points.- Both seed tasks expect
dbseed/serviceAccountKey.jsonto exist locally.
API documentation is written with KDoc in Kotlin sources and generated with Dokka.
Commands (run from the repository root):
./gradlew docsAndroidApp
./gradlew docsProject
./gradlew docsProjectOpen
./gradlew docsProjectServe
./gradlew docsProjectServeStopNotes:
- Consolidated HTML index:
build/dokka/html/index.html docsProjectServestarts a local background server and finishes successfully.- Short developer guide:
docs/kmp-documentation-guide.md
This project is prepared to validate Android changes in GitHub Actions and to generate signed
release artifacts for :androidApp (applicationId = dev.andrescoder.nutrisport).
pull_request -> main: runs./gradlew androidCiand uploads test/lint reports.push -> main: runs the same validation and then builds a signed.aaband.apk.workflow_dispatch: lets you trigger the same release-artifact flow manually from GitHub and, when needed, overrideversionCodeandversionNamefor a Play Store-ready release.- Artifacts are uploaded to GitHub Actions, not committed back into the repository.
- Commit the
.github/workflows/files in this repository. - Create these GitHub Actions secrets in
Settings -> Secrets and variables -> Actions:ANDROID_UPLOAD_KEYSTORE_BASE64ANDROID_UPLOAD_STORE_PASSWORDANDROID_UPLOAD_KEY_ALIASANDROID_UPLOAD_KEY_PASSWORDANDROID_MAPS_API_KEY
- Protect
main, require pull requests, and require theVerify Androidjob before merge. - Keep
versionCodeandversionNameinandroidApp/build.gradle.ktsup to date for normal branch builds, or provide them manually when launching a final Play Store release withworkflow_dispatch.
If you already have the Play upload keystore, convert it to Base64 and store that value in
ANDROID_UPLOAD_KEYSTORE_BASE64.
macOS:
base64 -i "$HOME/keys/nutrisport-upload.jks" | pbcopyLinux:
base64 -w 0 "$HOME/keys/nutrisport-upload.jks" | xclip -selection clipboardRun from the repository root:
./gradlew androidCiIf you need a local signed release build, export the signing credentials first:
export NUTRISPORT_UPLOAD_STORE_FILE="$HOME/keys/nutrisport-upload.jks"
export NUTRISPORT_UPLOAD_STORE_PASSWORD="<store-password>"
export NUTRISPORT_UPLOAD_KEY_ALIAS="nutrisport_upload"
export NUTRISPORT_UPLOAD_KEY_PASSWORD="<key-password>"Then build both release artifacts:
./gradlew androidReleaseArtifacts \
-Pandroid.injected.signing.store.file="$NUTRISPORT_UPLOAD_STORE_FILE" \
-Pandroid.injected.signing.store.password="$NUTRISPORT_UPLOAD_STORE_PASSWORD" \
-Pandroid.injected.signing.key.alias="$NUTRISPORT_UPLOAD_KEY_ALIAS" \
-Pandroid.injected.signing.key.password="$NUTRISPORT_UPLOAD_KEY_PASSWORD"Expected outputs:
androidApp/build/outputs/bundle/release/androidApp-release.aabandroidApp/build/outputs/apk/release/androidApp-release.apk
Optional verification:
jarsigner -verify -verbose -certs androidApp/build/outputs/bundle/release/androidApp-release.aab- For regular branch automation, merge to
mainand let the workflow build signed artifacts using the versions committed inandroidApp/build.gradle.kts. - For a definitive Play Store release, open
Actions -> Android CI and Release Artifacts -> Run workflow. - Fill in both manual inputs:
release_version_coderelease_version_name
- Run the workflow and wait for
Build Release Artifactsto finish in green. - Download the
android-release-...artifact from the run page. - Extract the ZIP downloaded by GitHub and upload the
.aabtoTesting -> Internal testingin Play Console. - Promote to production with staged rollout after validating internal metrics.
Important:
release_version_codeandrelease_version_namemust be provided together for manual releases.- Play Console requires a unique
versionCodefor every uploaded release.
- Never commit the upload keystore or passwords to Git.
- Keep at least two encrypted backups of the
.jksfile. - If you lose the upload key material, future Play updates may require a key reset process.
- Before the first production release, complete Play App Signing, Data safety, Content rating, and Privacy policy requirements.
cd Firebase/functions
npm install
npm run build
cd ..
firebase deploy --only functionsSelective deploy:
cd Firebase
firebase deploy --only functions:sendPushNotification,functions:hardDeleteProduct,functions:setUserAccessStateCheckout-related Functions:
cd Firebase
firebase deploy --only functions:createCashOnDeliveryOrder,functions:createPaypalOrder,functions:capturePaypalOrderPayPal secrets for checkout:
cd Firebase
firebase functions:secrets:set PAYPAL_CLIENT_ID
firebase functions:secrets:set PAYPAL_CLIENT_SECRETNotes:
- When rotating a PayPal secret, update it with
firebase functions:secrets:set ...and redeploy the PayPal Functions so the new secret version is picked up. - For a safe rollout, deploy checkout Functions before deploying stricter Firestore rules that block direct client order creation.
syncProductLocalizedTextrelies on the Google Cloud Translation API being enabled in the target project.createEmailDocumentonly writes to the Firestoremailcollection; a mail sender extension or equivalent backend consumer must exist in the Firebase project for emails to actually be sent.
cd Firebase
firebase deploy --only firestore:rulesCurrent checkout security model:
- Clients can read their own orders, but cannot create
ordersdocuments directly. - Final order creation happens only through Cloud Functions / Admin SDK.
- Pickup points are read-only for authenticated users and live in the root
pickup_pointscollection.
cd Firebase
firebase deploy --only firestore:indexesFor this feature, no extra composite index is required for pickup_points; the app only uses a
single-field equality filter on isActive.
- The admin web dashboard is maintained in a separate project (
nutrisport-admin).
































