fix(release): date/time pickers, debug logging guards, RC_API_KEY comment, ExternalLinks TODOs (gap fixes)

This commit is contained in:
null 2026-06-17 02:00:39 -05:00
parent 160fb38c4f
commit ede7c70ea7
7 changed files with 733 additions and 237 deletions

View File

@ -20,6 +20,8 @@ android {
versionName = "0.1.0" versionName = "0.1.0"
// RevenueCat API key is supplied via local.properties (RC_API_KEY) and never committed. // RevenueCat API key is supplied via local.properties (RC_API_KEY) and never committed.
// TODO: Replace the PLACEHOLDER_RC_API_KEY fallback with a real key in local.properties before release.
// The app will not process purchases correctly while the placeholder is active.
buildConfigField( buildConfigField(
"String", "String",
"RC_API_KEY", "RC_API_KEY",

View File

@ -6,9 +6,13 @@ import android.net.Uri
import android.widget.Toast import android.widget.Toast
object ExternalLinks { object ExternalLinks {
// TODO: Update placeholder URL before production.
const val PRIVACY_POLICY = "https://closer.app/privacy" const val PRIVACY_POLICY = "https://closer.app/privacy"
// TODO: Update placeholder URL before production.
const val TERMS_OF_SERVICE = "https://closer.app/terms" const val TERMS_OF_SERVICE = "https://closer.app/terms"
// TODO: Update placeholder URL before production.
const val SUBSCRIPTION_TERMS = "https://closer.app/subscription-terms" const val SUBSCRIPTION_TERMS = "https://closer.app/subscription-terms"
// TODO: Update placeholder URL before production.
const val SUPPORT = "https://couplesconnect.app/support" const val SUPPORT = "https://couplesconnect.app/support"
fun openUrl(context: Context, url: String) { fun openUrl(context: Context, url: String) {

View File

@ -1,5 +1,7 @@
package app.closer.data.questions package app.closer.data.questions
import android.util.Log
import app.closer.BuildConfig
import app.closer.domain.model.* import app.closer.domain.model.*
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
@ -26,6 +28,8 @@ import org.json.JSONObject
*/ */
object QuestionJsonParser { object QuestionJsonParser {
private const val TAG = "QuestionJsonParser"
data class ParsedQuestionBundle( data class ParsedQuestionBundle(
val category: QuestionCategory, val category: QuestionCategory,
val questions: List<Question> val questions: List<Question>
@ -35,7 +39,9 @@ object QuestionJsonParser {
val jsonContent = try { val jsonContent = try {
java.io.File(jsonFilePath).readText() java.io.File(jsonFilePath).readText()
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("QuestionJsonParser", "Failed to read JSON file: ${e.message}") if (BuildConfig.DEBUG) {
Log.e(TAG, "Failed to read JSON file: ${e.message}")
}
return null return null
} }
@ -60,7 +66,9 @@ object QuestionJsonParser {
ParsedQuestionBundle(category, questions) ParsedQuestionBundle(category, questions)
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("QuestionJsonParser", "Failed to parse JSON: ${e.message}") if (BuildConfig.DEBUG) {
Log.e(TAG, "Failed to parse JSON: ${e.message}")
}
null null
} }
} }

View File

@ -21,12 +21,19 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.DatePicker
import androidx.compose.material3.CardDefaults import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.DisplayMode
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TimeInput
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -146,6 +153,7 @@ private fun Header(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun InputCard( private fun InputCard(
state: DateBuilderUiState, state: DateBuilderUiState,
@ -154,6 +162,9 @@ private fun InputCard(
onBudgetChange: (Int) -> Unit, onBudgetChange: (Int) -> Unit,
onDurationChange: (String) -> Unit onDurationChange: (String) -> Unit
) { ) {
var showDatePicker by remember { mutableStateOf(false) }
var showTimePicker by remember { mutableStateOf(false) }
Surface( Surface(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp), shape = RoundedCornerShape(24.dp),
@ -169,9 +180,9 @@ private fun InputCard(
DateTimeField( DateTimeField(
label = "When?", label = "When?",
value = state.scheduledDate.takeIf { it > 0 }?.let { value = state.scheduledDate.takeIf { it > 0 }?.let {
java.text.SimpleDateFormat("MMMM dd, yyyy", java.util.Locale.getDefault()).format(java.util.Date(it)) java.text.SimpleDateFormat("EEE, MMM dd", java.util.Locale.getDefault()).format(java.util.Date(it))
} ?: "Select a date", } ?: "Select a date",
onClick = { /* TODO: Date picker dialog */ }, onClick = { showDatePicker = true },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
@ -179,7 +190,7 @@ private fun InputCard(
DateTimeField( DateTimeField(
label = "What time?", label = "What time?",
value = state.scheduledTime.ifEmpty { "Select a time" }, value = state.scheduledTime.ifEmpty { "Select a time" },
onClick = { /* TODO: Time picker dialog */ }, onClick = { showTimePicker = true },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
@ -197,6 +208,71 @@ private fun InputCard(
) )
} }
} }
if (showDatePicker) {
val datePickerState = rememberDatePickerState(
initialSelectedDateMillis = state.scheduledDate.takeIf { it > 0 },
initialDisplayMode = DisplayMode.Picker
)
DatePickerDialog(
onDismissRequest = { showDatePicker = false },
confirmButton = {
TextButton(
onClick = {
datePickerState.selectedDateMillis?.let { millis ->
onDateChange(millis)
}
showDatePicker = false
}
) {
Text("OK")
}
},
dismissButton = {
TextButton(onClick = { showDatePicker = false }) {
Text("Cancel")
}
}
) {
DatePicker(
state = datePickerState,
showModeToggle = false
)
}
}
if (showTimePicker) {
val timePickerState = rememberTimePickerState()
AlertDialog(
onDismissRequest = { showTimePicker = false },
confirmButton = {
TextButton(
onClick = {
val formattedTime = formatTime(timePickerState.hour, timePickerState.minute)
onTimeChange(formattedTime)
showTimePicker = false
}
) {
Text("OK")
}
},
dismissButton = {
TextButton(onClick = { showTimePicker = false }) {
Text("Cancel")
}
},
title = { Text("Select time") },
text = { TimeInput(state = timePickerState) }
)
}
}
private fun formatTime(hour: Int, minute: Int): String {
val calendar = java.util.Calendar.getInstance().apply {
set(java.util.Calendar.HOUR_OF_DAY, hour)
set(java.util.Calendar.MINUTE, minute)
}
return java.text.SimpleDateFormat("h:mm a", java.util.Locale.getDefault()).format(calendar.time)
} }
@Composable @Composable

View File

@ -1,139 +1,366 @@
# Private MVP QA Checklist # Closer — Private MVP QA Checklist
Manual testing checklist for the Closer app private MVP. > 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.
## Onboarding ---
- [ ] App launches to onboarding screen on fresh install ## Environment & Setup
- [ ] Onboarding screens advance correctly (swipe/tap)
- [ ] "Get Started" leads to sign-up screen
- [ ] Sign-up: email + password creates account
- [ ] Sign-up: validation errors show (empty fields, weak password, existing email)
- [ ] Login: existing user can sign in
- [ ] Login: "Forgot password" navigates to reset screen
- [ ] Create Profile: name + avatar saved to Firestore
- [ ] Profile creation completes and navigates to home
## Home Screen - [ ] 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.
- [ ] Home screen loads after onboarding ---
- [ ] Daily question card is visible
- [ ] Question pack sections display correctly
- [ ] Navigation between tabs (Home, Dates, Wheel, Settings)
- [ ] Pull-to-refresh reloads content
- [ ] Partner home view shows when coupled
## Questions ## 1. Onboarding & First Launch
### Daily Question ### 1.1 Splash / App launch
- [ ] Today's question loads - [ ] App launches to `MainActivity` without crash.
- [ ] User can answer (select option or type response) - [ ] `AppNavigation` starts at `AppRoute.ONBOARDING` on fresh install.
- [ ] Answer is saved to Firestore - [ ] Theme applies (`CloserTheme`) and status bar / navigation bar insets handled correctly.
- [ ] "Partner answered" notification flow works (if coupled) - [ ] App name in launcher is "Closer".
### Question Packs ### 1.2 Onboarding screen (`OnboardingScreen`)
- [ ] Pack library screen lists available packs - [ ] Value proposition visible and not placeholder text.
- [ ] Premium packs show lock icon (if not subscribed) - [ ] CTA navigates to `CREATE_PROFILE` or `LOGIN` as expected.
- [ ] Free packs can be opened - [ ] Secondary sign-in link works.
- [ ] Category picker shows categories - [ ] No dead-end: user can always reach either create profile or login.
- [ ] Selecting a category loads questions from that category
### Question Detail & Thread ### 1.3 Create profile (`CreateProfileScreen`)
- [ ] Question detail screen loads with full text - [ ] Name input accepts text and reflects in UI.
- [ ] Answer reveal shows both partner answers (if both answered) - [ ] Validation prevents empty submission.
- [ ] Discussion thread loads messages - [ ] Profile creation succeeds and navigates forward (home or pairing flow).
- [ ] User can send a message in thread - [ ] Loading / error states handled.
- [ ] Answer history screen shows past answers
## Date Match ### 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.
- [ ] Date match screen shows swipe UI ### 1.5 Known onboarding gaps (from code scan)
- [ ] Swipe right = "love", swipe left = "skip" - `AccountScreen` shows "Local profile" and disables "Sign in or create account" and "Export your data" rows. These need real wiring before public release.
- [ ] "Maybe" button works - Home header text changes based on `partnerName` presence; confirm both states render.
- [ ] Match reveal screen appears when both partners like same idea
- [ ] Date matches screen shows all mutual matches
- [ ] Empty state shows when no matches
## Date Builder ---
- [ ] Date builder screen loads with suggestion fields ## 2. Pairing
- [ ] Date and time picker buttons present (currently TODO placeholders)
- [ ] Save date plan works (if implemented)
## Bucket List ### 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").
- [ ] Bucket list screen shows saved date ideas ### 2.2 Accept invite (`AcceptInviteScreen`)
- [ ] Adding items works - [ ] 6-character code entry field with visual chunking and uppercase auto-capitalization.
- [ ] Completing/removing items works - [ ] 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.
## Wheel ### 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.
- [ ] Spin wheel animates and selects a category ### 2.4 Email invite (`EmailInviteScreen`)
- [ ] Category picker shows available categories - [ ] This is currently a `PlaceholderScreen`. Verify it renders and that primary/secondary actions navigate correctly.
- [ ] Wheel complete screen shows result - [ ] **Risk**: placeholder actions reference a hardcoded invite code `ABC123`; replace before public release.
- [ ] Wheel history screen shows past spins
## Partner Pairing ### 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.
### Create Invite ---
- [ ] Invite code generates and displays
- [ ] Copy button copies code to clipboard
- [ ] Share button opens Android share sheet (Intent.ACTION_SEND)
- [ ] "Accept instead" link navigates to AcceptInvite
### Accept Invite ## 3. Home
- [ ] Code entry field works
- [ ] Back button navigates correctly
- [ ] Valid code proceeds to InviteConfirm
### Invite Confirm ### 3.1 Home screen (`HomeScreen`)
- [ ] Partner info displays correctly - [ ] Loads without crash; loading, error, and success states all tested.
- [ ] "Confirm" button links couple in Firestore - [ ] Header shows correct subtitle for paired vs. unpaired user.
- [ ] "Wrong code" link returns to AcceptInvite - [ ] Streak pill appears when `streakCount > 0`.
- [ ] Back button works - [ ] 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.
## Settings ### 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.
- [ ] Settings screen loads with all sections ### 3.3 Moment cue / special dates section
- [ ] Profile row shows local profile state (not fake data) - [ ] `SpecialDatesSection` previews render.
- [ ] Partner row tappable, navigates to RelationshipSettings - [ ] Home moment cue card text not placeholder.
- [ ] Account screen shows profile info - [ ] Hardcoded names ("Jessica", "Mark") and dates in `SpecialDatesSection` must be replaced with real data before public release.
- [ ] Delete account screen requires checkbox acknowledgement
- [ ] Notification settings screen present
- [ ] Privacy screen present
- [ ] Subscription screen present
- [ ] Legal section: Privacy Policy link opens externally
- [ ] Legal section: Terms of Service link opens externally
- [ ] Sign out works
## Paywall ---
- [ ] Paywall screen shows benefits list ## 4. Daily Question
- [ ] Product options display with prices
- [ ] Purchase button triggers RevenueCat flow
- [ ] Restore purchases link works
- [ ] Privacy Policy and Terms links at bottom open externally
- [ ] "Subscription terms apply" disclaimer visible
- [ ] Thank-you overlay appears on successful purchase
- [ ] Loading and error states display correctly
## Notifications ### 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.
- [ ] App requests POST_NOTIFICATIONS permission on Android 13+ ### 4.2 Answer input components
- [ ] FCM token obtained and stored in Firestore - [ ] `QuestionAnswerInput`: empty-state handling, formatting, IME actions.
- [ ] Quiet hours can be set in notification settings - [ ] `QuestionHelpExpandable`: expand/collapse works.
- [ ] Notification channel "Reminders" created - [ ] `QuestionDiscussionThread`: disabled state copy is clear.
- [ ] `QuestionNavigationBar`: previous/next navigation and disabled states.
## Edge Cases & Crash Risks ### 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.
- [ ] App works offline (local cache) ---
- [ ] App handles no network gracefully (error states, not crashes)
- [ ] Rapid navigation doesn't crash (tap all tabs quickly)
- [ ] Back navigation from every screen works
- [ ] Rotation doesn't crash (if not locked to portrait)
- [ ] Empty states show for: no questions, no matches, no history, no partner
## Known Issues (from code scan) ## 5. Question Packs & Categories
- **TODOs in code**: `DateBuilderScreen.kt` has TODO placeholders for date/time picker dialogs (lines 174, 182) ### 5.1 Question pack library (`QuestionPackLibraryScreen`)
- **Debug logging**: `QuestionJsonParser.kt` uses `android.util.Log.e()` — should use Timber or remove before release - [ ] 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).
- [ ] Notifications, Subscription, Privacy & Terms rows open correct screens.
- [ ] 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.
- [ ] Delete account row navigates to `DELETE_ACCOUNT`.
- [ ] Back navigation works.
### 9.3 Notifications (`NotificationSettingsScreen`)
- [ ] All four toggles reflect persisted state.
- [ ] Toggling each calls repository and updates UI.
- [ ] Quiet hours description accurate (10 PM 8 AM).
### 9.4 Privacy (`PrivacyScreen`)
- [ ] External links open in browser.
- [ ] No browser available case handled via `ExternalLinks.openUrl` Toast fallback.
- [ ] Back navigation works.
- [ ] **Note**: Support URL is `https://couplesconnect.app/support` (legacy domain); should be migrated to `closer.app` before release.
### 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.
---
## 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 |
|---|------|-------|----------|
| 1 | Date builder | Date/time picker TODOs — fields are not interactive | High |
| 2 | Special dates | Hardcoded names/dates in `SpecialDatesSection` | High |
| 3 | Email invite | Placeholder screen with hardcoded `ABC123` code | Medium |
| 4 | Subscription | Placeholder screen, not real management | Medium |
| 5 | Partner home | Placeholder screen only | Medium |
| 6 | Settings | Account rows disabled ("Auth coming soon", "Export coming soon") | Medium |
| 7 | External links | Support URL points to `couplesconnect.app/support` | Low |
| 8 | Strings | 100+ hardcoded display strings; should move to `strings.xml` for localization | Low |
| 9 | Logging | `android.util.Log.e` used in `QuestionJsonParser` — acceptable for errors, but confirm no verbose/debug logs remain in release builds | Low |
---
## Sign-off
| Tester | Date | Build | Result |
|--------|------|-------|--------|
| | | | |

View File

@ -1,74 +1,163 @@
# Internal Testing Release Checklist # Closer — Internal Testing Build Checklist
Steps to prepare and distribute a private MVP build for internal testing. > Steps to prepare, sign, and distribute an internal testing build for the private MVP. Assumes Android Gradle Plugin 8.7.3, Kotlin 2.0.21, minSdk 26, targetSdk/compileSdk 35, and `app.closer` applicationId.
## 1. Signing Config ---
- [ ] Generate or locate keystore for signing ## 1. Pre-build Checklist
- Keytool command: `keytool -genkeypair -v -keystore closer-release.jks -keyalg RSA -keysize 2048 -validity 10000 -alias closer`
- [ ] Store keystore file securely (NOT in git)
- [ ] Add signing config to `app/build.gradle.kts`:
- `storeFile`, `storePassword`, `keyAlias`, `keyPassword`
- Read from `local.properties` or environment variables
- [ ] Verify debug and release build variants both compile
## 2. Build Configuration ### 1.1 Source readiness
- [ ] All feature branches for the MVP merged into the internal testing branch.
- [ ] No unintended `TODO` / `FIXME` blockers in release code (documented in `docs/qa/private-mvp-checklist.md`).
- [ ] No `com.couplesconnect` package references remain (verified via `grep -RIn` across `app/src`).
- [ ] No debug-only logging left in non-debug code paths (verbose/debug logs removed; error logging acceptable).
- [ ] Version matches private MVP: `versionName` should be `0.1.x` and `versionCode` incremented for each distributed build.
- [ ] `local.properties` contains required secrets but is not committed to git.
- [ ] Set `applicationId` to `app.closer` (verify not `com.couplesconnect`) ### 1.2 Secrets and API keys
- [ ] Set versionCode and versionName in `app/build.gradle.kts` - [ ] `google-services.json` is present in `app/` and matches the internal testing package (`app.closer`).
- [ ] Set `minSdk`, `targetSdk` to appropriate values - [ ] RevenueCat API key defined in `local.properties` as `RC_API_KEY`.
- [ ] Verify `RC_API_KEY` in `local.properties` (or placeholder for testing) - [ ] Firebase App Check debug token removed or disabled for release.
- [ ] Verify `google-services.json` is present and points to correct Firebase project - [ ] No hardcoded API keys, tokens, or credentials in Kotlin/XML source.
- [ ] Remove or guard debug logging (`android.util.Log` calls)
## 3. Firebase Configuration ### 1.3 Build configuration
- [ ] `build.gradle.kts` has `isMinifyEnabled = true` and `isShrinkResources = true` for `release`.
- [ ] ProGuard rules file (`proguard-rules.pro`) reviewed for Compose, Hilt, Room, Firebase, RevenueCat, and Navigation.
- [ ] `compileSdk = 35`, `targetSdk = 35`, `minSdk = 26`.
- [ ] `namespace = "app.closer"` and `applicationId = "app.closer"`.
- [ ] Kotlin `jvmTarget = "17"` and Java 17 compatibility set.
- [ ] `buildConfig = true` and `compose = true` enabled.
- [ ] Firebase project created (or confirmed existing) ---
- [ ] Authentication enabled: Email/Password sign-in method
- [ ] Firestore rules deployed and tested
- [ ] Cloud Functions deployed:
- [ ] `revenueCatWebhook`
- [ ] `syncEntitlement`
- [ ] `sendDailyQuestionReminder`
- [ ] `sendPartnerAnsweredNotification`
- [ ] `health`
- [ ] FCM enabled for the project
- [ ] RevenueCat project configured with Firebase integration (placeholder for now)
## 4. Test Accounts ## 2. Signing Config Checklist
- [ ] Create 2 test accounts (couple A: user1@test.com / user2@test.com) ### 2.1 Keystore
- [ ] Create 1 solo test account (single user: solo@test.com) - [ ] Internal testing keystore generated or available (`.jks` / `.keystore`).
- [ ] Verify sign-up → profile creation → home flow on each - [ ] Keystore password, key alias, and key password stored securely in CI or password manager — not in repo.
- [ ] Pair test couple via invite code - [ ] Keystore used for signing is backed up in a secure location (1Password, team vault, hardware token).
- [ ] Verify paired state persists across app restarts - [ ] Key validity period covers expected lifetime of the app (Google Play requires validity past 22 Oct 2033 for app signing key uploads).
## 5. Android App Distribution ### 2.2 Gradle signing configuration
- [ ] `signingConfigs` block in `app/build.gradle.kts` configured:
- [ ] Install Firebase App Distribution Gradle plugin ```kotlin
- [ ] Add `app distribution` section to `app/build.gradle.kts` signingConfigs {
- [ ] Build release APK: `./gradlew :app:assembleRelease` create("release") {
- [ ] Distribute via Firebase App Distribution: storeFile = file(System.getenv("KEYSTORE_PATH") ?: "release.keystore")
```bash storePassword = System.getenv("KEYSTORE_PASSWORD")
firebase appdistribution:distribute app/build/outputs/apk/release/app-release.apk \ keyAlias = System.getenv("KEY_ALIAS")
--app <FIREBASE_APP_ID> \ keyPassword = System.getenv("KEY_PASSWORD")
--groups internal-testers \ }
--release-notes "Private MVP v0.1.x" }
``` ```
- [ ] Add tester email addresses to Firebase App Distribution group - [ ] `buildTypes.release` references `signingConfigs.getByName("release")`.
- [ ] Testers receive download link via email - [ ] Local debug builds still use the default debug keystore.
## 6. Pre-Distribution Smoke Test ### 2.3 Verify signed APK/AAB
- [ ] Build release APK or AAB: `./gradlew :app:assembleRelease` or `:app:bundleRelease`.
- [ ] Confirm output is signed: `apksigner verify --verbose app-release.apk`.
- [ ] Check certificate fingerprints and expiration.
- [ ] Verify v1 (JAR) and v2/v3 APK signatures are present.
- [ ] Install release APK on a physical device ---
- [ ] Walk through the full QA checklist (`docs/qa/private-mvp-checklist.md`)
- [ ] Verify no crashes on cold start, warm start, or background restore
- [ ] Verify notification permissions prompt appears on Android 13+
- [ ] Verify paywall loads (even with placeholder API key)
## 7. Post-Distribution ## 3. Firebase / Production Environment Checklist
- [ ] Collect crash reports from Firebase Crashlytics ### 3.1 Firebase project
- [ ] Monitor Firestore usage in Firebase Console - [ ] Internal testing Firebase project created/separate from production.
- [ ] Monitor Cloud Function logs for errors - [ ] `app.closer` Android app registered with correct SHA-1/SHA-256 certificates.
- [ ] Collect tester feedback via dedicated channel - [ ] `google-services.json` downloaded and placed in `app/`.
### 3.2 Firebase services
- [ ] **Authentication**: Email/password provider enabled (and any OAuth providers used in the app).
- [ ] **Firestore**: Security rules reviewed; internal testing rules should match expected MVP rules.
- [ ] **Cloud Messaging**: Server key / FCM v1 credentials available for backend/push testing.
- [ ] **Crashlytics**: Crashlytics plugin applied and mapping file upload enabled.
- [ ] **Analytics**: No PII logged; events reviewed.
- [ ] **Remote Config**: Default values in-app handle absent remote values.
- [ ] **App Check**: Play Integrity provider configured for release builds; debug provider only for debug/internal.
### 3.3 Firebase secrets
- [ ] No Firebase API keys or FCM tokens committed.
- [ ] CI injects `google-services.json` or uses environment-based substitution.
---
## 4. Test Account Setup
### 4.1 Device / emulator accounts
- [ ] At least two test user accounts available (User A and User B) for couple pairing flows.
- [ ] Test accounts use `+` or tagged email addresses for isolation (e.g., `qa+a@closer.app`, `qa+b@closer.app`).
- [ ] Passwords stored in team vault; accounts enabled in Firebase Auth allowlist if applicable.
### 4.2 Data seeding
- [ ] Seed questions and categories in Firestore/remote config for internal testing.
- [ ] Seed at least one premium category and one free category.
- [ ] Prepare test RevenueCat products with sandbox pricing.
- [ ] Ensure test accounts have predictable subscription states (free, premium, expired).
### 4.3 Test scenarios matrix
| Scenario | User A | User B |
|----------|--------|--------|
| Unpaired new install | fresh account | fresh account |
| Paired couple | paired with B | paired with A |
| Premium unlock | subscribed | not subscribed |
| Different answer states | answered daily | not answered |
| Invite expiry | create invite | accept after expiry |
---
## 5. Distribution Checklist
### 5.1 Firebase App Distribution
- [ ] Firebase App Distribution Gradle plugin applied (optional but recommended).
- [ ] App Distribution configured with tester groups (e.g., `mvp-internal`, `qa-team`).
- [ ] Build variant selected: `release` (signed).
- [ ] Distribution command run: `./gradlew :app:assembleRelease :app:appDistributionUploadRelease`.
- [ ] Testers receive email/notification with download link.
- [ ] APK install succeeds and app launches.
### 5.2 Alternative distribution
- [ ] If not using App Distribution, use internal Google Play track with closed testing.
- [ ] Signed AAB uploaded to Play Console internal testing track.
- [ ] Testers added to internal testing list.
- [ ] Play Console review status tracked (internal tests may be immediately available).
### 5.3 Verification after install
- [ ] App installs and opens on target device.
- [ ] Onboarding flow reachable.
- [ ] Auth and pairing flows tested end-to-end on distributed build.
- [ ] Push notification received and quiet-hours suppression works.
- [ ] Paywall loads products in sandbox (where RevenueCat configured).
- [ ] Crashlytics reports non-fatal test event and appears in console.
---
## 6. Build Commands Reference
```bash
# Debug compile (fast smoke test)
./gradlew :app:compileDebugKotlin
# Debug install
./gradlew :app:installDebug
# Release APK (signed)
./gradlew :app:assembleRelease
# Release AAB (signed)
./gradlew :app:bundleRelease
# Firebase App Distribution upload
./gradlew :app:assembleRelease :app:appDistributionUploadRelease
```
---
## 7. Sign-off
| Role | Name | Date | Notes |
|------|------|------|-------|
| Build engineer | | | |
| QA lead | | | |
| Product owner | | | |

View File

@ -1,86 +1,176 @@
# Store Assets Checklist # Closer — Google Play Store Assets Checklist
Google Play Store asset preparation for Closer app release. > Checklist of assets, metadata, and compliance items needed for a Google Play Store submission. Targeting the private MVP / internal launch phase.
## Required Assets ---
### Screenshots (phone) ## 1. Store Listing Metadata
- [ ] 28 screenshots, minimum 320px, maximum 3840px
- [ ] Recommended: 1080x1920 or 1440x2560 (16:9 ratio)
- [ ] Required screenshots:
- [ ] Home screen with daily question
- [ ] Date Match swipe screen
- [ ] Date Matches (mutual likes)
- [ ] Question thread / answer reveal
- [ ] Spin Wheel
- [ ] Settings screen
### Feature Graphic ### 1.1 App name
- [ ] 1024x500px JPEG or PNG - [ ] App name finalized: **Closer**.
- [ ] Should convey app theme (couples, connection, questions) - [ ] App name matches `android:label` in `AndroidManifest.xml` (currently `@string/app_name`).
- [ ] Must not contain device frames or text that duplicates store listing - [ ] App name is not trademark-conflicting in target categories/territories.
### App Icon ### 1.2 Short description
- [ ] 512x512 PNG, 32-bit color, no transparency - [ ] Short description <= 80 characters.
- [ ] Adaptive icon already defined in project — verify it renders well - [ ] Communicates core value proposition for couples.
## Store Listing Text ### 1.3 Full description
- [ ] Full description drafted and reviewed.
- [ ] Mentions key features: daily questions, question packs, spin wheel, date planning, bucket list, answer reveal.
- [ ] Includes value/privacy angle (answers stay private, built for two people).
- [ ] No misleading claims or guarantees about relationships.
### Title ### 1.4 Keywords / tags
- [ ] Max 30 characters: "Closer — Couple Questions" - [ ] Primary category selected (e.g., Lifestyle, Dating, Health & Fitness).
- [ ] Relevant tags selected in Play Console.
### Short Description ---
- [ ] Max 80 characters
- [ ] Suggested: "Daily questions, date ideas, and deeper connection for couples."
### Full Description ## 2. Graphic Assets
- [ ] Max 4000 characters
- [ ] Should cover: daily questions, date match, spin wheel, bucket list, premium features
- [ ] Include relevant keywords naturally
### Category ### 2.1 App icon
- [ ] Primary: Lifestyle - [ ] Launcher icon provided in all required densities (`mipmap-mdpi` through `mipmap-xxxhdpi`).
- [ ] Secondary: Social (if available) - [ ] Adaptive icon foreground + background layers present (`ic_launcher_foreground`, `ic_launcher_background`).
- [ ] Icon tested on light and dark wallpapers.
- [ ] Round icon variant provided (`ic_launcher_round`).
### Tags ### 2.2 Feature graphic
- [ ] Couple, Relationship, Questions, Date Ideas, Connection - [ ] Feature graphic 1024 × 500 px (JPEG or PNG, no alpha).
- [ ] Brand name and key tagline legible at small sizes.
- [ ] Complies with Google Play policy (no device images, no price/calls-to-action like "Buy now").
## Legal Links ### 2.3 Screenshots
- [ ] Phone screenshots: minimum 2, recommended up to 8.
- Suggested screens:
- Onboarding / welcome
- Home dashboard
- Daily question + answer input
- Question pack library
- Spin wheel / category picker
- Date builder or bucket list
- Answer reveal / history
- Settings / subscription
- [ ] Screenshot dimensions one of: 1080×1920, 1080×2160, 1080×2220, etc. (9:16 to 9:20 typical).
- [ ] 7-inch tablet screenshots if targeting tablets (optional for MVP).
- [ ] 10-inch tablet screenshots if targeting tablets (optional for MVP).
- [ ] Screenshots do not contain placeholder or hardcoded test data.
- [ ] Status bar cleaned (no low-battery warnings, demo time 9:41, etc.).
- [ ] Privacy Policy URL: `https://closer.app/privacy` (currently placeholder — must be live before submission) ### 2.4 Promo video / preview
- [ ] Terms of Service URL: `https://closer.app/terms` (currently placeholder — must be live before submission) - [ ] Promo video optional but recommended; if used, YouTube URL added in Play Console.
- [ ] Subscription terms: `https://closer.app/subscription-terms` (placeholder) - [ ] Video shows real in-app experience, no prohibited content.
## Content Rating ### 2.5 Other assets
- [ ] 512 × 512 px high-res icon uploaded in Play Console.
- [ ] 16:9 promo graphic if running store experiments.
- [ ] Complete IARC questionnaire ---
- [ ] Expected rating: PEGI 3 / ESRB Everyone (no violence, no gambling, no user-generated content moderation concerns)
- [ ] Note: If question packs include sensitive topics, may need higher rating
## App Signing ## 3. Legal URLs
- [ ] Opt in to Google Play App Signing (recommended) - [ ] Privacy policy URL defined and reachable: currently `https://closer.app/privacy`.
- [ ] Upload signing certificate to Play Console - [ ] Terms of service URL defined and reachable: currently `https://closer.app/terms`.
- [ ] Keep local keystore backup in secure location - [ ] Subscription terms URL defined and reachable: currently `https://closer.app/subscription-terms`.
- [ ] DO NOT commit keystore to git - [ ] **Action needed**: Support URL currently points to `https://couplesconnect.app/support` in `ExternalLinks.kt`; migrate to `closer.app` domain and update store listing support/contact email.
- [ ] All legal pages load over HTTPS.
- [ ] Privacy policy accurately describes data collected (answers, FCM tokens, purchase data, couple pairing info).
## Pricing & Distribution ---
- [ ] Free with in-app purchases (RevenueCat) ## 4. Content Rating
- [ ] Subscription tiers configured in Google Play Console:
- [ ] Monthly premium
- [ ] Annual premium (if offering)
- [ ] Distribution: All countries (or select markets for soft launch)
## Pre-Launch Report ### 4.1 Rating questionnaire
- [ ] Content rating questionnaire completed in Play Console.
- [ ] App declared as targeting couples / adults.
- [ ] No user-generated content that requires additional moderation declared.
- [ ] Question content reviewed for mature themes; mark appropriately if intimacy questions are explicit.
- [ ] Run Google Play pre-launch report (automated after upload) ### 4.2 Age-based considerations
- [ ] Address any accessibility issues flagged - [ ] Determine if app should be rated Teen or Mature 17+ based on question language.
- [ ] Address any performance issues flagged - [ ] Ensure ads (none currently) do not affect rating.
- [ ] In-app purchase disclosure accurate.
## Known Gaps (resolve before public release) ---
- Date/Time pickers in DateBuilder are TODO placeholders ## 5. App Signing Key Checklist
- RevenueCat API key is placeholder
- Cloud Functions are scaffolded but not deployed to Firebase ### 5.1 Play App Signing
- Privacy/Terms URLs are placeholders — need real hosted pages - [ ] Decision made: Play App Signing enabled (recommended) or opt-out (requires justification).
- [ ] If using Play App Signing, upload key generated separately from signing key.
- [ ] Upload signing key backed up securely; loss blocks future updates.
- [ ] App signing key (held by Google) fingerprint recorded.
### 5.2 Key requirements
- [ ] Signing key algorithm: RSA, key size >= 2048 bits.
- [ ] Key validity period covers beyond 22 October 2033 (Google Play requirement).
- [ ] Keystore file, passwords, and alias stored in team password manager / HSM.
- [ ] CI pipeline injects signing credentials via environment variables, not hardcoded.
### 5.3 Verification
- [ ] Signed AAB/APK uploads successfully to Play Console.
- [ ] Play Console reports signature valid.
- [ ] Same signing key used for all future updates.
---
## 6. In-App Products / Subscriptions
- [ ] Subscription products created in Play Console matching RevenueCat product IDs.
- [ ] Product titles/descriptions match paywall copy.
- [ ] Base plans and offers configured (free trial if offered).
- [ ] Subscription terms URL linked.
- [ ] RevenueCat dashboard product IDs match Play Console SKUs.
- [ ] Test licensing enabled for internal testers.
---
## 7. Compliance & Policy
- [ ] App complies with Google Play Families Policy (if applicable).
- [ ] No deceptive behavior, malware, or abuse of permissions.
- [ ] Permissions declared (`INTERNET`, `POST_NOTIFICATIONS`) are necessary and explained if requested.
- [ ] Data safety form completed and consistent with privacy policy.
- [ ] Accessibility considerations documented; minimum touch targets and labels reviewed.
- [ ] No restricted content in screenshots or description.
---
## 8. Pre-Launch Checklist
- [ ] Internal testing track tested end-to-end with closed testers.
- [ ] Pre-launch report reviewed for crashes, ANRs, security, and performance.
- [ ] Store listing content spell-checked and approved.
- [ ] Release notes / "What's new" text prepared for first submission.
- [ ] Rollout plan: staged release percentage and rollback plan defined.
- [ ] Monitoring: Crashlytics, Analytics, and RevenueCat dashboards ready.
---
## 9. Asset Inventory
| Asset | Spec | Status | Owner |
|-------|------|--------|-------|
| App name | ≤ 30 chars displayed | Draft | |
| Short description | ≤ 80 chars | Draft | |
| Full description | ≤ 4,000 chars | Draft | |
| Launcher icon | Adaptive, all densities | Ready/Not ready | |
| Feature graphic | 1024 × 500 | Ready/Not ready | |
| Phone screenshots | 28 images | Ready/Not ready | |
| Tablet screenshots | Optional | Ready/Not ready | |
| Promo video | Optional | Ready/Not ready | |
| Privacy policy URL | HTTPS | Ready/Not ready | |
| Terms URL | HTTPS | Ready/Not ready | |
| Subscription terms URL | HTTPS | Ready/Not ready | |
| Support URL | HTTPS | Needs migration | |
---
## 10. Sign-off
| Role | Name | Date | Notes |
|------|------|------|-------|
| Product | | | |
| Design | | | |
| Legal / Compliance | | | |
| Engineering | | | |