refactor: replace raw string game types with GameType constants, remove unused feature flag module

This commit is contained in:
null 2026-06-18 03:51:12 -05:00
parent debc6aed02
commit 574fed27f7
13 changed files with 35 additions and 235 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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<FeatureFlag> =
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<FeatureFlag> =
allFeatures().filter { it.status == status }
fun featuresByPriority(priority: FeaturePriority): List<FeatureFlag> =
allFeatures().filter { it.priority == priority }
fun isPremiumFeature(key: String): Boolean =
getFeature(key)?.isPremium == true
fun isMvpFeature(key: String): Boolean =
getFeature(key)?.isMvp == true
}

View File

@ -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
}

View File

@ -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) }

View File

@ -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"
}

View File

@ -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
}
}

View File

@ -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) }
}

View File

@ -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"
}

View File

@ -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) }
}

View File

@ -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

View File

@ -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) }
}

View File

@ -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
)