From 574fed27f7560a42e979cb6908b86a5c5914bf7d Mon Sep 17 00:00:00 2001 From: null Date: Thu, 18 Jun 2026 03:51:12 -0500 Subject: [PATCH] refactor: replace raw string game types with GameType constants, remove unused feature flag module --- .../app/closer/core/feature/FeatureFlag.kt | 140 ------------------ .../closer/core/feature/FeaturePriority.kt | 11 -- .../closer/core/feature/FeatureRegistry.kt | 52 ------- .../app/closer/core/feature/FeatureStatus.kt | 11 -- .../QuestionSessionRepositoryImpl.kt | 7 +- .../java/app/closer/domain/model/GameType.kt | 8 + .../domain/usecase/GameSessionManager.kt | 9 +- .../closer/ui/desiresync/DesireSyncScreen.kt | 3 +- .../ui/games/WaitingForPartnerScreen.kt | 19 +-- .../app/closer/ui/howwell/HowWellScreen.kt | 3 +- .../closer/ui/pairing/AcceptInviteScreen.kt | 1 - .../closer/ui/thisorthat/ThisOrThatScreen.kt | 3 +- .../app/closer/ui/wheel/SpinWheelViewModel.kt | 3 +- 13 files changed, 35 insertions(+), 235 deletions(-) delete mode 100644 app/src/main/java/app/closer/core/feature/FeatureFlag.kt delete mode 100644 app/src/main/java/app/closer/core/feature/FeaturePriority.kt delete mode 100644 app/src/main/java/app/closer/core/feature/FeatureRegistry.kt delete mode 100644 app/src/main/java/app/closer/core/feature/FeatureStatus.kt create mode 100644 app/src/main/java/app/closer/domain/model/GameType.kt diff --git a/app/src/main/java/app/closer/core/feature/FeatureFlag.kt b/app/src/main/java/app/closer/core/feature/FeatureFlag.kt deleted file mode 100644 index 83682ab1..00000000 --- a/app/src/main/java/app/closer/core/feature/FeatureFlag.kt +++ /dev/null @@ -1,140 +0,0 @@ -package app.closer.core.feature - -/** - * Feature flag definition. - * Each feature has a stable key string, billing status, and priority. - * - * @property key Unique identifier for this feature (used in config, analytics, Remote Config) - * @property status FREE or PREMIUM - * @property priority MVP or LATER - * @property description Human-readable description - */ -sealed class FeatureFlag( - val key: String, - val status: FeatureStatus, - val priority: FeaturePriority, - val description: String -) { - data object DAILY_QUESTION : FeatureFlag( - key = "DAILY_QUESTION", - status = FeatureStatus.FREE, - priority = FeaturePriority.MVP, - description = "Answer one daily relationship question" - ) - - data object ANSWER_HISTORY : FeatureFlag( - key = "ANSWER_HISTORY", - status = FeatureStatus.FREE, - priority = FeaturePriority.MVP, - description = "View limited recent answer history" - ) - - data object BASIC_CATEGORIES : FeatureFlag( - key = "BASIC_CATEGORIES", - status = FeatureStatus.FREE, - priority = FeaturePriority.MVP, - description = "Basic question categories" - ) - - data object BASIC_REMINDERS : FeatureFlag( - key = "BASIC_REMINDERS", - status = FeatureStatus.FREE, - priority = FeaturePriority.MVP, - description = "Basic push reminders" - ) - - data object STREAK_TRACKING : FeatureFlag( - key = "STREAK_TRACKING", - status = FeatureStatus.FREE, - priority = FeaturePriority.MVP, - description = "Daily answer streak tracking" - ) - - data object SPIN_WHEEL_LIMITED : FeatureFlag( - key = "SPIN_WHEEL_LIMITED", - status = FeatureStatus.FREE, - priority = FeaturePriority.MVP, - description = "Limited spin wheel sessions" - ) - - data object PREMIUM_PACKS : FeatureFlag( - key = "PREMIUM_PACKS", - status = FeatureStatus.PREMIUM, - priority = FeaturePriority.MVP, - description = "Premium question packs" - ) - - data object FULL_SPIN_WHEEL : FeatureFlag( - key = "FULL_SPIN_WHEEL", - status = FeatureStatus.PREMIUM, - priority = FeaturePriority.MVP, - description = "All spin wheel categories + saved sessions" - ) - - data object UNLIMITED_QUESTIONS : FeatureFlag( - key = "UNLIMITED_QUESTIONS", - status = FeatureStatus.PREMIUM, - priority = FeaturePriority.LATER, - description = "Unlimited daily questions" - ) - - data object FULL_HISTORY : FeatureFlag( - key = "FULL_HISTORY", - status = FeatureStatus.PREMIUM, - priority = FeaturePriority.LATER, - description = "Full answer history with search/filter" - ) - - data object CUSTOM_QUESTIONS : FeatureFlag( - key = "CUSTOM_QUESTIONS", - status = FeatureStatus.PREMIUM, - priority = FeaturePriority.LATER, - description = "Create custom questions" - ) - - data object RELATIONSHIP_QUIZZES : FeatureFlag( - key = "RELATIONSHIP_QUIZZES", - status = FeatureStatus.PREMIUM, - priority = FeaturePriority.LATER, - description = "Deeper relationship quizzes" - ) - - data object PRIVATE_NOTES : FeatureFlag( - key = "PRIVATE_NOTES", - status = FeatureStatus.PREMIUM, - priority = FeaturePriority.LATER, - description = "Private notes per user" - ) - - data object EXPORT_MEMORIES : FeatureFlag( - key = "EXPORT_MEMORIES", - status = FeatureStatus.PREMIUM, - priority = FeaturePriority.LATER, - description = "Exportable memories" - ) - - data object ADVANCED_REMINDERS : FeatureFlag( - key = "ADVANCED_REMINDERS", - status = FeatureStatus.PREMIUM, - priority = FeaturePriority.LATER, - description = "Custom reminder times, quiet hours" - ) - - data object EXTRA_CATEGORIES : FeatureFlag( - key = "EXTRA_CATEGORIES", - status = FeatureStatus.PREMIUM, - priority = FeaturePriority.LATER, - description = "Extra question categories" - ) - - data object AI_QUESTIONS : FeatureFlag( - key = "AI_QUESTIONS", - status = FeatureStatus.PREMIUM, - priority = FeaturePriority.LATER, - description = "Future AI-assisted question suggestions" - ) - - // Convenience helpers - val isPremium: Boolean get() = status == FeatureStatus.PREMIUM - val isMvp: Boolean get() = priority == FeaturePriority.MVP -} diff --git a/app/src/main/java/app/closer/core/feature/FeaturePriority.kt b/app/src/main/java/app/closer/core/feature/FeaturePriority.kt deleted file mode 100644 index bfd7ede4..00000000 --- a/app/src/main/java/app/closer/core/feature/FeaturePriority.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.closer.core.feature - -/** - * Feature implementation priority. - * - MVP: Will be released in initial version (with or without paywall) - * - LATER: Future enhancement, not in initial release scope - */ -enum class FeaturePriority { - MVP, - LATER -} diff --git a/app/src/main/java/app/closer/core/feature/FeatureRegistry.kt b/app/src/main/java/app/closer/core/feature/FeatureRegistry.kt deleted file mode 100644 index 2cff6da3..00000000 --- a/app/src/main/java/app/closer/core/feature/FeatureRegistry.kt +++ /dev/null @@ -1,52 +0,0 @@ -package app.closer.core.feature - -/** - * Central registry for all feature flags. - * Holds all defined features and provides query helpers. - * - * Usage: - * ``` - * val features = FeatureRegistry.allFeatures() - * val premiumFeatures = FeatureRegistry.featuresByStatus(FeatureStatus.PREMIUM) - * val isPremium = FeatureRegistry.isPremiumFeature("CUSTOM_QUESTIONS") - * ``` - */ -object FeatureRegistry { - - // Query methods - fun allFeatures(): List = - listOf( - FeatureFlag.DAILY_QUESTION, - FeatureFlag.ANSWER_HISTORY, - FeatureFlag.BASIC_CATEGORIES, - FeatureFlag.BASIC_REMINDERS, - FeatureFlag.STREAK_TRACKING, - FeatureFlag.SPIN_WHEEL_LIMITED, - FeatureFlag.PREMIUM_PACKS, - FeatureFlag.FULL_SPIN_WHEEL, - FeatureFlag.UNLIMITED_QUESTIONS, - FeatureFlag.FULL_HISTORY, - FeatureFlag.CUSTOM_QUESTIONS, - FeatureFlag.RELATIONSHIP_QUIZZES, - FeatureFlag.PRIVATE_NOTES, - FeatureFlag.EXPORT_MEMORIES, - FeatureFlag.ADVANCED_REMINDERS, - FeatureFlag.EXTRA_CATEGORIES, - FeatureFlag.AI_QUESTIONS - ) - - fun getFeature(key: String): FeatureFlag? = - allFeatures().find { it.key == key } - - fun featuresByStatus(status: FeatureStatus): List = - allFeatures().filter { it.status == status } - - fun featuresByPriority(priority: FeaturePriority): List = - allFeatures().filter { it.priority == priority } - - fun isPremiumFeature(key: String): Boolean = - getFeature(key)?.isPremium == true - - fun isMvpFeature(key: String): Boolean = - getFeature(key)?.isMvp == true -} diff --git a/app/src/main/java/app/closer/core/feature/FeatureStatus.kt b/app/src/main/java/app/closer/core/feature/FeatureStatus.kt deleted file mode 100644 index 9e581c06..00000000 --- a/app/src/main/java/app/closer/core/feature/FeatureStatus.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.closer.core.feature - -/** - * Feature billing status. - * - FREE: Available to all users - * - PREMIUM: Requires active subscription/entitlement - */ -enum class FeatureStatus { - FREE, - PREMIUM -} diff --git a/app/src/main/java/app/closer/data/repository/QuestionSessionRepositoryImpl.kt b/app/src/main/java/app/closer/data/repository/QuestionSessionRepositoryImpl.kt index bc26082a..0942ad15 100644 --- a/app/src/main/java/app/closer/data/repository/QuestionSessionRepositoryImpl.kt +++ b/app/src/main/java/app/closer/data/repository/QuestionSessionRepositoryImpl.kt @@ -2,6 +2,7 @@ package app.closer.data.repository import app.closer.core.crash.CrashReporter import app.closer.data.remote.FirestoreCollections +import app.closer.domain.model.GameType import app.closer.domain.model.QuestionSession import app.closer.domain.repository.QuestionSessionRepository import com.google.firebase.firestore.FirebaseFirestore @@ -71,7 +72,7 @@ class QuestionSessionRepositoryImpl @Inject constructor( partnerCompletedAt = doc.getLong("partnerCompletedAt"), isPremium = doc.getBoolean("isPremium") ?: false, status = doc.getString("status") ?: "completed", - gameType = doc.getString("gameType") ?: "wheel" + gameType = doc.getString("gameType") ?: GameType.WHEEL ) } .onFailure { crashReporter.recordException(it) } @@ -104,7 +105,7 @@ class QuestionSessionRepositoryImpl @Inject constructor( partnerCompletedAt = doc.getLong("partnerCompletedAt"), isPremium = doc.getBoolean("isPremium") ?: false, status = doc.getString("status") ?: "active", - gameType = doc.getString("gameType") ?: "wheel" + gameType = doc.getString("gameType") ?: GameType.WHEEL ) } .onFailure { crashReporter.recordException(it) } @@ -138,7 +139,7 @@ class QuestionSessionRepositoryImpl @Inject constructor( partnerCompletedAt = doc.getLong("partnerCompletedAt"), isPremium = doc.getBoolean("isPremium") ?: false, status = doc.getString("status") ?: "active", - gameType = doc.getString("gameType") ?: "wheel" + gameType = doc.getString("gameType") ?: GameType.WHEEL ) } .onFailure { crashReporter.recordException(it) } diff --git a/app/src/main/java/app/closer/domain/model/GameType.kt b/app/src/main/java/app/closer/domain/model/GameType.kt new file mode 100644 index 00000000..00a1ff03 --- /dev/null +++ b/app/src/main/java/app/closer/domain/model/GameType.kt @@ -0,0 +1,8 @@ +package app.closer.domain.model + +object GameType { + const val WHEEL = "wheel" + const val THIS_OR_THAT = "this_or_that" + const val HOW_WELL = "how_well" + const val DESIRE_SYNC = "desire_sync" +} diff --git a/app/src/main/java/app/closer/domain/usecase/GameSessionManager.kt b/app/src/main/java/app/closer/domain/usecase/GameSessionManager.kt index a1a1727e..c723cfbc 100644 --- a/app/src/main/java/app/closer/domain/usecase/GameSessionManager.kt +++ b/app/src/main/java/app/closer/domain/usecase/GameSessionManager.kt @@ -1,6 +1,7 @@ package app.closer.domain.usecase import app.closer.domain.model.Couple +import app.closer.domain.model.GameType import app.closer.domain.model.QuestionSession import app.closer.domain.repository.AuthRepository import app.closer.domain.repository.CoupleRepository @@ -155,10 +156,10 @@ class GameSessionManager @Inject constructor( sessionRepository.observeActiveSessionForCouple(coupleId) private fun gameTypeLabel(gameType: String): String = when (gameType) { - "wheel" -> "Wheel" - "this_or_that" -> "This or That" - "how_well" -> "How Well Do You Know Me" - "desire_sync" -> "Desire Sync" + GameType.WHEEL -> "Wheel" + GameType.THIS_OR_THAT -> "This or That" + GameType.HOW_WELL -> "How Well Do You Know Me" + GameType.DESIRE_SYNC -> "Desire Sync" else -> gameType } } diff --git a/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt b/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt index 78ab4a7a..a5287cc6 100644 --- a/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt +++ b/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt @@ -1,5 +1,6 @@ package app.closer.ui.desiresync +import app.closer.domain.model.GameType import app.closer.ui.theme.closerCardColor import android.util.Log import androidx.compose.animation.animateColorAsState @@ -145,7 +146,7 @@ class DesireSyncViewModel @Inject constructor( private fun startSession() { viewModelScope.launch { - gameSessionManager.startGameForCurrentUser(gameType = "desire_sync") + gameSessionManager.startGameForCurrentUser(gameType = GameType.DESIRE_SYNC) .onSuccess { gameHandle = it } .onFailure { Log.w(TAG, "Could not start session", it) } } diff --git a/app/src/main/java/app/closer/ui/games/WaitingForPartnerScreen.kt b/app/src/main/java/app/closer/ui/games/WaitingForPartnerScreen.kt index ed7df670..1f4acf95 100644 --- a/app/src/main/java/app/closer/ui/games/WaitingForPartnerScreen.kt +++ b/app/src/main/java/app/closer/ui/games/WaitingForPartnerScreen.kt @@ -28,6 +28,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.closer.core.navigation.AppRoute +import app.closer.domain.model.GameType import app.closer.domain.usecase.GameSessionManager import app.closer.ui.components.CategoryGlyph import dagger.hilt.android.lifecycle.HiltViewModel @@ -42,7 +43,7 @@ import kotlinx.coroutines.launch data class WaitingForPartnerUiState( val isLoading: Boolean = true, - val gameType: String = "wheel", + val gameType: String = GameType.WHEEL, val partnerName: String = "Partner", val navigateTo: String? = null ) @@ -160,17 +161,17 @@ fun WaitingForPartnerScreen( } private fun gameTypeLabel(gameType: String): String = when (gameType) { - "wheel" -> "Wheel" - "this_or_that" -> "This or That" - "how_well" -> "How Well Do You Know Me" - "desire_sync" -> "Desire Sync" + GameType.WHEEL -> "Wheel" + GameType.THIS_OR_THAT -> "This or That" + GameType.HOW_WELL -> "How Well Do You Know Me" + GameType.DESIRE_SYNC -> "Desire Sync" else -> gameType } private fun gameTypeGlyphKey(gameType: String): String = when (gameType) { - "wheel" -> "play" - "this_or_that" -> "question" - "how_well" -> "predict" - "desire_sync" -> "sex_and_desire" + GameType.WHEEL -> "play" + GameType.THIS_OR_THAT -> "question" + GameType.HOW_WELL -> "predict" + GameType.DESIRE_SYNC -> "sex_and_desire" else -> "play" } diff --git a/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt b/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt index 67f5f14e..dc07f93b 100644 --- a/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt +++ b/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt @@ -1,5 +1,6 @@ package app.closer.ui.howwell +import app.closer.domain.model.GameType import app.closer.ui.theme.closerCardColor import android.util.Log import androidx.compose.foundation.Canvas @@ -167,7 +168,7 @@ class HowWellViewModel @Inject constructor( private fun startSession() { viewModelScope.launch { - gameSessionManager.startGameForCurrentUser(gameType = "how_well") + gameSessionManager.startGameForCurrentUser(gameType = GameType.HOW_WELL) .onSuccess { gameHandle = it } .onFailure { Log.w(TAG, "Could not start session", it) } } diff --git a/app/src/main/java/app/closer/ui/pairing/AcceptInviteScreen.kt b/app/src/main/java/app/closer/ui/pairing/AcceptInviteScreen.kt index 2f93c149..2f0f6847 100644 --- a/app/src/main/java/app/closer/ui/pairing/AcceptInviteScreen.kt +++ b/app/src/main/java/app/closer/ui/pairing/AcceptInviteScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState diff --git a/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt b/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt index 760703fa..12040154 100644 --- a/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt +++ b/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt @@ -1,5 +1,6 @@ package app.closer.ui.thisorthat +import app.closer.domain.model.GameType import app.closer.ui.theme.closerCardColor import android.util.Log import androidx.compose.animation.animateColorAsState @@ -140,7 +141,7 @@ class ThisOrThatViewModel @Inject constructor( private fun startSession() { viewModelScope.launch { - gameSessionManager.startGameForCurrentUser(gameType = "this_or_that") + gameSessionManager.startGameForCurrentUser(gameType = GameType.THIS_OR_THAT) .onSuccess { gameHandle = it } .onFailure { Log.w(TAG, "Could not start session", it) } } diff --git a/app/src/main/java/app/closer/ui/wheel/SpinWheelViewModel.kt b/app/src/main/java/app/closer/ui/wheel/SpinWheelViewModel.kt index 4772a9a6..0f24de75 100644 --- a/app/src/main/java/app/closer/ui/wheel/SpinWheelViewModel.kt +++ b/app/src/main/java/app/closer/ui/wheel/SpinWheelViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.closer.core.navigation.AppRoute +import app.closer.domain.model.GameType import app.closer.domain.repository.QuestionRepository import app.closer.domain.usecase.GameSessionManager import dagger.hilt.android.lifecycle.HiltViewModel @@ -116,7 +117,7 @@ class SpinWheelViewModel @Inject constructor( val startResult = runCatching { gameSessionManager.startGame( userId = userId, - gameType = "wheel", + gameType = GameType.WHEEL, categoryId = categoryId, questionIds = questionIds )