Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve

## [Unreleased]

### Changed

- Improved onboarding flow to guide existing users through equipment setup and bean configuration when new features are available.

## [1.4.1]

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,32 @@ interface OnboardingManager {
* @return true if restore was successful, false otherwise
*/
suspend fun restoreOnboardingState(backupData: String): Boolean

// DEBUG METHODS FOR TESTING ONBOARDING FLOWS

/**
* Resets onboarding state to simulate a completely new user.
* Clears all progress so the user will see the full onboarding flow.
*/
suspend fun resetToNewUser()

/**
* Configures onboarding state to simulate an existing user who has created beans.
* Sets appropriate flags to skip introduction and bean creation.
*/
suspend fun resetToExistingUserWithBeans()

/**
* Configures onboarding state to simulate an existing user who hasn't created beans.
* Sets appropriate flags to skip introduction but show equipment setup and bean creation.
*/
suspend fun resetToExistingUserNoBeans()

/**
* Forces equipment setup to appear by setting the version to an older value.
* Simulates the scenario where new equipment features have been added.
*/
suspend fun forceEquipmentSetup()
}

/**
Expand All @@ -65,6 +91,8 @@ data class OnboardingProgress(
val hasCreatedFirstBean: Boolean = false,
val hasRecordedFirstShot: Boolean = false,
val grinderConfigurationId: String? = null,
val basketConfigurationId: String? = null, // NEW: Track basket configuration
val equipmentSetupVersion: Int = 1, // NEW: Track equipment setup version for forcing updates
val onboardingStartedAt: Long = System.currentTimeMillis(),
val lastUpdatedAt: Long = System.currentTimeMillis()
) {
Expand All @@ -74,7 +102,11 @@ data class OnboardingProgress(
* @return true if all onboarding steps are complete
*/
fun isComplete(): Boolean {
return hasSeenIntroduction && hasCompletedEquipmentSetup && hasCreatedFirstBean && hasRecordedFirstShot
return hasSeenIntroduction &&
hasCompletedEquipmentSetup &&
equipmentSetupVersion >= CURRENT_EQUIPMENT_SETUP_VERSION &&
hasCreatedFirstBean &&
hasRecordedFirstShot
}

/**
Expand All @@ -85,12 +117,28 @@ data class OnboardingProgress(
fun getNextStep(): OnboardingStep? {
return when {
!hasSeenIntroduction -> OnboardingStep.INTRODUCTION
!hasCompletedEquipmentSetup -> OnboardingStep.EQUIPMENT_SETUP
!hasCompletedEquipmentSetup || equipmentSetupVersion < CURRENT_EQUIPMENT_SETUP_VERSION -> OnboardingStep.EQUIPMENT_SETUP
!hasCreatedFirstBean -> OnboardingStep.GUIDED_BEAN_CREATION
!hasRecordedFirstShot -> OnboardingStep.FIRST_SHOT
else -> null
}
}

/**
* Checks if equipment setup needs to be completed or updated.
* This will be true for new users and existing users who haven't completed the latest equipment setup.
*/
fun needsEquipmentSetup(): Boolean {
return !hasCompletedEquipmentSetup || equipmentSetupVersion < CURRENT_EQUIPMENT_SETUP_VERSION
}

companion object {
/**
* Current version of equipment setup. Increment this to force existing users
* to go through equipment setup again when new features are added.
*/
const val CURRENT_EQUIPMENT_SETUP_VERSION = 2 // Incremented for basket configuration
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,67 @@ class OnboardingPreferences @Inject constructor(
.apply()
}

// DEBUG METHODS FOR TESTING ONBOARDING FLOWS

override suspend fun resetToNewUser() = withContext(Dispatchers.IO) {
// Clear all onboarding state to simulate a completely new user
sharedPreferences.edit()
.remove(KEY_ONBOARDING_COMPLETE)
.remove(KEY_ONBOARDING_PROGRESS)
.remove(KEY_GRINDER_CONFIG_ID)
.apply()
}

override suspend fun resetToExistingUserWithBeans() = withContext(Dispatchers.IO) {
// Set state for existing user who has beans (skips intro and bean creation)
val existingUserProgress = OnboardingProgress(
hasSeenIntroduction = true,
hasCompletedEquipmentSetup = true,
hasCreatedFirstBean = true,
hasRecordedFirstShot = false, // Still needs to record first shot
equipmentSetupVersion = OnboardingProgress.CURRENT_EQUIPMENT_SETUP_VERSION,
onboardingStartedAt = System.currentTimeMillis() - 86400000, // 24 hours ago
lastUpdatedAt = System.currentTimeMillis()
)

sharedPreferences.edit()
.putString(KEY_ONBOARDING_PROGRESS, json.encodeToString(existingUserProgress.toSerializable()))
.putBoolean(KEY_ONBOARDING_COMPLETE, false) // Not complete until first shot
.apply()
}

override suspend fun resetToExistingUserNoBeans() = withContext(Dispatchers.IO) {
// Set state for existing user without beans (skips intro, shows equipment + bean creation)
val existingUserProgress = OnboardingProgress(
hasSeenIntroduction = true,
hasCompletedEquipmentSetup = true,
hasCreatedFirstBean = false, // Still needs to create beans
hasRecordedFirstShot = false,
equipmentSetupVersion = OnboardingProgress.CURRENT_EQUIPMENT_SETUP_VERSION,
onboardingStartedAt = System.currentTimeMillis() - 86400000, // 24 hours ago
lastUpdatedAt = System.currentTimeMillis()
)

sharedPreferences.edit()
.putString(KEY_ONBOARDING_PROGRESS, json.encodeToString(existingUserProgress.toSerializable()))
.putBoolean(KEY_ONBOARDING_COMPLETE, false)
.apply()
}

override suspend fun forceEquipmentSetup() = withContext(Dispatchers.IO) {
// Set equipment setup version to older value to force it to appear
val currentProgress = getOnboardingProgress()
val forcedProgress = currentProgress.copy(
equipmentSetupVersion = OnboardingProgress.CURRENT_EQUIPMENT_SETUP_VERSION - 1,
lastUpdatedAt = System.currentTimeMillis()
)

sharedPreferences.edit()
.putString(KEY_ONBOARDING_PROGRESS, json.encodeToString(forcedProgress.toSerializable()))
.putBoolean(KEY_ONBOARDING_COMPLETE, false)
.apply()
}

companion object {
private const val KEY_ONBOARDING_COMPLETE = "onboarding_complete"
private const val KEY_ONBOARDING_PROGRESS = "onboarding_progress"
Expand All @@ -145,6 +206,8 @@ private data class SerializableOnboardingProgress(
val hasCreatedFirstBean: Boolean = false,
val hasRecordedFirstShot: Boolean = false,
val grinderConfigurationId: String? = null,
val basketConfigurationId: String? = null,
val equipmentSetupVersion: Int = 1, // Default to 1 for existing users
val onboardingStartedAt: Long = System.currentTimeMillis(),
val lastUpdatedAt: Long = System.currentTimeMillis()
) {
Expand All @@ -155,6 +218,8 @@ private data class SerializableOnboardingProgress(
hasCreatedFirstBean = hasCreatedFirstBean,
hasRecordedFirstShot = hasRecordedFirstShot,
grinderConfigurationId = grinderConfigurationId,
basketConfigurationId = basketConfigurationId,
equipmentSetupVersion = equipmentSetupVersion,
onboardingStartedAt = onboardingStartedAt,
lastUpdatedAt = lastUpdatedAt
)
Expand All @@ -171,6 +236,8 @@ private fun OnboardingProgress.toSerializable(): SerializableOnboardingProgress
hasCreatedFirstBean = hasCreatedFirstBean,
hasRecordedFirstShot = hasRecordedFirstShot,
grinderConfigurationId = grinderConfigurationId,
basketConfigurationId = basketConfigurationId,
equipmentSetupVersion = equipmentSetupVersion,
onboardingStartedAt = onboardingStartedAt,
lastUpdatedAt = lastUpdatedAt
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,24 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.PersonAdd
import androidx.compose.material.icons.filled.PersonOutline
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
Expand All @@ -40,21 +47,23 @@ import com.jodli.coffeeshottimer.ui.theme.LocalSpacing
import kotlinx.coroutines.delay

/**
* Debug dialog that provides database management utilities for developers.
* Debug dialog that provides database management and onboarding testing utilities for developers.
* Only available in debug builds to prevent accidental use in production.
*
* Features:
* - Fill database with realistic test data for screenshots
* - Add more shots for testing data volume
* - Clear all database data for clean testing
* - Reset onboarding state for testing different user scenarios
* - Loading states and result feedback
* - Confirmation dialogs for destructive operations
*
* @param isVisible Whether the dialog should be displayed
* @param onDismiss Callback invoked when dialog should be dismissed
* @param onFillDatabase Callback invoked when fill database button is tapped
* @param onAddMoreShots Callback invoked when add more shots button is tapped
* @param onClearDatabase Callback invoked when clear database button is tapped
* @param onResetToNewUser Callback invoked when reset to new user button is tapped
* @param onResetToExistingUserNoBeans Callback invoked when reset to existing user without beans button is tapped
* @param onForceEquipmentSetup Callback invoked when force equipment setup button is tapped
* @param isLoading Whether a database operation is currently in progress
* @param operationResult Result message from the last operation (success or error)
* @param showConfirmation Whether to show confirmation dialog for clear operation
Expand All @@ -66,8 +75,10 @@ fun DebugDialog(
isVisible: Boolean,
onDismiss: () -> Unit,
onFillDatabase: () -> Unit,
onAddMoreShots: () -> Unit,
onClearDatabase: () -> Unit,
onResetToNewUser: () -> Unit,
onResetToExistingUserNoBeans: () -> Unit,
onForceEquipmentSetup: () -> Unit,
isLoading: Boolean,
operationResult: String?,
showConfirmation: Boolean,
Expand All @@ -81,7 +92,9 @@ fun DebugDialog(

Dialog(onDismissRequest = onDismiss) {
Card(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.8f),
shape = RoundedCornerShape(spacing.cornerLarge),
elevation = CardDefaults.cardElevation(defaultElevation = spacing.elevationDialog),
colors = CardDefaults.cardColors(
Expand All @@ -91,6 +104,7 @@ fun DebugDialog(
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(spacing.large)
) {
// Header with debug indicator
Expand Down Expand Up @@ -187,6 +201,16 @@ fun DebugDialog(
Column(
verticalArrangement = Arrangement.spacedBy(spacing.medium)
) {
// Database Operations Section
Text(
text = "Database Operations",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.SemiBold
)

Spacer(modifier = Modifier.height(spacing.small))

// Fill Database button
CoffeePrimaryButton(
text = stringResource(R.string.button_fill_database),
Expand All @@ -204,33 +228,77 @@ fun DebugDialog(

Spacer(modifier = Modifier.height(spacing.small))

// Add More Shots button
// Clear Database button
CoffeeSecondaryButton(
text = stringResource(R.string.button_add_more_shots),
onClick = onAddMoreShots,
icon = Icons.Default.Add,
text = stringResource(R.string.button_clear_database),
onClick = onShowConfirmation,
icon = Icons.Default.Delete,
enabled = !isLoading
)

Text(
text = stringResource(R.string.text_add_shots_description),
text = stringResource(R.string.text_clear_database_description),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = spacing.small)
)

Spacer(modifier = Modifier.height(spacing.large))

// Onboarding Testing Section
Text(
text = "Onboarding Testing",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.SemiBold
)

Spacer(modifier = Modifier.height(spacing.small))

// Clear Database button
// Reset to New User button
CoffeeSecondaryButton(
text = stringResource(R.string.button_clear_database),
onClick = onShowConfirmation,
icon = Icons.Default.Delete,
text = "New User",
onClick = onResetToNewUser,
icon = Icons.Default.PersonAdd,
enabled = !isLoading
)

Text(
text = stringResource(R.string.text_clear_database_description),
text = "Show full onboarding flow",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = spacing.small)
)

Spacer(modifier = Modifier.height(spacing.small))

// Reset to Existing User without Beans button
CoffeeSecondaryButton(
text = "User - No Beans",
onClick = onResetToExistingUserNoBeans,
icon = Icons.Default.PersonOutline,
enabled = !isLoading
)

Text(
text = "Trigger guided bean creation",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = spacing.small)
)

Spacer(modifier = Modifier.height(spacing.small))

// Force Equipment Setup button
CoffeeSecondaryButton(
text = "Equipment Setup",
onClick = onForceEquipmentSetup,
icon = Icons.Default.Settings,
enabled = !isLoading
)

Text(
text = "Force equipment setup flow",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = spacing.small)
Expand Down
Loading