# Closer — Private MVP QA Checklist

> Manual testing checklist for the internal MVP build. Covers every top-level flow in the app and notes known gaps discovered during the 2025-06 QA pass.
> Last updated: 2026-06-21 — reflects pairing security hardening, notification preferences, appearance screen, email invite removal, real PartnerHomeScreen, real SubscriptionScreen, ProGuard log stripping, strings.xml, and support URL fix. All 17 release blocker items resolved or pending deploy.

Product goal under test: **private, mutual-reveal relationship questions with real encryption and calmer UX**.

---

## Environment & Setup

- [ ] Clean install on test device (API 26+).
- [ ] Test device/emulator has network connectivity.
- [ ] Check non-network build: `./gradlew :app:compileDebugKotlin` passes.
- [ ] Verify `RC_API_KEY` in `local.properties` (or a placeholder) so billing build does not fail.
- [ ] Google-services JSON is present for the selected build variant.

---

## 1. Onboarding & First Launch

### 1.1 Splash / App launch
- [ ] App launches to `MainActivity` without crash.
- [ ] `AppNavigation` starts at `AppRoute.ONBOARDING` on fresh install.
- [ ] Theme applies (`CloserTheme`) and status bar / navigation bar insets handled correctly.
- [ ] App name in launcher is "Closer".

### 1.2 Onboarding screen (`OnboardingScreen`)
- [ ] Value proposition visible and not placeholder text.
- [ ] CTA navigates to `CREATE_PROFILE` or `LOGIN` as expected.
- [ ] Secondary sign-in link works.
- [ ] No dead-end: user can always reach either create profile or login.

### 1.3 Create profile (`CreateProfileScreen`)
- [ ] Name input accepts text and reflects in UI.
- [ ] Validation prevents empty submission.
- [ ] Profile creation succeeds and navigates forward (home or pairing flow).
- [ ] Loading / error states handled.

### 1.4 Login / Sign up / Forgot password
- [ ] `LoginScreen`: email/password fields, sign-in call, error shown on failure, success navigates to `HOME`.
- [ ] `SignUpScreen`: account creation, weak-password validation, navigates to profile creation.
- [ ] `ForgotPasswordScreen`: email reset flow, success/error messaging.
- [ ] Back navigation from auth screens returns to onboarding.

### 1.5 Known onboarding gaps (from code scan)

- ~~`AccountScreen` shows "Local profile" and disables "Sign in or create account" and "Export your data" rows.~~ ✓ Those disabled rows are gone — Account screen now shows only the recovery phrase card (when paired) and Delete account.
- Home header text changes based on `partnerName` presence; confirm both states render.

---

## 2. Pairing

### 2.1 Create invite (`CreateInviteScreen`)
- [ ] Generates 6-character code.
- [ ] Code displays with chunked formatting (e.g., "ABC – 123").
- [ ] Copy button copies raw code to clipboard and shows snackbar.
- [ ] Share button opens system share sheet with correct message.
- [ ] "Partner already has a code? Accept instead" navigates to `ACCEPT_INVITE`.
- [ ] Code expiry note shown ("expires in 24 hours").

### 2.2 Accept invite (`AcceptInviteScreen`)
- [ ] 6-character code entry field with visual chunking and uppercase auto-capitalization.
- [ ] Continue enabled only when code length == 6.
- [ ] Invalid/expired code shows error.
- [ ] Valid code navigates to `INVITE_CONFIRM/{inviteCode}`.
- [ ] "Need to create an invite instead?" link works.
- [ ] Keyboard Done triggers lookup.

### 2.3 Invite confirm (`InviteConfirmScreen`)
- [ ] Inviter name loaded and displayed.
- [ ] Pairing confirmation button shows loading spinner.
- [ ] Success navigates to home.
- [ ] "Not right — enter a different code" returns to accept screen.
- [ ] Error surfaced via snackbar.
- [ ] Recovery phrase is **not** prompted during confirm — it is returned automatically from the server and stored silently.
- [ ] No recovery phrase input field visible on this screen.

### 2.4 Share invite (was: Email invite — removed)

> `EmailInviteScreen` and the `EMAIL_INVITE` route have been deleted. Sharing is now handled entirely via the system share sheet in `CreateInviteScreen` / `CreateInviteView`. There is no separate email flow or backend email sending.

- [ ] **Android**: "Share" button on `CreateInviteScreen` opens system share sheet with invite message.
- [ ] **iOS**: "Share" button on `CreateInviteView` opens `UIActivityViewController` with the code.
- [ ] Share sheet offers SMS, email, Signal, WhatsApp, etc. — no Closer-specific channel required.
- [ ] Settings → Connection "Invite Partner" row navigates to `CreateInviteView` (not email view).

### 2.5 Relationship settings
- [ ] `SettingsScreen` partner card opens `RELATIONSHIP_SETTINGS` when paired.
- [ ] Leave couple confirmation dialog shown.
- [ ] Leave action calls repository and navigates to `CREATE_INVITE` on success.
- [ ] Error shown on failure.

---

## 3. Home

### 3.1 Home screen (`HomeScreen`)
- [ ] Loads without crash; loading, error, and success states all tested.
- [ ] Header shows correct subtitle for paired vs. unpaired user.
- [ ] Streak pill appears when `streakCount > 0`.
- [ ] Primary action card responds to tap.
- [ ] Secondary action feed renders and navigates.
- [ ] "More doorways" grid shows up to 2 categories; tap navigates to category.
- [ ] "All packs" button navigates to `QUESTION_PACKS`.
- [ ] Pull/refresh or retry on error works.

### 3.2 Partner home (`PartnerHomeScreen`)

- [x] ~~Renders `PlaceholderScreen` with correct copy.~~ Screen is now a real dashboard (blocker #5 fixed).
- [ ] Partner identity card shows avatar initial, name, and streak count.
- [ ] Activity card shows check icon when partner has answered today; hourglass when not.
- [ ] "Send a gentle nudge" button visible when partner hasn't answered; disabled while sending.
- [ ] Nudge success/error surfaces via snackbar.
- [ ] "View today's question" button navigates to `DAILY_QUESTION`.
- [ ] Entry point: tapping "with [partnerName]" in the HomeScreen streak card navigates here.
- [ ] Loading and error states render without crash.

### 3.3 Moment cue / special dates section

- [x] ~~`SpecialDatesSection` previews render with hardcoded names.~~ `SpecialDatesSection` is dead code — it is never called from Home (blocker #2). No hardcoded names render.
- [ ] Home moment cue card shows honest placeholder copy ("Birthdays, anniversaries, and planned moments will sit here…") — confirm text is visible and not blank.

---

## 4. Daily Question

### 4.1 Daily question screen (`DailyQuestionScreen`)
- [ ] Question text loads.
- [ ] Answer input accepts text and respects private/public toggle.
- [ ] Save/submit works, shows feedback.
- [ ] Navigates to answer reveal or answer history appropriately.
- [ ] Discussion section handles un-revealed state gracefully.

### 4.2 Answer input components
- [ ] `QuestionAnswerInput`: empty-state handling, formatting, IME actions.
- [ ] `QuestionHelpExpandable`: expand/collapse works.
- [ ] `QuestionDiscussionThread`: disabled state copy is clear.
- [ ] `QuestionNavigationBar`: previous/next navigation and disabled states.

### 4.3 Question thread (`QuestionThreadScreen`)
- [ ] Loads question and optional `prevId`/`nextId` args.
- [ ] Previous / next navigation works when ids provided.
- [ ] Both partner answers visible after reveal.
- [ ] Comments can be added (if unlocked).
- [ ] Back button returns to prior screen.

---

## 5. Question Packs & Categories

### 5.1 Question pack library (`QuestionPackLibraryScreen`)
- [ ] Loading, error, empty states tested.
- [ ] Categories/packs list scrolls.
- [ ] Free and premium locks indicated.
- [ ] Tap on premium pack navigates to `PAYWALL`.
- [ ] Tap on free/mixed pack navigates to `QUESTION_CATEGORY/{categoryId}`.

### 5.2 Question category (`QuestionCategoryScreen`)
- [ ] Loads questions for `categoryId`.
- [ ] Format and depth filters apply.
- [ ] Question cards navigate to `QUESTION_THREAD`.
- [ ] Locked premium packs route to paywall.
- [ ] Loading/error handled.

### 5.3 Question composer (`QuestionComposerScreen`)
- [ ] User can create a custom question.
- [ ] Category/depth/format options available.
- [ ] Save creates question and returns.
- [ ] Validation prevents empty submission.

---

## 6. Answers & Reveal

### 6.1 Answer reveal (`AnswerRevealScreen`)
- [ ] Loads answer for `questionId`.
- [ ] Private answer shown only to author until reveal.
- [ ] Reveal action updates state and reveals both answers.
- [ ] Loading, error, empty states tested.
- [ ] Navigation to discussion or history works.

### 6.2 Answer history (`AnswerHistoryScreen`)
- [ ] List of answered/revealed questions loads.
- [ ] Empty state shown when no answers.
- [ ] Remove action (if available) works and updates list.
- [ ] Tap navigates to thread/reveal.

---

## 7. Spin Wheel

### 7.1 Category picker (`CategoryPickerScreen`)
- [ ] Categories load; loading, error, empty states tested.
- [ ] Locked categories show lock icon and "Premium" pill.
- [ ] Locked tap routes to `PAYWALL`.
- [ ] Free tap routes to `SPIN_WHEEL/{categoryId}`.

### 7.2 Spin wheel (`SpinWheelScreen`)
- [ ] Category name displayed.
- [ ] Spin action triggers selection of `SpinWheelViewModel.SESSION_SIZE` questions.
- [ ] Visual spinning animation runs.
- [ ] Error surfaced.
- [ ] Ready state shows count and enables "Start session".
- [ ] "Spin again" re-rolls selection.

### 7.3 Wheel session (`WheelSessionScreen`)
- [ ] Loads session via `sessionId`.
- [ ] Progress indicator updates.
- [ ] Current question displays centered.
- [ ] Next / skip / end session buttons work.
- [ ] Empty session state shown when no active session.
- [ ] Last question button label reads "Finish".
- [ ] Navigates to `WHEEL_COMPLETE/{sessionId}` on finish.

### 7.4 Wheel complete (`WheelCompleteScreen`)
- [ ] Shows category name and answered/total count.
- [ ] Saves session to repository on init.
- [ ] "Back home" navigates to `HOME`.
- [ ] "Spin again" navigates to `CATEGORY_PICKER`.
- [ ] Handles 0-question edge case gracefully.

### 7.5 Wheel history (`WheelHistoryScreen`)
- [ ] Premium lock card shown for non-premium users.
- [ ] Premium users see list of completed sessions.
- [ ] Empty state with "Spin now" action.
- [ ] Error/retry works.
- [ ] Date formatting uses default locale.

---

## 8. Dates

### 8.1 Date match (`DateMatchScreen`)
- [ ] Swipe cards render.
- [ ] Match state shows correctly.
- [ ] Premium gating tested.
- [ ] Error/loading handled.

### 8.2 Date matches (`DateMatchesScreen`)
- [ ] List of matched date ideas loads.
- [ ] Empty state shown.
- [ ] Tap navigates to detail/builder.

### 8.3 Date builder (`DateBuilderScreen`)

- [x] ~~Date and time fields show "TODO" placeholders.~~ `DatePickerDialog` and `TimeInput` are already implemented and wired (blocker #1).
- [ ] Fields render: date picker, time input, budget, duration chips.
- [ ] Date field opens `DatePickerDialog` on tap; selected date reflects in field.
- [ ] Time field accepts input correctly.
- [ ] Budget input accepts digits only.
- [ ] Duration chips selectable.
- [ ] Save button calls view model.
- [ ] Back navigation works.

### 8.4 Bucket list (`BucketListScreen`)
- [ ] Items load; empty state shown.
- [ ] Category filter chips scroll and filter list.
- [ ] Add item FAB opens dialog.
- [ ] Add dialog: title required, description optional, category selectable.
- [ ] Toggle complete updates card style.
- [ ] Delete removes item.
- [ ] Back navigation works.

---

## 9. Settings

### 9.1 Settings home (`SettingsScreen`)
- [ ] Profile card shows display name / email or local profile note.
- [ ] Partner card shows paired state and partner name, or invite prompt.
- [ ] Tapping profile card opens `ACCOUNT`.
- [ ] Tapping partner card opens `RELATIONSHIP_SETTINGS` (paired) or `CREATE_INVITE` (unpaired).
- [ ] **Appearance** row (palette icon) present and opens `APPEARANCE` screen.
- [ ] Notifications, Subscription, Privacy & Terms rows open correct screens.
- [x] No "Email Invite" or "Invite by Email" row present anywhere in settings — `EmailInviteScreen` deleted (blocker #3).
- [ ] Legal links open external URLs.
- [ ] Delete account row opens `DELETE_ACCOUNT`.
- [ ] Sign out button works and shows loading state.

### 9.2 Account (`AccountScreen`)

- [ ] "Local profile" card shown for signed-out state.
- [x] ~~Disabled rows ("Auth coming soon", "Export coming soon") visually greyed out.~~ Those rows no longer exist (blocker #6).
- [ ] **Recovery phrase card** shown when paired (key icon, monospaced phrase, copy button).
- [ ] Copy button copies phrase to clipboard and shows "Recovery phrase copied" snackbar.
- [ ] Recovery phrase card absent when not paired (no phrase to show).
- [ ] Delete account row navigates to `DELETE_ACCOUNT`.
- [ ] Back navigation works.

### 9.3 Notifications (`NotificationSettingsScreen`)

- [ ] **Five toggles** present: Daily question, Partner answered, New chat message, Shared rhythm reminder, Quiet hours.
- [ ] All toggles reflect persisted `AppStorage` / DataStore state on load.
- [ ] Toggling "Partner answered" or "New chat message" writes `notifPartnerAnswered` / `notifChatMessage` to Firestore user doc (verify in Firebase console).
- [ ] Daily reminder and streak reminder toggles persist locally only (no Firestore write required).
- [ ] Quiet hours description accurate (10 PM – 8 AM).
- [ ] **iOS parity**: same two server-synced toggles (Partner answered, New chat message) present in iOS Notification Settings and sync to Firestore.

### 9.4 Appearance (`AppearanceScreen`)

- [ ] Three theme options: Device default, Light, Dark.
- [ ] Selecting a theme applies immediately across the app — no restart required.
- [ ] Selection persists after closing and reopening the app.
- [ ] Back navigation returns to Settings.
- [ ] "Device default" follows system dark/light mode correctly.

### 9.5 Privacy (`PrivacyScreen`)

- [ ] External links open in browser.
- [ ] No browser available case handled via `ExternalLinks.openUrl` Toast fallback.
- [ ] Back navigation works.
- [x] Support URL resolves correctly — updated to `https://closer.app/support` (blocker #7).

### 9.6 Subscription (`SubscriptionScreen`)

- [x] ~~Placeholder screen routing to paywall.~~ Real `SubscriptionViewModel` built (blocker #4).
- [ ] **Free state**: star icon, "Unlock Premium" header, benefits list, Upgrade button navigates to `PAYWALL`, Restore link.
- [ ] **Premium state**: "You're Premium" card, renewal date (when available), benefits list, "Manage subscription" opens Play Store, Restore link.
- [ ] Restore purchases shows snackbar on success; error surfaces via snackbar.
- [ ] Reads entitlement reactively — upgrading mid-session reflects immediately without restart.
- [ ] **Note**: Requires real RevenueCat API key + active product to fully test both states.

### 9.7 Relationship settings / Delete account
- [ ] See pairing section for leave-couple flow.
- [ ] Delete account confirmation dialog requires acknowledgment checkbox.
- [ ] Delete action calls `AuthRepository.deleteAccount()` and navigates to onboarding.
- [ ] Loading and error states shown.

---

## 10. Paywall

### 10.1 Paywall screen (`PaywallScreen`)
- [ ] Loads RevenueCat packages; loading/error handled.
- [ ] Benefits list accurate.
- [ ] Selecting plan updates selection state.
- [ ] Purchase button enabled only when plan selected.
- [ ] Purchase call uses current activity context.
- [ ] Restore purchases button works.
- [ ] Legal links open externally.
- [ ] Thank-you overlay shown on success; dismiss returns to previous screen.
- [ ] **Note**: Requires real RevenueCat API key and product configuration to fully test.

---

## 11. Notifications

### 11.1 FCM service (`AppMessagingService`)
- [ ] App receives push when in foreground and background.
- [ ] Quiet hours suppression works (10 PM – 8 AM).
- [ ] Notification channel created on Android O+.
- [ ] Tap action navigates to relevant screen.

### 11.2 Token registrar
- [ ] Token updates sent to backend/user document.
- [ ] Handles sign-out / re-sign-in correctly.

### 11.3 Partner answered notification (`onAnswerWritten` CF)

- [ ] Push received on partner's device when you submit a daily question answer.
- [ ] No push received when "Partner answered" toggle is off in Notification Settings.
- [ ] Push not sent to the answering user themselves — only the partner.

### 11.4 Chat message notification (`onMessageWritten` CF)

- [ ] Push received on partner's device when a message is sent in a question thread.
- [ ] No push received when "New chat message" toggle is off in Notification Settings.
- [ ] Push not sent to the message author themselves.
- [ ] **Deployment note**: `onMessageWritten` is a new function — must run `firebase deploy --only functions` before this test is possible.

---

## 12. General UX / Edge Cases

### 12.1 Navigation
- [ ] Bottom nav shows on top-level routes only.
- [ ] System back from top-level routes does not exit unexpectedly (navigate back within app).
- [ ] Shell top bar appears on `shellBackRoutes` and uses route title.
- [ ] Deep links / invite-confirm route handled: `invite_confirm/{inviteCode}`.

### 12.2 Input / Accessibility
- [ ] Text fields have proper keyboard options (email, ASCII, capitalization).
- [ ] Labels / placeholders readable.
- [ ] Touch targets >= 48 dp for all clickable surfaces.
- [ ] Max lines and ellipses used to prevent overflow clipping.

### 12.3 Theming
- [ ] Light theme renders consistently (current primary theme).
- [ ] Dark theme colors defined but not wired to system dark mode (`isSystemInDarkTheme()` not used). Verify this is intentional for MVP.
- [ ] No stale Next.js / React Native references in resources or manifests.

### 12.4 Data / Offline
- [ ] App does not crash when offline (graceful error cards).
- [ ] Local answer repository persists data.
- [ ] Firebase calls fail cleanly and retry available where exposed.

---

## 13. Release Blockers Logged from This QA Pass

These findings came from the static review and should be fixed before public or store release. Do **not** block internal MVP on these unless explicitly required.

**Summary: 16 of 17 items closed. 1 item pending a deploy command (#17).**

| # | Area | Issue | Severity | Status |
| --- | --- | --- | --- | --- |
| 1 | Date builder | Date/time picker TODOs — fields are not interactive | High | **Not an issue** — `DatePickerDialog` and `TimeInput` are already implemented and wired |
| 2 | Special dates | Hardcoded names/dates in `SpecialDatesSection` | High | **Not an issue** — `SpecialDatesSection` is dead code, never rendered; home shows honest placeholder copy |
| 3 | Email invite | Placeholder screen with hardcoded `ABC123` code | Medium | **Fixed** — screen deleted; share sheet is the flow |
| 4 | Subscription | Placeholder screen, not real management | Medium | **Fixed** — real `SubscriptionViewModel` reads `EntitlementChecker.isPremium()` + `CustomerInfo`; free state shows benefits + Upgrade button; premium state shows renewal date + "Manage subscription" → Play Store + Restore |
| 5 | Partner home | Placeholder screen only | Medium | **Fixed** — real `PartnerHomeViewModel` + screen with partner identity card, today activity status, send-nudge button, and navigation wired from HomeScreen streak card tap |
| 6 | Settings | Account rows disabled ("Auth coming soon", "Export coming soon") | Medium | **Not an issue** — `AccountScreen` no longer has those rows; only Delete account row present |
| 7 | External links | Support URL points to `couplesconnect.app/support` | Low | **Fixed** — updated to `https://closer.app/support` in `ExternalLinks.kt` |
| 8 | Strings | 100+ hardcoded display strings; should move to `strings.xml` for localization | Low | **Partial** — `strings.xml` built with 90+ entries; settings cluster (Appearance, Notifications, Account, Privacy) updated to use `stringResource()`; remaining screens (Home, Pairing, Daily Question, etc.) still hardcoded |
| 9 | Logging | `android.util.Log.e` used in `QuestionJsonParser` — confirm no verbose/debug logs in release builds | Low | **Fixed** — no sensitive data in any log; `Log.d`/`Log.v` stripped in release via `-assumenosideeffects` ProGuard rule |
| 10 | Pairing security | Direct Firestore fallback in `createInvite` bypassed server-side rules | High | **Fixed** — fallback removed; CF is the only path |
| 11 | Pairing security | No rate limiting on `acceptInviteCallable` — 6-char codes are enumerable | High | **Fixed** — 10 attempts/hour per user; `invite_attempts` TTL via Firestore field override |
| 12 | Pairing security | `recoveryPhrase` left in plaintext on invite doc post-accept | High | **Fixed** — wiped via `FieldValue.delete()` in accept batch |
| 13 | Pairing security | `encryptionVersion` hardcoded to 2 even when no E2EE fields present (iOS) | High | **Fixed** — derived from key presence: 2 if E2EE, 0 if plaintext |
| 14 | Notifications | No push sent when partner answers or sends a chat message | Medium | **Fixed** — `onAnswerWritten` gated on prefs; `onMessageWritten` CF added |
| 15 | Notifications | Notification prefs were local-only; server CFs had no way to respect them | Medium | **Fixed** — prefs synced to Firestore user doc on toggle (Android + iOS) |
| 16 | Functions | `invite_attempts` subcollection had no cleanup — would grow forever | Medium | **Fixed** — `expiresAt` TTL field added; `firestore.indexes.json` configures auto-delete |
| 17 | Deploy | `onMessageWritten` CF + `invite_attempts` TTL not yet live — run `firebase deploy --only functions && firebase deploy --only firestore:indexes` | Medium | **Pending** — code complete; deploy step requires Firebase CLI access from the project owner |

---

## Sign-off

| Tester | Date | Build | Result |
|--------|------|-------|--------|
|        |      |       |        |
