# 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, and email invite removal. --- ## 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. These need real wiring before public release. - 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`) - [ ] Renders `PlaceholderScreen` with correct copy. - [ ] Actions navigate to invite flow or home. - [ ] **Gap**: screen is a placeholder; not a functional partner dashboard yet. ### 3.3 Moment cue / special dates section - [ ] `SpecialDatesSection` previews render. - [ ] Home moment cue card text not placeholder. - [ ] Hardcoded names ("Jessica", "Mark") and dates in `SpecialDatesSection` must be replaced with real data before public release. --- ## 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`) - [ ] Fields render: date, time, budget, duration. - [ ] **Gap**: date and time fields show "TODO: Date picker dialog" / "TODO: Time picker dialog"; they are not interactive yet. - [ ] 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. - [ ] No "Email Invite" or "Invite by Email" row present anywhere in settings. - [ ] 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. - [ ] Disabled rows visually greyed out. - [ ] **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.4 Privacy (`PrivacyScreen`) - [ ] External links open in browser. - [ ] No browser available case handled via `ExternalLinks.openUrl` Toast fallback. - [ ] Back navigation works. - [ ] Support URL resolves correctly — now `https://closer.app/support`. ### 9.5 Subscription (`SubscriptionScreen`) - [ ] Renders placeholder with paywall and settings actions. - [ ] **Gap**: subscription management is placeholder; needs real UI before release. ### 9.6 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. | # | 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 | Open | | 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 | iOS deploy | `onMessageWritten` CF not yet deployed — iOS chat notifications not active until `firebase deploy --only functions` is run | Medium | Open — deploy required | --- ## Sign-off | Tester | Date | Build | Result | |--------|------|-------|--------| | | | | |