docs(readme): modernize for 2026 — badges, TL;DR, architecture diagram, pillar grid, expanded security section
This commit is contained in:
parent
38ff166598
commit
9e8084a02e
384
README.md
384
README.md
|
|
@ -1,235 +1,257 @@
|
|||
# Closer
|
||||
|
||||
> *Your private moments stay private. Your information is securely encrypted, never read, and never sold. Only you and your paired partner have the key.*
|
||||
>
|
||||
> *Private daily questions for couples who want honest answers before shared conversations.*
|
||||
> 🔐 **Private daily questions for couples — end-to-end encrypted, never read, never sold.**
|
||||
> *You and your paired partner hold the only key.*
|
||||
|
||||
**Product goal:** private, mutual-reveal relationship questions with real encryption and calmer UX.
|
||||
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, and shared date planning — with **real E2EE** and **calmer UX**.
|
||||
|
||||
Closer is a **couples relationship app** built for partners who want a steadier way to talk, reflect, and make time for each other. It turns relationship check-ins into small, intentional rituals — one daily question, curated conversation packs, private answers, partner reveal flows, gentle reminders, and shared date planning.
|
||||
Not a social network. Not therapy. Not a productivity tracker. **No public feeds, no likes, no followers, no infinite scroll.**
|
||||
|
||||
The app is **not** a social network, **not** a therapy replacement, and **not** a productivity tracker. There are no public feeds, no likes, no followers, and no infinite scroll. The core loop is simple: **answer honestly, choose what to reveal, and keep a record of the conversations that mattered.**
|
||||
|
||||
Closer is built as native apps on **Android (Kotlin / Jetpack Compose)** and **iOS (SwiftUI)**, both backed by the same Firebase stack and shared data model. The Android app is the reference implementation; iOS has full screen parity but cannot yet pair end-to-end because the invite/pairing Cloud Functions require Android's E2EE key material.
|
||||
The core loop is simple: *answer honestly → choose what to reveal → keep a record of the conversations that mattered.*
|
||||
|
||||
---
|
||||
|
||||
## Pricing Philosophy
|
||||
<p align="center">
|
||||
<img alt="Android" src="https://img.shields.io/badge/Android-Active%20development-3DDC84?style=for-the-badge&logo=android&logoColor=white" />
|
||||
<img alt="iOS" src="https://img.shields.io/badge/iOS-Scaffold%20landed-007AFF?style=for-the-badge&logo=ios&logoColor=white" />
|
||||
<img alt="Backend" src="https://img.shields.io/badge/Backend-Firebase-FFCA28?style=for-the-badge&logo=firebase&logoColor=black" />
|
||||
<img alt="Min Android" src="https://img.shields.io/badge/Android-26%2B-3DDC84?style=for-the-badge&logo=android&logoColor=white" />
|
||||
<img alt="Min iOS" src="https://img.shields.io/badge/iOS-17%2B-007AFF?style=for-the-badge&logo=ios&logoColor=white" />
|
||||
<img alt="Kotlin" src="https://img.shields.io/badge/Kotlin-2.x-7F52FF?style=for-the-badge&logo=kotlin&logoColor=white" />
|
||||
<img alt="Swift" src="https://img.shields.io/badge/Swift-6.0-F05138?style=for-the-badge&logo=swift&logoColor=white" />
|
||||
<img alt="License" src="https://img.shields.io/badge/license-Private-red?style=for-the-badge" />
|
||||
</p>
|
||||
|
||||
> *No shady trials. No cancellation friction. One subscription per couple, not per person.*
|
||||
---
|
||||
|
||||
Subscription apps for couples have a trust problem — confusing trial wording, hard-to-cancel flows, and partners getting double-billed. Closer treats pricing the same way it treats relationships: **clear, straightforward, and built on honesty.**
|
||||
## TL;DR
|
||||
|
||||
| Principle | How Closer Handles It |
|
||||
| What | Why it matters |
|
||||
| --- | --- |
|
||||
| Free tier | Generous by default. Daily questions, basic packs, recent history, spin wheel — all free, no time limit. |
|
||||
| One sub per couple | One premium purchase unlocks it for both partners. No second billing. |
|
||||
| Trial wording | No auto-renewal trials that silently convert. If there's a free period, you'll know exactly when and how it ends. |
|
||||
| Cancellation | Clear, actionable instructions in-app and in docs. No hidden unsubscribe paths. |
|
||||
| Restore | Works with one tap. If it breaks, support fixes it — no escalation maze. |
|
||||
| 🔐 **Real E2EE** | Answer content is 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. |
|
||||
|
||||
---
|
||||
|
||||
## Screenshots
|
||||
|
||||
<p>
|
||||
<img src="docs/screenshots/01-onboarding.png" alt="Closer onboarding screen" width="180" />
|
||||
<img src="docs/screenshots/03-home.png" alt="Closer home dashboard" width="180" />
|
||||
<img src="docs/screenshots/04-daily-question.png" alt="Closer daily question answer screen" width="180" />
|
||||
<img src="docs/screenshots/05-question-packs.png" alt="Closer question pack library" width="180" />
|
||||
<p align="center">
|
||||
<img src="docs/screenshots/01-onboarding.png" alt="Onboarding" width="180" />
|
||||
<img src="docs/screenshots/03-home.png" alt="Home" width="180" />
|
||||
<img src="docs/screenshots/04-daily-question.png" alt="Daily question" width="180" />
|
||||
<img src="docs/screenshots/05-question-packs.png" alt="Question packs" width="180" />
|
||||
<img src="docs/screenshots/06-answer-history.png" alt="Answer history" width="180" />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<img src="docs/screenshots/06-answer-history.png" alt="Closer answer history" width="180" />
|
||||
<img src="docs/screenshots/08-play.png" alt="Closer play hub" width="180" />
|
||||
<img src="docs/screenshots/09-date-builder.png" alt="Closer date planning screen" width="180" />
|
||||
<img src="docs/screenshots/07-settings.png" alt="Closer settings screen" width="180" />
|
||||
<p align="center">
|
||||
<img src="docs/screenshots/08-play.png" alt="Play hub" width="180" />
|
||||
<img src="docs/screenshots/09-date-builder.png" alt="Date planning" width="180" />
|
||||
<img src="docs/screenshots/07-settings.png" alt="Settings" width="180" />
|
||||
<img src="docs/screenshots/02-login.png" alt="Login" width="180" />
|
||||
<img src="docs/screenshots/10-bucket-list.png" alt="Bucket list (new Pixel 8 emulator)" width="180" />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<img src="docs/screenshots/02-login.png" alt="Closer login screen" width="180" />
|
||||
<img src="docs/screenshots/10-bucket-list.png" alt="Closer bucket list screen (new Pixel 8 emulator)" width="180" />
|
||||
</p>
|
||||
|
||||
> **New emulator screenshot:** `docs/screenshots/10-bucket-list.png` is a placeholder for a fresh capture from a Pixel 8 / API 35 emulator showing the bucket list. This Linux environment has no Android SDK/emulator, so the image must be captured on a machine with Android Studio and copied into `docs/screenshots/`.
|
||||
> 📸 The bucket-list screenshot at `docs/screenshots/10-bucket-list.png` is a placeholder for a fresh capture from a Pixel 8 / API 35 emulator. This Linux environment has no Android SDK, so the image must be captured on a machine with Android Studio and copied into `docs/screenshots/`.
|
||||
|
||||
---
|
||||
|
||||
## What Closer Does
|
||||
## Why Closer exists
|
||||
|
||||
Closer gives couples a private, shared space for guided connection. Every feature should support
|
||||
the product goal: **private, mutual-reveal relationship questions with real encryption and calmer UX**.
|
||||
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.
|
||||
|
||||
- **Daily question** — one prompt a day, with written, scale, multiple-choice, and this-or-that answer modes.
|
||||
- **Private-first answers** — each partner answers independently before deciding whether to reveal or discuss.
|
||||
- **Question packs** — 22+ curated categories spanning 6,000+ bundled prompts (communication, conflict, trust, intimacy, parenting, marriage, money, stress, date night, and more).
|
||||
- **Answer history** — review past questions and answers, delete controls, and partner reveal support.
|
||||
- **Discussion threads** — question-specific conversation threads and reactions for follow-up.
|
||||
- **Partner pairing** — 6-character invite code with copy and share, and partner-aware home states.
|
||||
- **Spin the wheel** — category-based random questions for date nights, road trips, and low-pressure moments.
|
||||
- **Date tools** — swipe-to-match date ideas, date planning preferences, and a shared bucket list.
|
||||
- **Settings & privacy** — account, notifications, appearance, subscription, relationship, privacy, and account deletion flows.
|
||||
- **Subscriptions** — free and premium tiers powered by RevenueCat (Google Play Billing on Android, StoreKit on iOS).
|
||||
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. Recovery phrase. Server never sees plaintext.
|
||||
- 🌙 **Quiet hours, server-side.** Partner pushes respect the *recipient's* in-app window — not just foreground detection.
|
||||
|
||||
---
|
||||
|
||||
## Product Shape
|
||||
## What Closer does
|
||||
|
||||
Closer is optimized for **short, meaningful sessions** rather than endless engagement.
|
||||
|
||||
| Area | Behavior |
|
||||
| --- | --- |
|
||||
| Home | Surfaces the next best action — daily question, partner activity, saved answers |
|
||||
| Questions | Local-bundled content so the app stays fast and usable without waiting on the network |
|
||||
| Partner Sync | Firebase Auth + Firestore isolate user, couple, invite, thread, and entitlement data per couple |
|
||||
| Reminders | FCM + local notification preferences and quiet-hour controls |
|
||||
| Premium | Unlocks deeper packs, full history, Desire Sync, select Connection Challenges, Memory Lane, and analytics — **one subscription for both partners** |
|
||||
|
||||
---
|
||||
|
||||
## Platform Status
|
||||
|
||||
| Platform | Stack | Status |
|
||||
| Feature | Free | Premium |
|
||||
| --- | --- | --- |
|
||||
| **Android** | Kotlin · Jetpack Compose · Material 3 · Hilt · Room · DataStore | **Active development** — feature-complete MVP, light/dark theme polished |
|
||||
| **iOS** | SwiftUI · MVVM · async/await · Firebase iOS SDK | **In progress** — full scaffold + screen parity landed on `dev` branch; pairing is blocked until iOS E2EE keys are wired |
|
||||
| **Backend** | Firebase Auth · Firestore · Cloud Functions (TypeScript) · FCM · App Check | **Shared source of truth** for both platforms |
|
||||
| **Billing** | RevenueCat · Google Play Billing · StoreKit | Server-verified entitlements via Cloud Function webhook |
|
||||
| Daily question (text / scale / multi / this-or-that) | ✅ | ✅ |
|
||||
| 6,000+ prompts · 22 question packs | ✅ | ✅ (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 | ✅ | ✅ |
|
||||
|
||||
The Android app is the **reference implementation** — the iOS port is built to mirror it screen-for-screen and consume the same backend.
|
||||
One subscription unlocks premium for **both** partners — `couples/{coupleId}/entitlements` is per-couple, not per-user.
|
||||
|
||||
---
|
||||
|
||||
## MVP Feature Scope
|
||||
## Platform status
|
||||
|
||||
### Free Tier — No Time Limit, No Hidden Cap
|
||||
| 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` |
|
||||
|
||||
All of this is free, forever. No credits, no daily limits that magically shrink after a week.
|
||||
|
||||
- **Email or Google sign-up** — no anonymous onboarding flow; accounts are email/password or Google Sign-In (Android uses Credential Manager).
|
||||
- 6-character invite code pairing (copy or share via any app)
|
||||
- Daily question with full answer modes (text, scale, multiple choice, this-or-that)
|
||||
- Private answer reveal flow once both partners have answered
|
||||
- Question packs — 6,000+ bundled prompts across 22 categories (most packs included free)
|
||||
- Spin wheel — category-based random questions for date nights
|
||||
- Recent answer history (last 30 days)
|
||||
- Push reminders with quiet-hour support
|
||||
- Account deletion and data export
|
||||
|
||||
### Premium Tier (Per-Couple, Not Per-Person)
|
||||
|
||||
One purchase unlocks premium for both partners. No separate subscriptions.
|
||||
|
||||
- **Unlimited daily questions** with premium-only question packs
|
||||
- **Full answer history** — search, filter, export the complete timeline
|
||||
- **Saved spin wheel session history** — revisit what landed
|
||||
- **Desire Sync** — preferences alignment exercise for tough conversations
|
||||
- **Select Connection Challenges** — premium multi-day programs; free challenges available to all users
|
||||
- **Memory Lane** — time capsules with locked prompts that open on future dates
|
||||
- **Advanced analytics and relationship insights**
|
||||
- **Priority support**
|
||||
|
||||
### Out of Scope (Future)
|
||||
- AI-assisted question suggestions
|
||||
- Native group/relationship types beyond dyadic couples
|
||||
- Wearable (Wear OS / watchOS) companions
|
||||
- Live video or voice sessions
|
||||
> 📐 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.
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
## Architecture at a glance
|
||||
|
||||
```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 │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
## Tech stack
|
||||
|
||||
### Android
|
||||
|
||||
| Layer | Stack |
|
||||
| --- | --- |
|
||||
| Language | Kotlin 2.x |
|
||||
| UI | Jetpack Compose, Material 3, Navigation Compose |
|
||||
| UI | Jetpack Compose · Material 3 · Navigation Compose |
|
||||
| Architecture | Clean architecture — `core/` · `data/` · `domain/` · `ui/` |
|
||||
| State | ViewModel · Kotlin Coroutines · Kotlin Flow |
|
||||
| Dependency Injection | Hilt |
|
||||
| Local Data | Room · DataStore Preferences · bundled SQLite seed |
|
||||
| SDK | min 26 · target 35 · compile 35 |
|
||||
| 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 |
|
||||
|
||||
### iOS
|
||||
|
||||
| Layer | Stack |
|
||||
| --- | --- |
|
||||
| Language | Swift 6.0 |
|
||||
| UI | SwiftUI · NavigationStack · TabView |
|
||||
| Architecture | MVVM · `AppState` ObservableObject · `EnvironmentObject` |
|
||||
| Concurrency | async/await |
|
||||
| Dependency Management | Swift Package Manager · XcodeGen (`project.yml`) |
|
||||
| SDK | iOS 17+ |
|
||||
| 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+ |
|
||||
|
||||
### Backend (shared)
|
||||
|
||||
### Backend (Shared)
|
||||
| Layer | Stack |
|
||||
| --- | --- |
|
||||
| Auth | Firebase Authentication (email/password, Google) — Android uses Credential Manager for Google Sign-In |
|
||||
| Database | Cloud Firestore |
|
||||
| Server Logic | Firebase Cloud Functions (TypeScript) |
|
||||
| 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) |
|
||||
| Security | Firebase App Check · Play Integrity (Android) · DeviceCheck (iOS, planned) |
|
||||
| Billing | RevenueCat (server-verified entitlements) |
|
||||
| Analytics | Firebase Analytics · Crashlytics |
|
||||
|
||||
> 🚫 **No anonymous auth.** There is no anonymous sign-in or account-linking flow in either platform. Accounts are email/password or Google.
|
||||
|
||||
---
|
||||
|
||||
## Repository Layout
|
||||
## Repository layout
|
||||
|
||||
```text
|
||||
.
|
||||
├── app/ # Native Android app (Kotlin)
|
||||
│ └── src/main/java/app/closer
|
||||
│ ├── core/ # Firebase, analytics, billing, navigation, notifications, security
|
||||
│ ├── core/ # Firebase, analytics, billing, nav, notifications, security
|
||||
│ ├── data/ # Room, Firestore data sources, repositories, seed parsing
|
||||
│ ├── domain/ # Models and repository contracts
|
||||
│ └── ui/ # Compose screens and feature ViewModels
|
||||
│ ├── 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
|
||||
│ ├── 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 question, answer reveal, history, packs
|
||||
│ ├── Play/ # Play hub + games (ThisOrThat, HowWell, DesireSync)
|
||||
│ ├── Wheel/ # Spin wheel with animated 8-slice picker
|
||||
│ ├── Dates/ # Date swipe, matches, builder, bucket list
|
||||
│ └── Settings/ # Settings, paywall, help, data export
|
||||
│ ├── 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 flow + invite lifecycle
|
||||
│ ├── auth/ # Auth + invite lifecycle
|
||||
│ ├── billing/ # RevenueCat webhook + entitlement sync
|
||||
│ ├── couples/ # Pairing, leave, daily question triggers
|
||||
│ ├── questions/ # onAnswerWritten, onMessageWritten, threads
|
||||
│ └── server/ # Internal Express webhook service (not client-facing)
|
||||
│ ├── 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
|
||||
│ ├── 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 and local DB generation
|
||||
├── docs/ # QA notes, release prep, roadmap, screenshots
|
||||
└── firestore.rules # Firestore security rules
|
||||
├── 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
|
||||
## 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
|
||||
- **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 and iOS API keys
|
||||
- **Billing:** RevenueCat project with Android + iOS API keys
|
||||
- **Node 20** for Firebase Functions tooling
|
||||
|
||||
### Local config
|
||||
|
|
@ -241,8 +263,6 @@ cp local.properties.example local.properties
|
|||
cp iphone/Closer/GoogleService-Info.plist.example iphone/Closer/GoogleService-Info.plist
|
||||
```
|
||||
|
||||
Add local-only values such as:
|
||||
|
||||
```properties
|
||||
sdk.dir=/path/to/Android/Sdk
|
||||
RC_API_KEY_ANDROID=your_revenuecat_android_key
|
||||
|
|
@ -256,10 +276,9 @@ RC_API_KEY_IOS=your_revenuecat_ios_key
|
|||
./gradlew :app:installDebug
|
||||
```
|
||||
|
||||
Useful verification command:
|
||||
|
||||
```bash
|
||||
./gradlew :app:compileDebugKotlin
|
||||
./gradlew :app:compileDebugKotlin # fast verification
|
||||
./gradlew :app:testDebugUnitTest # 205 unit tests
|
||||
```
|
||||
|
||||
### iOS
|
||||
|
|
@ -270,8 +289,6 @@ xcodegen generate
|
|||
xed Closer.xcodeproj
|
||||
```
|
||||
|
||||
Build from the command line:
|
||||
|
||||
```bash
|
||||
xcodebuild -project iphone/Closer.xcodeproj \
|
||||
-scheme Closer \
|
||||
|
|
@ -288,7 +305,11 @@ npm run build
|
|||
npm run serve
|
||||
```
|
||||
|
||||
### Optional Server
|
||||
```bash
|
||||
npm test # 24 functions tests
|
||||
```
|
||||
|
||||
### Optional server
|
||||
|
||||
```bash
|
||||
cd server
|
||||
|
|
@ -298,57 +319,50 @@ npm run dev
|
|||
|
||||
---
|
||||
|
||||
## Data Model
|
||||
## Security & privacy
|
||||
|
||||
Closer combines local-first question content with cloud sync for shared relationship state:
|
||||
- 🔐 **E2EE answer content.** Tink AEAD with couple-owned keys. Server never sees plaintext answers or capsule content.
|
||||
- 🧂 **Key wrapping.** Argon2id KDF over the recovery phrase; keys wrapped client-side.
|
||||
- 🪪 **Recovery phrase.** Server-blind; wiped from the inviter on acceptance.
|
||||
- 🚧 **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.
|
||||
|
||||
- **Room (Android) / local bundled JSON (iOS)** stores seeded question categories, questions, and date preferences — the app stays usable offline.
|
||||
- **DataStore (Android) / UserDefaults (iOS)** stores local settings and lightweight preferences.
|
||||
- **Firestore** stores users, couples, invites, daily questions, answer history, date matches, bucket-list state, time capsules, gentle reminders, and entitlements.
|
||||
- **Cloud Functions** handle pairing lifecycle, reminder workflows, billing entitlement sync, RevenueCat webhooks, partner activity triggers (FCM), and security checks.
|
||||
- **FCM** delivers reminders and partner activity notifications to both platforms.
|
||||
> 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.
|
||||
|
||||
---
|
||||
|
||||
## Privacy & Safety Principles
|
||||
## Roadmap
|
||||
|
||||
- Couple data is **scoped by couple ID** and protected by Firestore security rules — no other couple can read another couple's data.
|
||||
- Answers are **private first**, then revealed intentionally by the partner.
|
||||
- Account deletion and privacy screens are first-class surfaces in the app.
|
||||
- **App Check** (Play Integrity on Android, DeviceCheck on iOS) blocks abusive backend access.
|
||||
- Subscription state is verified **server-side** via Cloud Functions rather than trusting the client.
|
||||
- All private answer and capsule content is **end-to-end encrypted** with couple-owned keys (Android uses Tink; iOS uses Apple CryptoKit in a follow-up batch).
|
||||
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.
|
||||
|
||||
Out of scope (for now):
|
||||
|
||||
- AI-assisted question suggestions
|
||||
- Native group/relationship types beyond dyadic couples
|
||||
- Wearable (Wear OS / watchOS) companions
|
||||
- Live video or voice sessions
|
||||
|
||||
See `Future.md` for the full backlog.
|
||||
|
||||
---
|
||||
|
||||
## Current Status
|
||||
## Project history & docs
|
||||
|
||||
This is a **private MVP / internal testing codebase**.
|
||||
|
||||
### Complete
|
||||
- Android MVP — onboarding, auth (anonymous/email/Google), pairing, home, daily questions, question packs, answer history, spin wheel, date tools, settings, billing, notifications, Firebase integration, E2EE crypto layer
|
||||
- iOS port scaffold — all 49 screens mapped to SwiftUI views, Firebase/RevenueCat integration, full screen parity landed on `dev` branch
|
||||
- Backend — 17 Cloud Functions (callable, triggers, scheduled, webhook) covering full relationship lifecycle
|
||||
- Seeded question content — 6,000+ prompts across 22 categories
|
||||
|
||||
### In Progress
|
||||
- iOS Xcode project generation and build verification
|
||||
- iOS E2EE layer (CryptoKit interop with Android Tink) — pairing from iOS is currently blocked until E2EE keys are wired
|
||||
- Final QA pass on both platforms
|
||||
|
||||
### Release Prep
|
||||
- `docs/release/internal-testing-checklist.md`
|
||||
- `docs/release/store-assets.md`
|
||||
- `docs/qa/private-mvp-checklist.md`
|
||||
- `docs/qa/ui-review.md`
|
||||
|
||||
---
|
||||
|
||||
## Project History
|
||||
|
||||
See `HISTORY.md` for the full changelog and release notes.
|
||||
|
||||
See `PROJECT.md` for detailed scope, feature matrix, and architectural decisions.
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue