diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index 20d993c..7661929 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -19,10 +19,10 @@ jobs:
run: chmod +x gradlew
- name: Build debug APK with Gradle
- run: ./gradlew assembleDebug
+ run: ./gradlew assembleFossDebug
- name: Run tests
- run: ./gradlew testDebugUnitTest
+ run: ./gradlew testFossDebugUnitTest
- name: Upload debug APK artifact
uses: actions/upload-artifact@v5
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 139e2d9..a396eba 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -43,8 +43,8 @@ android {
applicationId = "org.nsh07.pomodoro"
minSdk = 27
targetSdk = 36
- versionCode = 15
- versionName = "1.6.0"
+ versionCode = 16
+ versionName = "1.6.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -58,6 +58,19 @@ android {
)
}
}
+
+ flavorDimensions += "version"
+ productFlavors {
+ create("foss") {
+ dimension = "version"
+ isDefault = true
+ }
+ create("play") {
+ dimension = "version"
+ versionNameSuffix = "-play"
+ }
+ }
+
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
@@ -103,6 +116,9 @@ dependencies {
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
+ "playImplementation"(libs.revenuecat.purchases)
+ "playImplementation"(libs.revenuecat.purchases.ui)
+
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
diff --git a/app/src/foss/java/org/nsh07/pomodoro/billing/FossBillingManager.kt b/app/src/foss/java/org/nsh07/pomodoro/billing/FossBillingManager.kt
new file mode 100644
index 0000000..529b58c
--- /dev/null
+++ b/app/src/foss/java/org/nsh07/pomodoro/billing/FossBillingManager.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2025 Nishant Mishra
+ *
+ * This file is part of Tomato - a minimalist pomodoro timer for Android.
+ *
+ * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
+ * General Public License as published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tomato.
+ * If not, see .
+ */
+
+package org.nsh07.pomodoro.billing
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/**
+ * Google Play implementation of BillingManager
+ */
+class FossBillingManager : BillingManager {
+ override val isPlus = MutableStateFlow(true).asStateFlow()
+ override val isLoaded = MutableStateFlow(true).asStateFlow()
+}
+
+object BillingManagerProvider {
+ val manager: BillingManager = FossBillingManager()
+}
\ No newline at end of file
diff --git a/app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt b/app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt
new file mode 100644
index 0000000..6df4870
--- /dev/null
+++ b/app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2025 Nishant Mishra
+ *
+ * This file is part of Tomato - a minimalist pomodoro timer for Android.
+ *
+ * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
+ * General Public License as published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tomato.
+ * If not, see .
+ */
+
+package org.nsh07.pomodoro.billing
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme.colorScheme
+import androidx.compose.material3.MaterialTheme.typography
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import org.nsh07.pomodoro.R
+import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
+
+@Composable
+fun TomatoPlusPaywallDialog(
+ isPlus: Boolean,
+ onDismiss: () -> Unit
+) {
+ val uriHandler = LocalUriHandler.current
+
+ BackHandler(enabled = true, onDismiss)
+
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(colorScheme.surface)
+ ) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Icon(
+ painterResource(R.drawable.bmc),
+ null,
+ tint = colorScheme.onSurface
+ )
+ Spacer(Modifier.height(16.dp))
+ Text(
+ stringResource(R.string.tomato_foss),
+ style = typography.headlineSmall,
+ fontFamily = robotoFlexTopBar,
+ color = colorScheme.onSurface
+ )
+ Spacer(Modifier.height(8.dp))
+ Text(
+ stringResource(R.string.tomato_foss_desc, "BuyMeACoffee"),
+ textAlign = TextAlign.Center,
+ color = colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(horizontal = 24.dp)
+ )
+ Spacer(Modifier.height(16.dp))
+ Button(onClick = { uriHandler.openUri("https://coff.ee/nsh07") }) {
+ Text("Buy Me A Coffee")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/foss/java/org/nsh07/pomodoro/billing/initializePurchases.kt b/app/src/foss/java/org/nsh07/pomodoro/billing/initializePurchases.kt
new file mode 100644
index 0000000..7a44002
--- /dev/null
+++ b/app/src/foss/java/org/nsh07/pomodoro/billing/initializePurchases.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2025 Nishant Mishra
+ *
+ * This file is part of Tomato - a minimalist pomodoro timer for Android.
+ *
+ * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
+ * General Public License as published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tomato.
+ * If not, see .
+ */
+
+package org.nsh07.pomodoro.billing
+
+import android.content.Context
+
+fun initializePurchases(context: Context) {}
\ No newline at end of file
diff --git a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt
index a129af6..1917159 100644
--- a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt
@@ -30,12 +30,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.nsh07.pomodoro.ui.AppScreen
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
import org.nsh07.pomodoro.ui.theme.TomatoTheme
-import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
import org.nsh07.pomodoro.utils.toColor
class MainActivity : ComponentActivity() {
- private val timerViewModel: TimerViewModel by viewModels(factoryProducer = { TimerViewModel.Factory })
private val settingsViewModel: SettingsViewModel by viewModels(factoryProducer = { SettingsViewModel.Factory })
private val appContainer by lazy {
@@ -62,6 +60,20 @@ class MainActivity : ComponentActivity() {
val seed = preferencesState.colorScheme.toColor()
+ val isPlus by settingsViewModel.isPlus.collectAsStateWithLifecycle()
+ val isPurchaseStateLoaded by settingsViewModel.isPurchaseStateLoaded.collectAsStateWithLifecycle()
+ val isSettingsLoaded by settingsViewModel.isSettingsLoaded.collectAsStateWithLifecycle()
+
+ LaunchedEffect(isPurchaseStateLoaded, isPlus, isSettingsLoaded) {
+ if (isPurchaseStateLoaded && isSettingsLoaded) {
+ if (!isPlus) {
+ settingsViewModel.resetPaywalledSettings()
+ } else {
+ settingsViewModel.reloadSettings()
+ }
+ }
+ }
+
TomatoTheme(
darkTheme = darkTheme,
seedColor = seed,
@@ -73,7 +85,7 @@ class MainActivity : ComponentActivity() {
}
AppScreen(
- timerViewModel = timerViewModel,
+ isPlus = isPlus,
isAODEnabled = preferencesState.aodEnabled,
setTimerFrequency = {
appContainer.appTimerRepository.timerFrequency = it
diff --git a/app/src/main/java/org/nsh07/pomodoro/TomatoApplication.kt b/app/src/main/java/org/nsh07/pomodoro/TomatoApplication.kt
index 87be588..47de7fe 100644
--- a/app/src/main/java/org/nsh07/pomodoro/TomatoApplication.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/TomatoApplication.kt
@@ -1,8 +1,26 @@
+/*
+ * Copyright (c) 2025 Nishant Mishra
+ *
+ * This file is part of Tomato - a minimalist pomodoro timer for Android.
+ *
+ * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
+ * General Public License as published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tomato.
+ * If not, see .
+ */
+
package org.nsh07.pomodoro
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
+import org.nsh07.pomodoro.billing.initializePurchases
import org.nsh07.pomodoro.data.AppContainer
import org.nsh07.pomodoro.data.DefaultAppContainer
@@ -12,6 +30,8 @@ class TomatoApplication : Application() {
super.onCreate()
container = DefaultAppContainer(this)
+ initializePurchases(this)
+
val notificationChannel = NotificationChannel(
"timer",
getString(R.string.timer_progress),
diff --git a/app/src/main/java/org/nsh07/pomodoro/billing/BillingManager.kt b/app/src/main/java/org/nsh07/pomodoro/billing/BillingManager.kt
new file mode 100644
index 0000000..4cdc106
--- /dev/null
+++ b/app/src/main/java/org/nsh07/pomodoro/billing/BillingManager.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2025 Nishant Mishra
+ *
+ * This file is part of Tomato - a minimalist pomodoro timer for Android.
+ *
+ * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
+ * General Public License as published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tomato.
+ * If not, see .
+ */
+
+package org.nsh07.pomodoro.billing
+
+import kotlinx.coroutines.flow.StateFlow
+
+interface BillingManager {
+ val isPlus: StateFlow
+ val isLoaded: StateFlow
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt
index 4ca23c9..bf445c8 100644
--- a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt
@@ -26,6 +26,8 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import kotlinx.coroutines.flow.MutableStateFlow
import org.nsh07.pomodoro.R
+import org.nsh07.pomodoro.billing.BillingManager
+import org.nsh07.pomodoro.billing.BillingManagerProvider
import org.nsh07.pomodoro.service.addTimerActions
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
import org.nsh07.pomodoro.utils.millisecondsToStr
@@ -34,6 +36,7 @@ interface AppContainer {
val appPreferenceRepository: AppPreferenceRepository
val appStatRepository: AppStatRepository
val appTimerRepository: AppTimerRepository
+ val billingManager: BillingManager
val notificationManager: NotificationManagerCompat
val notificationManagerService: NotificationManager
val notificationBuilder: NotificationCompat.Builder
@@ -54,6 +57,8 @@ class DefaultAppContainer(context: Context) : AppContainer {
override val appTimerRepository: AppTimerRepository by lazy { AppTimerRepository() }
+ override val billingManager: BillingManager by lazy { BillingManagerProvider.manager }
+
override val notificationManager: NotificationManagerCompat by lazy {
NotificationManagerCompat.from(context)
}
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt
index 905e377..16a539b 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt
@@ -23,6 +23,8 @@ import androidx.compose.animation.Crossfade
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.calculateEndPadding
@@ -41,8 +43,10 @@ import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
@@ -54,6 +58,7 @@ import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import androidx.window.core.layout.WindowSizeClass
+import org.nsh07.pomodoro.billing.TomatoPlusPaywallDialog
import org.nsh07.pomodoro.service.TimerService
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
@@ -65,10 +70,11 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AppScreen(
- modifier: Modifier = Modifier,
- timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory),
isAODEnabled: Boolean,
- setTimerFrequency: (Float) -> Unit
+ isPlus: Boolean,
+ setTimerFrequency: (Float) -> Unit,
+ modifier: Modifier = Modifier,
+ timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory)
) {
val context = LocalContext.current
@@ -91,6 +97,7 @@ fun AppScreen(
}
}
+ var showPaywall by remember { mutableStateOf(false) }
Scaffold(
bottomBar = {
@@ -157,6 +164,7 @@ fun AppScreen(
entry {
TimerScreen(
timerState = uiState,
+ isPlus = isPlus,
progress = { progress },
onAction = { action ->
when (action) {
@@ -218,6 +226,7 @@ fun AppScreen(
entry {
SettingsScreenRoot(
+ setShowPaywall = { showPaywall = it },
modifier = modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection),
@@ -240,4 +249,12 @@ fun AppScreen(
)
}
}
+
+ AnimatedVisibility(
+ showPaywall,
+ enter = slideInVertically { it },
+ exit = slideOutVertically { it }
+ ) {
+ TomatoPlusPaywallDialog(isPlus = isPlus) { showPaywall = false }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt
index a22b1f5..2f8d2dd 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt
@@ -17,6 +17,7 @@
package org.nsh07.pomodoro.ui.settingsScreen
+import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import androidx.compose.animation.fadeIn
@@ -67,6 +68,7 @@ import org.nsh07.pomodoro.service.TimerService
import org.nsh07.pomodoro.ui.Screen
import org.nsh07.pomodoro.ui.settingsScreen.components.AboutCard
import org.nsh07.pomodoro.ui.settingsScreen.components.ClickableListItem
+import org.nsh07.pomodoro.ui.settingsScreen.components.PlusPromo
import org.nsh07.pomodoro.ui.settingsScreen.screens.AlarmSettings
import org.nsh07.pomodoro.ui.settingsScreen.screens.AppearanceSettings
import org.nsh07.pomodoro.ui.settingsScreen.screens.TimerSettings
@@ -80,6 +82,7 @@ import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreenRoot(
+ setShowPaywall: (Boolean) -> Unit,
modifier: Modifier = Modifier,
viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory)
) {
@@ -102,6 +105,7 @@ fun SettingsScreenRoot(
viewModel.longBreakTimeTextFieldState
}
+ val isPlus by viewModel.isPlus.collectAsStateWithLifecycle()
val alarmEnabled by viewModel.alarmEnabled.collectAsStateWithLifecycle(true)
val vibrateEnabled by viewModel.vibrateEnabled.collectAsStateWithLifecycle(true)
val dndEnabled by viewModel.dndEnabled.collectAsStateWithLifecycle(false)
@@ -119,6 +123,7 @@ fun SettingsScreenRoot(
}
SettingsScreen(
+ isPlus = isPlus,
preferencesState = preferencesState,
backStack = backStack,
focusTimeInputFieldState = focusTimeInputFieldState,
@@ -143,13 +148,16 @@ fun SettingsScreenRoot(
},
onThemeChange = viewModel::saveTheme,
onColorSchemeChange = viewModel::saveColorScheme,
+ setShowPaywall = setShowPaywall,
modifier = modifier
)
}
+@SuppressLint("LocalContextGetResourceValueCall")
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun SettingsScreen(
+ isPlus: Boolean,
preferencesState: PreferencesState,
backStack: SnapshotStateList,
focusTimeInputFieldState: TextFieldState,
@@ -168,6 +176,7 @@ private fun SettingsScreen(
onAlarmSoundChanged: (Uri?) -> Unit,
onThemeChange: (String) -> Unit,
onColorSchemeChange: (Color) -> Unit,
+ setShowPaywall: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
@@ -217,10 +226,20 @@ private fun SettingsScreen(
) {
item { Spacer(Modifier.height(12.dp)) }
- item { AboutCard() }
+ if (!isPlus) item {
+ PlusPromo(isPlus, setShowPaywall)
+ Spacer(Modifier.height(14.dp))
+ }
+
+ item { AboutCard(isPlus) }
item { Spacer(Modifier.height(12.dp)) }
+ if (isPlus) item {
+ PlusPromo(isPlus, setShowPaywall)
+ Spacer(Modifier.height(14.dp))
+ }
+
itemsIndexed(settingsScreens) { index, item ->
ClickableListItem(
leadingContent = {
@@ -266,14 +285,17 @@ private fun SettingsScreen(
entry {
AppearanceSettings(
preferencesState = preferencesState,
+ isPlus = isPlus,
onBlackThemeChange = onBlackThemeChange,
onThemeChange = onThemeChange,
onColorSchemeChange = onColorSchemeChange,
+ setShowPaywall = setShowPaywall,
onBack = backStack::removeLastOrNull
)
}
entry {
TimerSettings(
+ isPlus = isPlus,
aodEnabled = preferencesState.aodEnabled,
dndEnabled = dndEnabled,
focusTimeInputFieldState = focusTimeInputFieldState,
@@ -282,7 +304,8 @@ private fun SettingsScreen(
sessionsSliderState = sessionsSliderState,
onAodEnabledChange = onAodEnabledChange,
onDndEnabledChange = onDndEnabledChange,
- onBack = backStack::removeLastOrNull
+ setShowPaywall = setShowPaywall,
+ onBack = backStack::removeLastOrNull,
)
}
}
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutCard.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutCard.kt
index 3bf2f6c..1f85c76 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutCard.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutCard.kt
@@ -51,7 +51,10 @@ import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
// Taken from https://github.com/shub39/Grit/blob/master/app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt
@Composable
-fun AboutCard(modifier: Modifier = Modifier) {
+fun AboutCard(
+ isPlus: Boolean,
+ modifier: Modifier = Modifier
+) {
val uriHandler = LocalUriHandler.current
val context = LocalContext.current
@@ -77,7 +80,8 @@ fun AboutCard(modifier: Modifier = Modifier) {
) {
Column {
Text(
- text = stringResource(R.string.app_name),
+ if (!isPlus) stringResource(R.string.app_name)
+ else stringResource(R.string.app_name_plus),
style = MaterialTheme.typography.titleLarge,
fontFamily = robotoFlexTopBar
)
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt
index 03549b3..095ffec 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt
@@ -17,9 +17,9 @@
package org.nsh07.pomodoro.ui.settingsScreen.components
+import android.os.Build
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
@@ -28,6 +28,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -38,6 +40,7 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
@@ -56,6 +59,7 @@ fun ColorSchemePickerListItem(
color: Color,
items: Int,
index: Int,
+ isPlus: Boolean,
onColorChange: (Color) -> Unit,
modifier: Modifier = Modifier
) {
@@ -65,6 +69,7 @@ fun ColorSchemePickerListItem(
Color(0xff9fd75c), Color(0xffc1d02d), Color(0xfffabd00), Color(0xffffb86e),
Color.White
)
+ val zeroCorner = remember { CornerSize(0) }
Column(
modifier
@@ -76,45 +81,49 @@ fun ColorSchemePickerListItem(
}
)
) {
- ListItem(
- leadingContent = {
- Icon(
- painterResource(R.drawable.colors),
- null
- )
- },
- headlineContent = { Text("Dynamic color") },
- supportingContent = { Text("Adapt theme colors from your wallpaper") },
- trailingContent = {
- val checked = color == colorSchemes.last()
- Switch(
- checked = checked,
- onCheckedChange = {
- if (it) onColorChange(colorSchemes.last())
- else onColorChange(colorSchemes.first())
- },
- thumbContent = {
- if (checked) {
- Icon(
- painter = painterResource(R.drawable.check),
- contentDescription = null,
- modifier = Modifier.size(SwitchDefaults.IconSize),
- )
- } else {
- Icon(
- painter = painterResource(R.drawable.clear),
- contentDescription = null,
- modifier = Modifier.size(SwitchDefaults.IconSize),
- )
- }
- },
- colors = switchColors
- )
- },
- colors = listItemColors,
- modifier = Modifier.clip(middleListItemShape)
- )
- Spacer(Modifier.height(2.dp))
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ ListItem(
+ leadingContent = {
+ Icon(
+ painterResource(R.drawable.colors),
+ null
+ )
+ },
+ headlineContent = { Text(stringResource(R.string.dynamic_color)) },
+ supportingContent = { Text(stringResource(R.string.dynamic_color_desc)) },
+ trailingContent = {
+ val checked = color == colorSchemes.last()
+ Switch(
+ checked = checked,
+ onCheckedChange = {
+ if (it) onColorChange(colorSchemes.last())
+ else onColorChange(colorSchemes.first())
+ },
+ enabled = isPlus,
+ thumbContent = {
+ if (checked) {
+ Icon(
+ painter = painterResource(R.drawable.check),
+ contentDescription = null,
+ modifier = Modifier.size(SwitchDefaults.IconSize),
+ )
+ } else {
+ Icon(
+ painter = painterResource(R.drawable.clear),
+ contentDescription = null,
+ modifier = Modifier.size(SwitchDefaults.IconSize),
+ )
+ }
+ },
+ colors = switchColors
+ )
+ },
+ colors = listItemColors,
+ modifier = Modifier.clip(middleListItemShape)
+ )
+ Spacer(Modifier.height(2.dp))
+ }
+
ListItem(
leadingContent = {
Icon(
@@ -131,24 +140,31 @@ fun ColorSchemePickerListItem(
)
},
colors = listItemColors,
- modifier = Modifier.clip(middleListItemShape)
+ modifier = Modifier.clip(
+ RoundedCornerShape(
+ topStart = middleListItemShape.topStart,
+ topEnd = middleListItemShape.topEnd,
+ zeroCorner,
+ zeroCorner
+ )
+ )
)
- Column(
- verticalArrangement = Arrangement.spacedBy(8.dp),
+ LazyRow(
+ contentPadding = PaddingValues(horizontal = 48.dp),
+ userScrollEnabled = isPlus,
modifier = Modifier
.background(listItemColors.containerColor)
.padding(bottom = 8.dp)
) {
- LazyRow(contentPadding = PaddingValues(horizontal = 48.dp)) {
- items(colorSchemes.dropLast(1)) {
- ColorPickerButton(
- it,
- it == color,
- modifier = Modifier.padding(4.dp)
- ) {
- onColorChange(it)
- }
+ items(colorSchemes.dropLast(1)) {
+ ColorPickerButton(
+ color = it,
+ isSelected = it == color,
+ enabled = isPlus,
+ modifier = Modifier.padding(4.dp)
+ ) {
+ onColorChange(it)
}
}
}
@@ -160,12 +176,17 @@ fun ColorSchemePickerListItem(
fun ColorPickerButton(
color: Color,
isSelected: Boolean,
+ enabled: Boolean,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
IconButton(
shapes = IconButtonDefaults.shapes(),
- colors = IconButtonDefaults.iconButtonColors(containerColor = color),
+ colors = IconButtonDefaults.iconButtonColors(
+ containerColor = color,
+ disabledContainerColor = color.copy(0.3f)
+ ),
+ enabled = enabled,
modifier = modifier.size(48.dp),
onClick = onClick
) {
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/PlusDivider.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/PlusDivider.kt
new file mode 100644
index 0000000..e950564
--- /dev/null
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/PlusDivider.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2025 Nishant Mishra
+ *
+ * This file is part of Tomato - a minimalist pomodoro timer for Android.
+ *
+ * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
+ * General Public License as published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tomato.
+ * If not, see .
+ */
+
+package org.nsh07.pomodoro.ui.settingsScreen.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.MaterialTheme.colorScheme
+import androidx.compose.material3.MaterialTheme.typography
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun PlusDivider(
+ setShowPaywall: (Boolean) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Box(contentAlignment = Alignment.Center, modifier = modifier.padding(vertical = 14.dp)) {
+ HorizontalDivider(modifier = Modifier.clip(CircleShape), thickness = 4.dp)
+ Button(
+ onClick = { setShowPaywall(true) },
+ modifier = Modifier
+ .background(colorScheme.surfaceContainer)
+ .padding(horizontal = 8.dp)
+ ) {
+ Text("Customize further with Tomato+", style = typography.titleSmall)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/PlusPromo.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/PlusPromo.kt
new file mode 100644
index 0000000..858dbdb
--- /dev/null
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/PlusPromo.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2025 Nishant Mishra
+ *
+ * This file is part of Tomato - a minimalist pomodoro timer for Android.
+ *
+ * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
+ * General Public License as published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tomato.
+ * If not, see .
+ */
+
+package org.nsh07.pomodoro.ui.settingsScreen.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme.colorScheme
+import androidx.compose.material3.MaterialTheme.typography
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import org.nsh07.pomodoro.R
+import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
+
+@Composable
+fun PlusPromo(
+ isPlus: Boolean,
+ setShowPaywall: (Boolean) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val container = if (isPlus) colorScheme.surfaceBright else colorScheme.primary
+ val onContainer = if (isPlus) colorScheme.onSurface else colorScheme.onPrimary
+ val onContainerVariant = if (isPlus) colorScheme.onSurfaceVariant else colorScheme.onPrimary
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = modifier
+ .clip(CircleShape)
+ .background(container)
+ .padding(16.dp)
+ .clickable { setShowPaywall(true) }
+ ) {
+ Icon(
+ painterResource(R.drawable.tomato_logo_notification),
+ null,
+ tint = onContainerVariant,
+ modifier = Modifier
+ .size(24.dp)
+ )
+ Spacer(Modifier.width(8.dp))
+ Text(
+ if (!isPlus) stringResource(R.string.get_plus)
+ else stringResource(R.string.app_name_plus),
+ style = typography.titleLarge,
+ fontFamily = robotoFlexTopBar,
+ color = onContainer
+ )
+ Spacer(Modifier.weight(1f))
+ Icon(
+ painterResource(R.drawable.arrow_forward_big),
+ null,
+ tint = onContainerVariant
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt
index 0392765..411de79 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt
@@ -43,6 +43,7 @@ import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
+import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.cardShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@@ -69,11 +70,13 @@ fun ThemePickerListItem(
Column(
modifier
.clip(
- when (index) {
- 0 -> topListItemShape
- items - 1 -> bottomListItemShape
- else -> middleListItemShape
- },
+ if (items > 1)
+ when (index) {
+ 0 -> topListItemShape
+ items - 1 -> bottomListItemShape
+ else -> middleListItemShape
+ }
+ else cardShape,
),
) {
ListItem(
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt
index eb55fb8..6f6d903 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt
@@ -48,6 +48,7 @@ import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
import org.nsh07.pomodoro.ui.settingsScreen.components.ColorSchemePickerListItem
+import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider
import org.nsh07.pomodoro.ui.settingsScreen.components.ThemePickerListItem
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
@@ -55,16 +56,18 @@ import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
-import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
+import org.nsh07.pomodoro.ui.theme.TomatoTheme
import org.nsh07.pomodoro.utils.toColor
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AppearanceSettings(
preferencesState: PreferencesState,
+ isPlus: Boolean,
onBlackThemeChange: (Boolean) -> Unit,
onThemeChange: (String) -> Unit,
onColorSchemeChange: (Color) -> Unit,
+ setShowPaywall: (Boolean) -> Unit,
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
@@ -100,22 +103,26 @@ fun AppearanceSettings(
item {
Spacer(Modifier.height(14.dp))
}
- item {
- ColorSchemePickerListItem(
- color = preferencesState.colorScheme.toColor(),
- items = 3,
- index = 0,
- onColorChange = onColorSchemeChange
- )
- }
item {
ThemePickerListItem(
theme = preferencesState.theme,
onThemeChange = onThemeChange,
+ items = if (isPlus) 3 else 1,
+ index = 0
+ )
+ }
+
+ if (!isPlus) {
+ item { PlusDivider(setShowPaywall) }
+ }
+
+ item {
+ ColorSchemePickerListItem(
+ color = preferencesState.colorScheme.toColor(),
items = 3,
- index = 1,
- modifier = Modifier
- .clip(middleListItemShape)
+ index = if (isPlus) 1 else 0,
+ isPlus = isPlus,
+ onColorChange = onColorSchemeChange,
)
}
item {
@@ -136,6 +143,7 @@ fun AppearanceSettings(
Switch(
checked = item.checked,
onCheckedChange = { item.onClick(it) },
+ enabled = isPlus,
thumbContent = {
if (item.checked) {
Icon(
@@ -168,11 +176,15 @@ fun AppearanceSettings(
@Composable
fun AppearanceSettingsPreview() {
val preferencesState = PreferencesState()
- AppearanceSettings(
- preferencesState = preferencesState,
- onBlackThemeChange = {},
- onThemeChange = {},
- onColorSchemeChange = {},
- onBack = {}
- )
+ TomatoTheme {
+ AppearanceSettings(
+ preferencesState = preferencesState,
+ isPlus = false,
+ onBlackThemeChange = {},
+ onThemeChange = {},
+ onColorSchemeChange = {},
+ setShowPaywall = {},
+ onBack = {}
+ )
+ }
}
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt
index 421140c..b0c7ea3 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt
@@ -75,6 +75,7 @@ import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
import org.nsh07.pomodoro.ui.settingsScreen.components.MinuteInputField
+import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
@@ -88,6 +89,7 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun TimerSettings(
+ isPlus: Boolean,
aodEnabled: Boolean,
dndEnabled: Boolean,
focusTimeInputFieldState: TextFieldState,
@@ -97,7 +99,8 @@ fun TimerSettings(
onAodEnabledChange: (Boolean) -> Unit,
onDndEnabledChange: (Boolean) -> Unit,
onBack: () -> Unit,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
+ setShowPaywall: (Boolean) -> Unit
) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val context = LocalContext.current
@@ -260,7 +263,8 @@ fun TimerSettings(
)
}
item { Spacer(Modifier.height(12.dp)) }
- itemsIndexed(switchItems) { index, item ->
+
+ itemsIndexed(if (isPlus) switchItems else switchItems.take(1)) { index, item ->
ListItem(
leadingContent = {
Icon(
@@ -295,15 +299,60 @@ fun TimerSettings(
},
colors = listItemColors,
modifier = Modifier.clip(
- when (index) {
+ if (isPlus) when (index) {
0 -> topListItemShape
switchItems.size - 1 -> bottomListItemShape
else -> middleListItemShape
}
+ else cardShape
)
)
}
+ if (!isPlus) {
+ item {
+ PlusDivider(setShowPaywall)
+ }
+ itemsIndexed(switchItems.drop(1)) { index, item ->
+ ListItem(
+ leadingContent = {
+ Icon(
+ painterResource(item.icon),
+ contentDescription = null,
+ modifier = Modifier.padding(top = 4.dp)
+ )
+ },
+ headlineContent = { Text(stringResource(item.label)) },
+ supportingContent = { Text(stringResource(item.description)) },
+ trailingContent = {
+ Switch(
+ checked = item.checked,
+ onCheckedChange = { item.onClick(it) },
+ enabled = isPlus,
+ thumbContent = {
+ if (item.checked) {
+ Icon(
+ painter = painterResource(R.drawable.check),
+ contentDescription = null,
+ modifier = Modifier.size(SwitchDefaults.IconSize),
+ )
+ } else {
+ Icon(
+ painter = painterResource(R.drawable.clear),
+ contentDescription = null,
+ modifier = Modifier.size(SwitchDefaults.IconSize),
+ )
+ }
+ },
+ colors = switchColors
+ )
+ },
+ colors = listItemColors,
+ modifier = Modifier.clip(cardShape)
+ )
+ }
+ }
+
item {
var expanded by remember { mutableStateOf(false) }
Column(
@@ -352,14 +401,16 @@ private fun TimerSettingsPreview() {
steps = 6
)
TimerSettings(
+ isPlus = false,
+ aodEnabled = true,
+ dndEnabled = false,
focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState,
- aodEnabled = true,
- dndEnabled = false,
- onBack = {},
onAodEnabledChange = {},
- onDndEnabledChange = {}
+ onDndEnabledChange = {},
+ setShowPaywall = {},
+ onBack = {}
)
}
\ No newline at end of file
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt
index 0a3262f..94ba2c6 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt
@@ -40,17 +40,25 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.nsh07.pomodoro.TomatoApplication
+import org.nsh07.pomodoro.billing.BillingManager
import org.nsh07.pomodoro.data.AppPreferenceRepository
import org.nsh07.pomodoro.data.TimerRepository
import org.nsh07.pomodoro.ui.Screen
@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
class SettingsViewModel(
+ private val billingManager: BillingManager,
private val preferenceRepository: AppPreferenceRepository,
private val timerRepository: TimerRepository,
) : ViewModel() {
val backStack = mutableStateListOf(Screen.Settings.Main)
+ val isPlus = billingManager.isPlus
+ val isPurchaseStateLoaded = billingManager.isLoaded
+
+ private val _isSettingsLoaded = MutableStateFlow(false)
+ val isSettingsLoaded = _isSettingsLoaded.asStateFlow()
+
private val _preferencesState = MutableStateFlow(PreferencesState())
val preferencesState = _preferencesState.asStateFlow()
@@ -90,23 +98,8 @@ class SettingsViewModel(
init {
viewModelScope.launch {
- val theme = preferenceRepository.getStringPreference("theme")
- ?: preferenceRepository.saveStringPreference("theme", "auto")
- val colorScheme = preferenceRepository.getStringPreference("color_scheme")
- ?: preferenceRepository.saveStringPreference("color_scheme", Color.White.toString())
- val blackTheme = preferenceRepository.getBooleanPreference("black_theme")
- ?: preferenceRepository.saveBooleanPreference("black_theme", false)
- val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled")
- ?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
-
- _preferencesState.update { currentState ->
- currentState.copy(
- theme = theme,
- colorScheme = colorScheme,
- blackTheme = blackTheme,
- aodEnabled = aodEnabled
- )
- }
+ reloadSettings()
+ _isSettingsLoaded.value = true
}
}
@@ -231,14 +224,46 @@ class SettingsViewModel(
}
}
+ fun resetPaywalledSettings() {
+ _preferencesState.update { currentState ->
+ currentState.copy(
+ aodEnabled = false,
+ blackTheme = false,
+ colorScheme = Color.White.toString()
+ )
+ }
+ }
+
+ suspend fun reloadSettings() {
+ val theme = preferenceRepository.getStringPreference("theme")
+ ?: preferenceRepository.saveStringPreference("theme", "auto")
+ val colorScheme = preferenceRepository.getStringPreference("color_scheme")
+ ?: preferenceRepository.saveStringPreference("color_scheme", Color.White.toString())
+ val blackTheme = preferenceRepository.getBooleanPreference("black_theme")
+ ?: preferenceRepository.saveBooleanPreference("black_theme", false)
+ val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled")
+ ?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
+
+ _preferencesState.update { currentState ->
+ currentState.copy(
+ theme = theme,
+ colorScheme = colorScheme,
+ blackTheme = blackTheme,
+ aodEnabled = aodEnabled
+ )
+ }
+ }
+
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as TomatoApplication)
val appPreferenceRepository = application.container.appPreferenceRepository
val appTimerRepository = application.container.appTimerRepository
+ val appBillingManager = application.container.billingManager
SettingsViewModel(
+ billingManager = appBillingManager,
preferenceRepository = appPreferenceRepository,
timerRepository = appTimerRepository,
)
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt
index ee77820..a1a44f4 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt
@@ -107,6 +107,7 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
@Composable
fun SharedTransitionScope.TimerScreen(
timerState: TimerState,
+ isPlus: Boolean,
progress: () -> Float,
onAction: (TimerAction) -> Unit,
modifier: Modifier = Modifier
@@ -159,7 +160,8 @@ fun SharedTransitionScope.TimerScreen(
when (it) {
TimerMode.BRAND ->
Text(
- stringResource(R.string.app_name),
+ if (!isPlus) stringResource(R.string.app_name)
+ else stringResource(R.string.app_name_plus),
style = TextStyle(
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
@@ -552,6 +554,7 @@ fun TimerScreenPreview() {
SharedTransitionLayout {
TimerScreen(
timerState,
+ isPlus = true,
{ 0.3f },
{}
)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f4c25b8..79d3d99 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -81,4 +81,10 @@
Sound
Do Not Disturb
Turn on DND when running a Focus timer
+ Tomato+
+ Get Tomato+
+ Dynamic color
+ Adapt theme colors from your wallpaper
+ Tomato FOSS
+ All features are unlocked in this version. If my app made a difference in your life, please consider supporting me by donating on %1$s.
\ No newline at end of file
diff --git a/app/src/play/AndroidManifest.xml b/app/src/play/AndroidManifest.xml
new file mode 100644
index 0000000..97908e4
--- /dev/null
+++ b/app/src/play/AndroidManifest.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/play/java/org/nsh07/pomodoro/billing/PlayBillingManager.kt b/app/src/play/java/org/nsh07/pomodoro/billing/PlayBillingManager.kt
new file mode 100644
index 0000000..a658884
--- /dev/null
+++ b/app/src/play/java/org/nsh07/pomodoro/billing/PlayBillingManager.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2025 Nishant Mishra
+ *
+ * This file is part of Tomato - a minimalist pomodoro timer for Android.
+ *
+ * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
+ * General Public License as published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tomato.
+ * If not, see .
+ */
+
+package org.nsh07.pomodoro.billing
+
+import android.util.Log
+import com.revenuecat.purchases.Purchases
+import com.revenuecat.purchases.getCustomerInfoWith
+import com.revenuecat.purchases.interfaces.UpdatedCustomerInfoListener
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+private const val ENTITLEMENT_ID = "plus"
+
+/**
+ * Google Play implementation of BillingManager
+ */
+class PlayBillingManager : BillingManager {
+ private val _isPlus = MutableStateFlow(false)
+ override val isPlus = _isPlus.asStateFlow()
+
+ private val _isLoaded = MutableStateFlow(false)
+ override val isLoaded = _isLoaded.asStateFlow()
+
+ private val purchases by lazy { Purchases.sharedInstance }
+
+ init {
+ purchases.updatedCustomerInfoListener =
+ UpdatedCustomerInfoListener { customerInfo ->
+ _isPlus.value = customerInfo.entitlements[ENTITLEMENT_ID]?.isActive == true
+ }
+
+ // Fetch initial customer info
+ purchases.getCustomerInfoWith(
+ onSuccess = { customerInfo ->
+ _isPlus.value = customerInfo.entitlements[ENTITLEMENT_ID]?.isActive == true
+ _isLoaded.value = true
+ },
+ onError = { error ->
+ Log.e("GooglePlayPaywallManager", "Error fetching customer info: $error")
+ _isLoaded.value = true
+ }
+ )
+ }
+}
+
+object BillingManagerProvider {
+ val manager: BillingManager = PlayBillingManager()
+}
\ No newline at end of file
diff --git a/app/src/play/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt b/app/src/play/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt
new file mode 100644
index 0000000..bd7b318
--- /dev/null
+++ b/app/src/play/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2025 Nishant Mishra
+ *
+ * This file is part of Tomato - a minimalist pomodoro timer for Android.
+ *
+ * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
+ * General Public License as published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tomato.
+ * If not, see .
+ */
+
+package org.nsh07.pomodoro.billing
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.FilledTonalIconButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.revenuecat.purchases.ui.revenuecatui.Paywall
+import com.revenuecat.purchases.ui.revenuecatui.PaywallOptions
+import com.revenuecat.purchases.ui.revenuecatui.customercenter.CustomerCenter
+import org.nsh07.pomodoro.R
+
+@Composable
+fun TomatoPlusPaywallDialog(
+ isPlus: Boolean,
+ onDismiss: () -> Unit
+) {
+ val paywallOptions = remember {
+ PaywallOptions.Builder(dismissRequest = onDismiss).build()
+ }
+
+ Scaffold { innerPadding ->
+ if (!isPlus) {
+ Paywall(paywallOptions)
+
+ FilledTonalIconButton(
+ onClick = onDismiss,
+ modifier = Modifier
+ .padding(innerPadding)
+ .padding(16.dp)
+ ) {
+ Icon(
+ painterResource(R.drawable.arrow_back),
+ null
+ )
+ }
+ } else {
+ CustomerCenter(onDismiss = onDismiss)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/play/java/org/nsh07/pomodoro/billing/initializePurchases.kt b/app/src/play/java/org/nsh07/pomodoro/billing/initializePurchases.kt
new file mode 100644
index 0000000..00e2437
--- /dev/null
+++ b/app/src/play/java/org/nsh07/pomodoro/billing/initializePurchases.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2025 Nishant Mishra
+ *
+ * This file is part of Tomato - a minimalist pomodoro timer for Android.
+ *
+ * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
+ * General Public License as published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tomato.
+ * If not, see .
+ */
+
+package org.nsh07.pomodoro.billing
+
+import android.content.Context
+import com.revenuecat.purchases.Purchases
+import com.revenuecat.purchases.PurchasesConfiguration
+
+fun initializePurchases(context: Context) {
+ Purchases.configure(
+ PurchasesConfiguration
+ .Builder(context, "goog_jBpRIBjTYvhKYluCqkPXSHbuFbX")
+ .build()
+ )
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 783dd5d..85f21bf 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -12,6 +12,7 @@ ksp = "2.2.20-2.0.4"
lifecycleRuntimeKtx = "2.9.4"
materialKolor = "3.0.1"
navigation3 = "1.0.0-beta01"
+revenuecat = "9.12.0"
room = "2.8.3"
vico = "2.2.1"
@@ -39,6 +40,8 @@ androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
material-kolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" }
+revenuecat-purchases = { module = "com.revenuecat.purchases:purchases", version.ref = "revenuecat" }
+revenuecat-purchases-ui = { module = "com.revenuecat.purchases:purchases-ui", version.ref = "revenuecat" }
vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico" }
[plugins]