storehop

Storehop

A native Android shopping-list app for people who shop at more than one store. One master list of items is tagged across multiple stores; each store gets its own per-aisle shopping view, and checking an item off at one store cascades — buying mozzarella at Lidl drops it from the Aldi and Pingo Doce lists too, so a single shopping trip satisfies the need.

The app ships with a starter set of stores and categories. Anything not on that list (a regional chain, a one-off shop, a category for a specific household) is added or renamed by the user.

Status

Android v0.8.0 (Play Closed testing) introduces a one-time $7.99 Premium IAP that gates household-invite creation and CSV export. The app stays Free to install; everything you’d want from v0.7.x — cloud sync, cross-store cascade, joining + using a shared household someone else invited you to — is still free. The inviter-pays model means whoever pays once can generate invites; invitees join + use the shared household unconditionally. Two paths grandfather users into a silent LegacyUser entitlement (functionally equivalent to Premium): a Firebase account creationTimestamp predating the v0.8 release date OR a match against the explicit PREMIUM_VIP_EMAILS allowlist in EntitlementRepository.kt. The dev account, Mike, Amanda, and Mom are in that allowlist so they keep free Premium permanently. See the v0.8.0 entry in CHANGELOG for the full breakdown, including the in-line .1.4 patches (VIP allowlist, vc bumps, Mom added, and the v0.8.0.4 fix for a pull-before-push race where local soft-deletes were being resurrected by the next pull).

Android v0.7.1 shipped a lossless sideload-APK → Play migration path: user preferences (theme, language, sort modes, hide-checked-off) cloud-sync to /userPrefs/{uid} via Firestore, and Settings → Data has a “Force sync now” button so beta testers can drain every pending write before uninstalling. Full runbook at docs/v0.7.1-migration.md.

Android v0.7.0 shipped multi-user household sharing on top of the existing single-user feature set: invite-code generate (8-char Crockford base32, 24h TTL, single-use) + join + leave, household-scoped data for every entity (items, stores, categories, xref, store-category-order, purchase-records), and a cross-store cascade that now extends across household members by design (Amanda buying milk at Aldi drops Mike’s “milk needed at Lidl” entry). Statistics deliberately stay per-user so Mike sees what HE bought, not the household combined. Existing v0.5–v0.6 features all carry over: anonymous-first onboarding with optional Google Sign-In, two-way Firestore + Storage cloud sync (push and pull), Shop and Items tabs with item photos, share-list-as-text, theme + language picker (English, European Portuguese, Spanish, Italian), drag-reorder stores (long-press on any tile), per-store aisle ordering, cross-store check-off cascade, Manage Categories, hide / show checked-off items toggle, QuickAdd autocomplete against the master Items library, in-app update prompt via Play Core, CSV import / export of items and categories, and a Statistics screen with a 12-week trend chart. See docs/play-store-submission.md for the Play Console listing answers and docs/privacy-policy.md for the privacy policy hosted at the Play listing’s required URL.

iOS port (in ios/ — SwiftUI + GRDB + Firebase iOS SDK, mirrors the Android architecture 1:1): now at v0.8.1 in main, fully caught up to the Android branch. v0.8.1 brings the bulk store-tag flow over (long-press an Items-list row → contextual toolbar → “Tag to stores…” sheet → apply across multiple items in one transaction). Android’s v0.8.1 architectural xref-join fix is a no-op on iOS because GRDB has no equivalent of Room’s leaky @Junction — every iOS join query has filtered tombstones in SQL since day one.

Beyond Android parity, the iOS branch is now App Store-ready code-side: the AppIcon ships (1024×1024 sage flatten of the shared design/shophop-icon-512.png source), light + dark mode both render the actual brand palette (a Hex-the-Asset-Catalog-namespace fix that was silently failing before — the app was painting with iOS system defaults). Full E2E suite of 13 XCUITest cases + a DesignSystemTourTest visual-regression artifact that walks every major screen in both appearances. 187 unit + 13 UI tests green.

TestFlight ship is gated on the Apple-side config bundle: real GoogleService-Info.plist, DEVELOPMENT_TEAM in project.yml, PrivacyInfo.xcprivacy manifest, App Store Connect premium_lifetime IAP setup, and a 2-device manual smoke. Full walkthrough at docs/ios-app-store-submission.md.

Tech stack

Building

Open the project in Android Studio and let it sync. From the command line, the standard tasks are:

./gradlew :app:assembleDebug         build the debug APK
./gradlew :app:installDebug          install on a connected device
./gradlew :app:testDebugUnitTest     run the unit-test suite
./gradlew :app:bundleRelease         build the signed release AAB

Release signing reads keystore.properties at the repo root (gitignored; see app/build.gradle.kts for the expected keys). When that file is absent the release task succeeds but the AAB is left unsigned, so CI and fresh checkouts can still run assembleDebug and the unit tests.

The unit-test suite uses Robolectric and runs without an emulator.

Project layout

app/src/main/
    java/com/storehop/app/
        auth/             Google Sign-In, FirebaseAuth session provider
        data/
            dao/          Room DAOs
            db/           Database, migrations, seeder, JSON-backed seeds
            entity/       Sync-ready entities (UUID PKs, soft delete)
            prefs/        DataStore-backed user preferences
            repository/   Repository interfaces and implementations
            storage/      Firebase Storage uploader for item photos
            util/         IdGenerator, UserSessionProvider
        di/               Hilt modules (App, Database, Firebase, Prefs, Repo)
        sync/             Push side of the Firestore sync engine + DTOs
        ui/
            auth/         Sign-in screen
            items/        Items master list, Add/Edit form
            nav/          Compose Navigation routes
            settings/     Account, theme, language picker
            shop/         Store picker, Shop-at-Store, share-as-text
            theme/        Material 3 theme
            util/         CategoryLabel localization helper
        MainActivity.kt
        StorehopApplication.kt
    assets/seed/          stores.json, categories.json, store_categories.json
    res/
        values/           English strings, theme colors, launcher icon
        values-pt-rPT/    European Portuguese strings

Data layer

Every entity carries id (UUID), createdAt, updatedAt, deletedAt (soft-delete tombstone), userId, and pendingSync. This shape supports offline edits, deterministic merging across devices, and the push-side Firestore sync engine in sync/. Per-store need state lives on item_store_xref.isNeeded; the default check-off cascades across every tagged store (one trip clears the list everywhere), and the manual markNeededAtStore path leaves a per-store override hook for later if we ever want to expose it. The Room schema is exported to app/schemas/ and tracked in version control so migrations are reviewable.

Seeded stores and categories use stable string IDs (for example store_lidl, cat_produce) rather than generated UUIDs so the seed pack remains stable across devices and across reseeds.

Roadmap