diff --git a/README.md b/README.md index 3ada8c30..6bf9b6bc 100644 --- a/README.md +++ b/README.md @@ -1,269 +1,183 @@

- Closer feature graphic + Closer feature graphic

Closer

A private space for two.
- Private daily questions, intentional reveals, shared games, and calm rituals for couples. + Daily questions, mutual reveals, shared games, and calm rituals for couples.

- Android - iOS - Backend - Min Android - Min iOS - Kotlin - Swift - License + Screenshots + · + What makes it different + · + Status + · + Run locally + · + QA +

+ +

+ + Android reference app · iOS scaffold in progress · Firebase backend · RevenueCat billing · Private repo +

--- -> **Private daily questions for couples — end-to-end encrypted, never read, never sold.** -> *You and your paired partner hold the only key.* +Closer is a native relationship app for couples who want a quieter way to check in with each other. +Each partner answers privately, reveals intentionally, and keeps a record of the conversations that +matter. -A native couples-relationship app that turns check-ins into small, intentional rituals: one daily question, curated conversation packs, private answers, mutual reveal, gentle reminders, shared games, and date planning — with **real E2EE** and **calmer UX**. +It is not a social network, therapy replacement, or productivity tracker. There are no public feeds, +likes, followers, or infinite-scroll loops. The product loop is small on purpose: -Not a social network. Not therapy. Not a productivity tracker. **No public feeds, no likes, no followers, no infinite scroll.** - -The core loop is simple: *answer honestly → choose what to reveal → keep a record of the conversations that mattered.* - -## TL;DR - -| What | Why it matters | -| --- | --- | -| 🔐 **Real E2EE** | Answers **and chat messages** are encrypted on-device. Server only sees ciphertext. Couple-owned keys via Tink (Android). | -| 💑 **One subscription per couple** | No double-billing partners. Premium unlocks for both — server-verified. | -| 🚫 **No engagement traps** | No infinite scroll. No likes. No follower counts. One daily question is the loop. | -| 🌗 **Decoupled theme + art** | In-app light/dark controls art; system theme isn't required to match. | -| 📱 **Native on both platforms** | Kotlin/Compose on Android, SwiftUI on iOS — same Firebase backend, same data model. | -| 🧪 **QA you can run** | `scripts/theme-scan.sh` (Pass C) and `scripts/wiring-scan.sh` (Pass N) catch the silent-dead-feature and theme-hardcoding classes before merge. | - ---- +```text +answer honestly -> reveal together -> keep the conversation going +``` ## Screenshots Fresh Android dark-mode captures from the current emulator build. -Daily question stages on Home: + + + + + + + + + + + +
ReadyYour turnReveal ready
Home screen with the daily question ready to answer in dark modeHome screen after the partner answered and it is your turn in dark modeHome screen with the daily question reveal ready in dark mode
-| Question Ready | Your Turn | Reveal Ready | -| :---: | :---: | :---: | -| Home screen with the daily question ready to answer in dark mode | Home screen after the partner answered and it is your turn in dark mode | Home screen with the daily question reveal ready in dark mode | + + + + + + + + + + + + + +
PlayThis or ThatTodayChallenge
Play hub in dark modeThis or That in dark modeDaily question in dark modeConnection challenge in dark mode
-Other core screens: +## What makes it different -| Play | This or That | Today | Challenge | -| :---: | :---: | :---: | :---: | -| Play hub in dark mode | This or That in dark mode | Daily question in dark mode | Connection challenge in dark mode | + + + + + + + + + + + +
Private first
Partners answer independently before seeing each other's response.
Mutual reveal
The app is built around intentional sharing, not performative posting.
Couple-owned trust
Android encrypts answers, chat, history, and media on-device with couple-owned keys.
One subscription per couple
Premium unlocks for both partners through server-verified entitlements.
Curated prompts
Question packs are written and reviewed, not generated at answer time.
Calm by design
No feeds, likes, followers, public profiles, or pressure mechanics.
---- +## Product surface -## Why Closer exists - -Subscription apps for couples have a trust problem — confusing trial wording, hard-to-cancel flows, partners getting double-billed. Couples products have a *different* trust problem: partners are asked to be vulnerable in the same space where everything else (social, productivity, dating) wants their engagement, their data, and their attention. - -Closer treats both the same way: **clear, straightforward, and built on honesty.** - -- 🪞 **Private first, reveal second.** Each partner answers independently. Both decide what to share. -- 🧠 **Curated, not generated.** 6,000+ hand-written prompts across 22 categories — no AI confabulation in the core loop. -- 💸 **One sub, not two.** Subscription unlocks for both partners. Server-verified. No silent trial conversions. -- 🔒 **Encryption that earns the word.** Tink AEAD with couple-owned keys. Answers, messages, and history — server never sees plaintext. Recover with your phrase *or* your partner. -- 🌙 **Quiet hours, server-side.** Partner pushes respect the *recipient's* in-app window — not just foreground detection. - ---- - -## What Closer does - -| Feature | Free | Premium | +| Area | Free | Premium | | --- | --- | --- | -| Daily question (text / scale / multi / this-or-that) | ✅ | ✅ | -| 6,000+ prompts · 22 question packs | ✅ (free) + 🎟️ (premium tiers) | ✅ (incl. premium-only packs) | -| Private answers + mutual-reveal flow | ✅ | ✅ | -| Spin the wheel — category-randomized questions | ✅ | ✅ | -| Recent answer history (last 30 days) | ✅ | ✅ | -| Full answer history (search, filter, export) | — | ✅ | -| Saved spin-wheel sessions | — | ✅ | -| Memory Lane (locked time capsules) | — | ✅ | -| Desire Sync (preferences alignment exercise) | — | ✅ | -| Select Connection Challenges (multi-day programs) | ✅ (free) + 🎟️ (premium tiers) | ✅ | -| Push reminders with quiet-hour support | ✅ | ✅ | -| Account deletion + data export | ✅ | ✅ | +| Daily question | Included | Included | +| Private answers + mutual reveal | Included | Included | +| Curated question packs | Free + mixed access | Full access | +| Spin the Wheel | Included | Included | +| This or That / How Well Do You Know Me | Included | Included | +| Recent answer history | Included | Included | +| Memory Lane capsules | Limited | Full access | +| Desire Sync | Limited | Full access | +| Connection Challenges | Free + mixed access | Full access | +| Date ideas, matches, and bucket list | Included | Premium ideas gated | +| Push reminders + quiet hours | Included | Included | +| Account deletion | Included | Included | -One subscription unlocks premium for **both** partners — `couples/{coupleId}/entitlements` is per-couple, not per-user. - ---- +Data export is not currently offered. The in-app privacy copy intentionally says so until a real export +flow exists. ## Platform status -| Platform | Stack | Status | Notes | -| --- | --- | --- | --- | -| **Android** | Kotlin · Jetpack Compose · Material 3 · Hilt · Room · DataStore | 🟢 **Reference implementation** | Feature-complete MVP, light/dark theme polished | -| **iOS** | SwiftUI · MVVM · async/await · Firebase iOS SDK | 🟡 **Scaffold landed on `dev`** | Full screen parity; pairing from iOS is blocked until E2EE keys are wired (Android-only today) | -| **Backend** | Firebase Auth · Firestore · Cloud Functions · FCM · App Check | 🟢 **Shared source of truth** | 17 callable/trigger/scheduled/webhook functions | -| **Billing** | RevenueCat · Google Play Billing · StoreKit | 🟢 **Server-verified** | Webhook → Firestore entitlements → `CouplePremiumChecker` | +| Platform | Status | Notes | +| --- | --- | --- | +| Android | Reference implementation | Kotlin, Jetpack Compose, Material 3, Hilt, Room, DataStore, Firebase, Tink | +| iOS | Scaffold in progress | SwiftUI screen parity exists; pairing is blocked until E2EE interop is complete | +| Backend | Shared source of truth | Firebase Auth, Firestore, Cloud Functions, FCM, App Check | +| Billing | Server verified | RevenueCat webhook writes Firestore entitlements observed by the app | -> 📐 iOS scaffold has **all 49 screens** mapped to SwiftUI views, Firebase + RevenueCat integration, and full screen parity on the `dev` branch. CryptoKit-based E2EE parity (interop with Android's Tink key material) is the only blocker for end-to-end iOS pairing. +Android is the product source of truth today. iOS has the app shell, Firebase/RevenueCat integration, +and SwiftUI screens, but end-to-end pairing waits on CryptoKit/Tink interoperability. ---- - -## Architecture at a glance +## Architecture ```text - ┌──────────────────────────┐ ┌──────────────────────────┐ - │ Android (Kotlin/Compose)│ │ iOS (SwiftUI) │ - │ • Hilt DI · Room · DSK │ │ • MVVM · AppState · SPM │ - │ • Tink AEAD + Argon2id │ │ • CryptoKit (follow-up) │ - └────────────┬─────────────┘ └────────────┬─────────────┘ - │ │ - └──────────────┬─────────────────────┘ - │ - ┌──────────▼──────────┐ - │ Firebase │ - │ • Auth (email / │ - │ Google via │ - │ Credential Mgr) │ - │ • Firestore │ - │ (couple-scoped) │ - │ • Cloud Functions │ - │ • FCM │ - │ • App Check │ - │ (Play Integrity)│ - └──────────┬──────────┘ - │ - ┌──────────▼──────────┐ - │ RevenueCat │ - │ → webhook │ - │ → Firestore │ - │ entitlements │ - └─────────────────────┘ +Android (Kotlin/Compose) iOS (SwiftUI) +Hilt · Room · DataStore MVVM · AppState · SPM +Tink AEAD · Argon2id CryptoKit interop in progress + \ / + \ / + v v + Firebase + RevenueCat + Auth · Firestore · Functions · FCM + App Check · server-verified billing ``` -- **Couple-scoped data.** Firestore rules deny cross-couple reads/writes. Users only see their own + their partner's surface. -- **Server-mediated pairing.** 6-character invite codes are enumerable; invite reads/writes are server-side only. -- **Server-verified billing.** RevenueCat → Cloud Function webhook → Firestore `users/{uid}/entitlements/premium` → `CouplePremiumChecker` observes both partners' premium state. -- **Local-first questions.** Prompts ship in the app so daily questions load instantly; only assignment and sync hit the network. +Core rules: ---- +- Couple-scoped Firestore data. Users only read/write their own couple surface. +- Server-mediated pairing. Invite lifecycle runs through Cloud Functions. +- Local-first question content. Prompts ship with the app; assignment/sync uses Firebase. +- Server-verified premium. Clients observe entitlements; they do not self-grant access. +- No anonymous auth. Accounts use email/password or Google sign-in. -## Tech stack +## Security and privacy -### Android +- Android encrypts answer content, chat messages, media, capsules, and backed-up conversation history + on-device before sync. +- The server stores ciphertext for private content and never receives plaintext answers. +- Recovery phrase wrapping uses Argon2id-derived keys. +- Partner-assisted restore lets a trusted partner help restore the couple key to a new device. +- Quiet hours are enforced server-side before push delivery. +- Account deletion exists; data export does not yet. -| Layer | Stack | -| --- | --- | -| Language | Kotlin 2.x | -| UI | Jetpack Compose · Material 3 · Navigation Compose | -| Architecture | Clean architecture — `core/` · `data/` · `domain/` · `ui/` | -| State | ViewModel · Kotlin Coroutines · Kotlin Flow | -| DI | Hilt | -| Local data | Room · DataStore Preferences · bundled SQLite seed | -| Crypto | Google Tink AEAD + Argon2id (Bouncy Castle KDF) | -| Auth | Firebase Auth · **Credential Manager** for Google Sign-In | -| Build | min 26 · target 35 · compile 35 · Java 17 · KSP | +The canonical technical reference is +[docs/Engineering_Reference_Manual.md](docs/Engineering_Reference_Manual.md). -### iOS +## Local development -| Layer | Stack | -| --- | --- | -| Language | Swift 6.0 | -| UI | SwiftUI · NavigationStack · TabView | -| Architecture | MVVM · `AppState` ObservableObject · `EnvironmentObject` | -| Concurrency | async/await · Swift 6 strict concurrency | -| Dependency management | Swift Package Manager · XcodeGen (`project.yml`) | -| Auth | Firebase Auth · Google Sign-In SDK | -| Crypto | Apple CryptoKit (E2EE parity — follow-up) | -| SDK | iOS 17.0+ | +
+Prerequisites -### Backend (shared) +- Android Studio, Android SDK, and JDK 17 +- Node 20 for Firebase Functions tooling +- Firebase project with Auth, Firestore, Cloud Messaging, Crashlytics, Analytics, and App Check +- `app/google-services.json` +- RevenueCat project and Android API key -| Layer | Stack | -| --- | --- | -| Auth | Firebase Authentication — **email/password · Google** (Android uses Credential Manager) | -| Database | Cloud Firestore (couple-scoped rules) | -| Server logic | Firebase Cloud Functions (TypeScript) | -| Push | Firebase Cloud Messaging (FCM) | -| Security | Firebase App Check · Play Integrity (Android) · DeviceCheck (iOS, planned) | -| Billing | RevenueCat (server-verified entitlements) | -| Analytics | Firebase Analytics · Crashlytics | +For iOS work: -> 🚫 **No anonymous auth.** There is no anonymous sign-in or account-linking flow in either platform. Accounts are email/password or Google. +- macOS, Xcode 16, XcodeGen, and iOS 17+ +- `iphone/Closer/GoogleService-Info.plist` +- RevenueCat iOS API key ---- +
-## Repository layout - -```text -. -├── app/ # Native Android app (Kotlin) -│ └── src/main/java/app/closer -│ ├── core/ # Firebase, analytics, billing, nav, notifications, security -│ ├── data/ # Room, Firestore data sources, repositories, seed parsing -│ ├── domain/ # Models + repository contracts -│ └── ui/ # Compose screens + feature ViewModels -├── iphone/ # Native iOS app (SwiftUI) -│ ├── ARCHITECTURE_AUDIT.md # iOS port blueprint (49 screens, schema, models) -│ ├── project.yml # XcodeGen project spec -│ ├── Package.swift # SPM dependency manifest -│ └── Closer/ -│ ├── Models/ # Firestore + domain codable types -│ ├── Core/ # Auth · Billing · Notifications -│ ├── Services/ # FirestoreService (callable wrappers) -│ ├── Theme/ # CloserTheme (colors, typography, spacing) -│ ├── Components/ # Shared SwiftUI components -│ ├── Navigation/ # Root ContentView + TabView routing -│ ├── Onboarding/ # Onboarding · login · signup · profile creation -│ ├── Pairing/ # Invite code · partner confirm · recovery -│ ├── Home/ # Home dashboard · streak · partner mirror -│ ├── Questions/ # Daily Q · answer reveal · history · packs -│ ├── Play/ # Play hub + games (ToT, HowWell, DesireSync, …) -│ ├── Wheel/ # Spin wheel -│ ├── Dates/ # Date swipe · matches · builder · bucket list -│ └── Settings/ # Settings · paywall · help · data export -├── functions/ # Firebase Cloud Functions (TypeScript) -│ └── src/ -│ ├── auth/ # Auth + invite lifecycle -│ ├── billing/ # RevenueCat webhook + entitlement sync -│ ├── couples/ # Pairing, leave, daily-question triggers -│ ├── questions/ # onAnswerWritten · onMessageWritten · threads -│ ├── games/ # onGameSessionUpdate · onGamePartFinished -│ ├── notifications/ # quiet-hours helper · reminders -│ └── server/ # Internal Express webhook service -├── scripts/ # Automated QA / lint scanners -│ ├── theme-scan.sh # Pass C: light/dark theme-hardcoding scanner -│ └── wiring-scan.sh # Pass N: dead-feature / orphan-wiring scanner -├── server/ # Optional Express webhook/health service -├── seed/ # Question-pack JSON + local DB generation -├── docs/ # QA notes · release prep · roadmap · screenshots -└── firestore.rules # Firestore security rules (single source of truth) -``` - -> 🧪 `scripts/theme-scan.sh` and `scripts/wiring-scan.sh` are run before every QA pass. They statically catch the two costliest QA classes: hardcoded theme colors and silent dead features. - ---- - -## Getting started - -### Prerequisites - -- **Android:** Android Studio · Android SDK · JDK 17 -- **iOS:** Xcode 16 · macOS · [XcodeGen](https://github.com/yonaskolb/XcodeGen) (`brew install xcodegen`) -- **Firebase:** Project with Auth · Firestore · Cloud Messaging · Crashlytics · Analytics · App Check -- **Android config:** `app/google-services.json` -- **iOS config:** `iphone/Closer/GoogleService-Info.plist` -- **Billing:** RevenueCat project with Android + iOS API keys -- **Node 20** for Firebase Functions tooling - -### Local config +
+Android ```bash -# Android cp local.properties.example local.properties -# iOS -cp iphone/Closer/GoogleService-Info.plist.example iphone/Closer/GoogleService-Info.plist ``` ```properties @@ -272,19 +186,16 @@ RC_API_KEY_ANDROID=your_revenuecat_android_key RC_API_KEY_IOS=your_revenuecat_ios_key ``` -### Android - ```bash ./gradlew :app:assembleDebug ./gradlew :app:installDebug +./gradlew :app:testDebugUnitTest ``` -```bash -./gradlew :app:compileDebugKotlin # fast verification -./gradlew :app:testDebugUnitTest # 205 unit tests -``` +
-### iOS +
+iOS ```bash cd iphone @@ -294,82 +205,86 @@ xed Closer.xcodeproj ```bash xcodebuild -project iphone/Closer.xcodeproj \ - -scheme Closer \ - -destination 'platform=iOS Simulator,name=iPhone 15' \ - build + -scheme Closer \ + -destination 'platform=iOS Simulator,name=iPhone 15' \ + build ``` -### Firebase Functions +
+ +
+Firebase Functions ```bash cd functions npm install npm run build +npm test +``` + +```bash npm run serve ``` -```bash -npm test # 24 functions tests +
+ +## QA and release + +The repo keeps repeatable QA checks close to the code: + +| Check | Purpose | +| --- | --- | +| [qa/README.md](qa/README.md) | Ava + Ben emulator fixtures and cold-start smoke notes | +| `scripts/theme-scan.sh` | Finds hardcoded theme/color regressions | +| `scripts/wiring-scan.sh` | Finds orphaned routes, dead actions, and silent wiring breaks | +| [docs/qa/](docs/qa/) | Manual QA playbooks and private MVP checklist | +| [docs/release/](docs/release/) | Internal testing, store assets, and release prep | + +Current Ava + Ben emulator sessions are documented in [qa/README.md](qa/README.md). Do not clear those +app sessions unless you are intentionally rebuilding the fixture. + +## Repository map + +```text +app/ Android app +iphone/ iOS app +functions/ Firebase Cloud Functions +server/ Optional Express service +seed/ Question content and seed tooling +scripts/ Static QA scanners +qa/ Re-runnable emulator QA notes and scripts +docs/ Architecture, release, screenshots, and QA docs +firestore.rules Firestore rules source of truth ``` -### Optional server - -```bash -cd server -npm install -npm run dev -``` - ---- - -## Security & privacy - -- 🔐 **E2EE content.** Tink AEAD with couple-owned keys. Answers, **chat messages** (text *and* images), locked capsules, and conversation history are encrypted on-device — the server only ever sees ciphertext. Messages live under `couples/{coupleId}/conversations/…`; images upload to Storage as opaque encrypted bytes, and even the inbox preview line is encrypted. -- 💾 **Encrypted conversation backup.** Chat and thread history is backed up as couple-key ciphertext — cheap incremental appends plus periodic full snapshots — so history can return to a new device. The backup is never readable server-side. -- 🧂 **Key wrapping.** Argon2id KDF over the recovery phrase; keys wrapped client-side. -- 🪪 **Recovery phrase.** Server-blind; wiped from the inviter on acceptance. One of **two** recovery paths. -- 🤝 **Partner-assisted restore.** Lost or wiped your device? Your partner can restore your full history with **no recovery phrase**: your new device publishes a fresh public key, you read a 6-digit code aloud on a separate channel, and they wrap the couple key to that key (ECIES `keybox:v1:`). The server only relays the sealed blob — never the key itself. Your own devices get a *"was this you?"* security alert whenever a restore is requested or completes. -- 🚧 **Firestore rules.** Couple-scoped; deny-by-default; field allowlists on `users/{uid}` updates; shape-restricted couple create. -- 🛡️ **App Check.** Play Integrity (Android), DeviceCheck (iOS, planned) — blocks abusive backend access. -- 🌙 **Quiet hours, server-side.** Suppression is enforced where the push is **sent**, not where it might be foregrounded. Client cannot bypass by being backgrounded. -- 💸 **Server-verified billing.** Cloud Function writes `users/{uid}/entitlements/premium` from the RevenueCat webhook. Client cannot self-grant. - -> Full architecture reference: [`docs/Engineering_Reference_Manual.md`](docs/Engineering_Reference_Manual.md) — the canonical source of truth for the security model, data model, and Cloud Functions wiring. - ---- - ## Roadmap In progress: -- 🔐 **iOS E2EE parity** (CryptoKit interop with Android's Tink key material) — *unblocks pairing from iOS*. -- 🧪 **On-device / instrumented test coverage** (Compose UI / Espresso smoke) — currently `app/src/test` only. -- 🎨 **Activity `uiMode` sync** to in-app theme (C-DARKART-002) — the dark-variant `-night` PNGs only render in the right combination today; architectural fix in `MainActivity`. -- 🛒 **Real release config** — version, legal/support URLs, RevenueCat offerings verified end-to-end on internal testing. +- iOS E2EE interoperability with Android's Tink key material +- More on-device/instrumented smoke coverage +- Activity `uiMode` sync for in-app theme and dark artwork +- Internal-testing release configuration, legal/support URLs, and RevenueCat offering validation -Out of scope (for now): +Out of scope for now: -- AI-assisted question suggestions -- Native group/relationship types beyond dyadic couples -- Wearable (Wear OS / watchOS) companions +- AI-generated core questions +- Group relationship spaces beyond dyadic couples +- Wear OS / watchOS companions - Live video or voice sessions -See `Future.md` for the full backlog. - ---- - -## Project history & docs +## Project docs | Doc | Purpose | | --- | --- | -| [`docs/Engineering_Reference_Manual.md`](docs/Engineering_Reference_Manual.md) | Architecture, security model, data model, known landmines | -| [`docs/release/`](docs/release/) | Release prep + store assets | -| [`docs/qa/`](docs/qa/) | QA playbook + private-MVP checklist | -| `Future.md` | Backlog + roadmap | -| `HISTORY.md` | Changelog + release notes | -| `PROJECT.md` | Scope, feature matrix, architectural decisions | - ---- +| [docs/Engineering_Reference_Manual.md](docs/Engineering_Reference_Manual.md) | Architecture, security model, data model, and known landmines | +| [qa/README.md](qa/README.md) | Emulator fixtures and autonomous QA smokes | +| [docs/brand/](docs/brand/) | Visual identity, asset system, generated art | +| [docs/release/](docs/release/) | Release prep and store assets | +| [docs/qa/](docs/qa/) | Manual QA checklist | +| [Future.md](Future.md) | Backlog and roadmap | +| [HISTORY.md](HISTORY.md) | Changelog and release notes | +| [PROJECT.md](PROJECT.md) | Scope, feature matrix, architectural decisions | ## License