From 9c6282e10534d2b19229f2c4b1363c6b893b1c5b Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sun, 26 Oct 2025 08:16:41 +0530 Subject: [PATCH 01/11] feat(build): add play build flavor for paywall --- app/build.gradle.kts | 15 +++++++++++++++ app/src/play/AndroidManifest.xml | 21 +++++++++++++++++++++ gradle/libs.versions.toml | 2 ++ 3 files changed, 38 insertions(+) create mode 100644 app/src/play/AndroidManifest.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 139e2d9..be26edf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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,8 @@ dependencies { implementation(libs.androidx.room.ktx) ksp(libs.androidx.room.compiler) + "playImplementation"(libs.purchases) + testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) 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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 783dd5d..9356ceb 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" +purchases = "9.12.0" room = "2.8.3" vico = "2.2.1" @@ -39,6 +40,7 @@ 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" } +purchases = { module = "com.revenuecat.purchases:purchases", version.ref = "purchases" } vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico" } [plugins] From fcab066fabd378cf80bfe448b416e71bc9bd3239 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sun, 26 Oct 2025 08:23:40 +0530 Subject: [PATCH 02/11] ci: update build flavor name in CI to fix failing build --- .github/workflows/android.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 7aed88d1c50ebbb97b4eabf9c82d698d7aea8948 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sun, 26 Oct 2025 08:25:27 +0530 Subject: [PATCH 03/11] chore(release): bump version --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index be26edf..31e1df6 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" } From 16b4a9f125309ff3a9a80c671031b80a23b42736 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Mon, 27 Oct 2025 11:31:07 +0530 Subject: [PATCH 04/11] feat(billing): implement a simple RevenueCat billing system Only for the Google Play version. The FOSS version remains free. --- app/build.gradle.kts | 3 +- .../pomodoro/billing/FossBillingManager.kt | 32 ++++++++++ .../billing/TomatoPlusPaywallDialog.kt | 27 ++++++++ .../pomodoro/billing/initializePurchases.kt | 22 +++++++ .../java/org/nsh07/pomodoro/MainActivity.kt | 6 +- .../org/nsh07/pomodoro/TomatoApplication.kt | 20 ++++++ .../nsh07/pomodoro/billing/BillingManager.kt | 24 +++++++ .../org/nsh07/pomodoro/data/AppContainer.kt | 5 ++ .../java/org/nsh07/pomodoro/ui/AppScreen.kt | 13 +++- .../ui/settingsScreen/SettingsScreen.kt | 49 +++++++++++++++ .../viewModel/SettingsViewModel.kt | 6 ++ .../pomodoro/billing/PlayBillingManager.kt | 58 +++++++++++++++++ .../billing/TomatoPlusPaywallDialog.kt | 63 +++++++++++++++++++ .../pomodoro/billing/initializePurchases.kt | 30 +++++++++ gradle/libs.versions.toml | 5 +- 15 files changed, 354 insertions(+), 9 deletions(-) create mode 100644 app/src/foss/java/org/nsh07/pomodoro/billing/FossBillingManager.kt create mode 100644 app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt create mode 100644 app/src/foss/java/org/nsh07/pomodoro/billing/initializePurchases.kt create mode 100644 app/src/main/java/org/nsh07/pomodoro/billing/BillingManager.kt create mode 100644 app/src/play/java/org/nsh07/pomodoro/billing/PlayBillingManager.kt create mode 100644 app/src/play/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt create mode 100644 app/src/play/java/org/nsh07/pomodoro/billing/initializePurchases.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 31e1df6..a396eba 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -116,7 +116,8 @@ dependencies { implementation(libs.androidx.room.ktx) ksp(libs.androidx.room.compiler) - "playImplementation"(libs.purchases) + "playImplementation"(libs.revenuecat.purchases) + "playImplementation"(libs.revenuecat.purchases.ui) testImplementation(libs.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..4a155a3 --- /dev/null +++ b/app/src/foss/java/org/nsh07/pomodoro/billing/FossBillingManager.kt @@ -0,0 +1,32 @@ +/* + * 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() +} + +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..973041c --- /dev/null +++ b/app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt @@ -0,0 +1,27 @@ +/* + * 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.runtime.Composable + +@Composable +fun TomatoPlusPaywallDialog( + isPlus: Boolean, + onDismiss: () -> Unit +) { +} \ 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..4f34773 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,8 @@ class MainActivity : ComponentActivity() { val seed = preferencesState.colorScheme.toColor() + val isPlus by settingsViewModel.isPlus.collectAsStateWithLifecycle() + TomatoTheme( darkTheme = darkTheme, seedColor = seed, @@ -73,7 +73,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..66b1659 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/billing/BillingManager.kt @@ -0,0 +1,24 @@ +/* + * 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 +} \ 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..5455ad9 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt @@ -41,8 +41,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 +56,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 +68,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 +95,7 @@ fun AppScreen( } } + var showPaywall by remember { mutableStateOf(false) } Scaffold( bottomBar = { @@ -218,6 +223,7 @@ fun AppScreen( entry { SettingsScreenRoot( + setShowPaywall = { showPaywall = it }, modifier = modifier.padding( start = contentPadding.calculateStartPadding(layoutDirection), end = contentPadding.calculateEndPadding(layoutDirection), @@ -240,4 +246,5 @@ fun AppScreen( ) } } + if (showPaywall) 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..356c922 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 @@ -25,19 +26,26 @@ import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.togetherWith import androidx.compose.foundation.background +import androidx.compose.foundation.clickable 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.fillMaxSize 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.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.SliderState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -50,6 +58,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext @@ -80,6 +89,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 +112,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 +130,7 @@ fun SettingsScreenRoot( } SettingsScreen( + isPlus = isPlus, preferencesState = preferencesState, backStack = backStack, focusTimeInputFieldState = focusTimeInputFieldState, @@ -143,13 +155,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 +183,7 @@ private fun SettingsScreen( onAlarmSoundChanged: (Uri?) -> Unit, onThemeChange: (String) -> Unit, onColorSchemeChange: (Color) -> Unit, + setShowPaywall: (Boolean) -> Unit, modifier: Modifier = Modifier ) { val context = LocalContext.current @@ -217,6 +233,39 @@ private fun SettingsScreen( ) { item { Spacer(Modifier.height(12.dp)) } + if (!isPlus) item { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(CircleShape) + .background(colorScheme.primary) + .padding(16.dp) + .clickable { setShowPaywall(true) } + ) { + Icon( + painterResource(R.drawable.tomato_logo_notification), + null, + tint = colorScheme.onPrimary, + modifier = Modifier + .size(24.dp) + ) + Spacer(Modifier.width(8.dp)) + Text( + "Get Tomato+", + style = typography.titleLarge, + fontFamily = robotoFlexTopBar, + color = colorScheme.onPrimary + ) + Spacer(Modifier.weight(1f)) + Icon( + painterResource(R.drawable.arrow_forward_big), + null, + tint = colorScheme.onPrimary + ) + } + Spacer(Modifier.height(14.dp)) + } + item { AboutCard() } item { Spacer(Modifier.height(12.dp)) } 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..6f9041d 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,21 @@ 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 + private val _preferencesState = MutableStateFlow(PreferencesState()) val preferencesState = _preferencesState.asStateFlow() @@ -237,8 +241,10 @@ class SettingsViewModel( 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/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..94bfcb2 --- /dev/null +++ b/app/src/play/java/org/nsh07/pomodoro/billing/PlayBillingManager.kt @@ -0,0 +1,58 @@ +/* + * 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 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 + }, + onError = { error -> + Log.e("GooglePlayPaywallManager", "Error fetching customer info: $error") + } + ) + } +} + +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..1ecda02 --- /dev/null +++ b/app/src/play/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.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 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 9356ceb..85f21bf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ ksp = "2.2.20-2.0.4" lifecycleRuntimeKtx = "2.9.4" materialKolor = "3.0.1" navigation3 = "1.0.0-beta01" -purchases = "9.12.0" +revenuecat = "9.12.0" room = "2.8.3" vico = "2.2.1" @@ -40,7 +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" } -purchases = { module = "com.revenuecat.purchases:purchases", version.ref = "purchases" } +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] From 86bb382f4b645d2b556f1f09ea821884ca882034 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Mon, 27 Oct 2025 12:13:53 +0530 Subject: [PATCH 05/11] feat(billing): add font for billing screen, add animation --- app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt | 11 ++++++++++- .../pomodoro/ui/settingsScreen/SettingsScreen.kt | 5 +++-- app/src/main/res/values/strings.xml | 2 ++ .../nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt | 3 +-- app/src/play/res/font/roboto_flex.ttf | 0 5 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 app/src/play/res/font/roboto_flex.ttf 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 5455ad9..b7cab88 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 @@ -246,5 +248,12 @@ fun AppScreen( ) } } - if (showPaywall) TomatoPlusPaywallDialog(isPlus = isPlus) { showPaywall = false } + + 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 356c922..00b2de8 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 @@ -233,7 +233,7 @@ private fun SettingsScreen( ) { item { Spacer(Modifier.height(12.dp)) } - if (!isPlus) item { + item { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier @@ -251,7 +251,8 @@ private fun SettingsScreen( ) Spacer(Modifier.width(8.dp)) Text( - "Get Tomato+", + if (!isPlus) stringResource(R.string.get_plus) + else stringResource(R.string.app_name_plus), style = typography.titleLarge, fontFamily = robotoFlexTopBar, color = colorScheme.onPrimary diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f4c25b8..a71a0b4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -81,4 +81,6 @@ Sound Do Not Disturb Turn on DND when running a Focus timer + Tomato+ + Get Tomato+ \ 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 index 1ecda02..bd7b318 100644 --- a/app/src/play/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt +++ b/app/src/play/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt @@ -37,8 +37,7 @@ fun TomatoPlusPaywallDialog( onDismiss: () -> Unit ) { val paywallOptions = remember { - PaywallOptions.Builder(dismissRequest = onDismiss) - .build() + PaywallOptions.Builder(dismissRequest = onDismiss).build() } Scaffold { innerPadding -> diff --git a/app/src/play/res/font/roboto_flex.ttf b/app/src/play/res/font/roboto_flex.ttf new file mode 100644 index 0000000..e69de29 From 540b941b23ae37229a914ac6a59d8752db4ee6f2 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Mon, 27 Oct 2025 19:06:00 +0530 Subject: [PATCH 06/11] fix(billing): remove font to reduce install size --- app/src/play/res/font/roboto_flex.ttf | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/src/play/res/font/roboto_flex.ttf diff --git a/app/src/play/res/font/roboto_flex.ttf b/app/src/play/res/font/roboto_flex.ttf deleted file mode 100644 index e69de29..0000000 From a985e8d0fc7eda2e48318d8c0f515d3ce274988e Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Mon, 27 Oct 2025 20:27:09 +0530 Subject: [PATCH 07/11] fix(billing): adapt UI according to plus status --- .../billing/TomatoPlusPaywallDialog.kt | 57 +++++++++++++ .../java/org/nsh07/pomodoro/ui/AppScreen.kt | 1 + .../ui/settingsScreen/SettingsScreen.kt | 49 ++--------- .../ui/settingsScreen/components/AboutCard.kt | 8 +- .../ui/settingsScreen/components/PlusPromo.kt | 82 +++++++++++++++++++ .../pomodoro/ui/timerScreen/TimerScreen.kt | 5 +- 6 files changed, 159 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/PlusPromo.kt diff --git a/app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt b/app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt index 973041c..e406f57 100644 --- a/app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt +++ b/app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt @@ -17,11 +17,68 @@ 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.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( + "Tomato FOSS", + style = typography.headlineSmall, + fontFamily = robotoFlexTopBar, + color = colorScheme.onSurface + ) + Spacer(Modifier.height(8.dp)) + Text( + "All features are unlocked in this version. If my app made a difference in your life, please consider supporting me by donating on ${"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/main/java/org/nsh07/pomodoro/ui/AppScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt index b7cab88..16a539b 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt @@ -164,6 +164,7 @@ fun AppScreen( entry { TimerScreen( timerState = uiState, + isPlus = isPlus, progress = { progress }, onAction = { action -> when (action) { 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 00b2de8..af64b04 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 @@ -26,26 +26,19 @@ import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.togetherWith import androidx.compose.foundation.background -import androidx.compose.foundation.clickable 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.fillMaxSize 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.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme.colorScheme -import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.SliderState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -58,7 +51,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext @@ -76,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 @@ -233,44 +226,20 @@ private fun SettingsScreen( ) { item { Spacer(Modifier.height(12.dp)) } - item { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clip(CircleShape) - .background(colorScheme.primary) - .padding(16.dp) - .clickable { setShowPaywall(true) } - ) { - Icon( - painterResource(R.drawable.tomato_logo_notification), - null, - tint = colorScheme.onPrimary, - 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 = colorScheme.onPrimary - ) - Spacer(Modifier.weight(1f)) - Icon( - painterResource(R.drawable.arrow_forward_big), - null, - tint = colorScheme.onPrimary - ) - } + if (!isPlus) item { + PlusPromo(isPlus, setShowPaywall) Spacer(Modifier.height(14.dp)) } - item { AboutCard() } + 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 = { 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/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/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 }, {} ) From e4421ffdacb7eacf7321a73df302dba640fa6bcd Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Mon, 27 Oct 2025 20:45:16 +0530 Subject: [PATCH 08/11] feat(settings): reorder appearance settings, move strings to res Closes: #97 --- .../components/ColorSchemePickerListItem.kt | 95 +++++++++++-------- .../screens/AppearanceSettings.kt | 35 +++---- app/src/main/res/values/strings.xml | 2 + 3 files changed, 76 insertions(+), 56 deletions(-) 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..2587400 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,6 +17,7 @@ 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 @@ -28,6 +29,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 +41,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 @@ -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,48 @@ 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()) + }, + 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,7 +139,14 @@ fun ColorSchemePickerListItem( ) }, colors = listItemColors, - modifier = Modifier.clip(middleListItemShape) + modifier = Modifier.clip( + RoundedCornerShape( + topStart = middleListItemShape.topStart, + topEnd = middleListItemShape.topEnd, + zeroCorner, + zeroCorner + ) + ) ) Column( 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..e4f44bf 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 @@ -56,6 +56,7 @@ 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) @@ -100,24 +101,24 @@ 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 = 3, - index = 1, + index = 0, modifier = Modifier .clip(middleListItemShape) ) } + item { + ColorSchemePickerListItem( + color = preferencesState.colorScheme.toColor(), + items = 3, + index = 1, + onColorChange = onColorSchemeChange + ) + } item { val item = SettingsSwitchItem( checked = preferencesState.blackTheme, @@ -168,11 +169,13 @@ fun AppearanceSettings( @Composable fun AppearanceSettingsPreview() { val preferencesState = PreferencesState() - AppearanceSettings( - preferencesState = preferencesState, - onBlackThemeChange = {}, - onThemeChange = {}, - onColorSchemeChange = {}, - onBack = {} - ) + TomatoTheme { + AppearanceSettings( + preferencesState = preferencesState, + onBlackThemeChange = {}, + onThemeChange = {}, + onColorSchemeChange = {}, + onBack = {} + ) + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a71a0b4..d03d14c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -83,4 +83,6 @@ Turn on DND when running a Focus timer Tomato+ Get Tomato+ + Dynamic color + Adapt theme colors from your wallpaper \ No newline at end of file From c8d4114db54280f189d0130494196a8c610c945e Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Mon, 27 Oct 2025 21:15:09 +0530 Subject: [PATCH 09/11] feat(settings): add paywall restriction in Appearance settings --- .../billing/TomatoPlusPaywallDialog.kt | 5 +- .../ui/settingsScreen/SettingsScreen.kt | 2 + .../components/ColorSchemePickerListItem.kt | 32 +++++++----- .../settingsScreen/components/PlusDivider.kt | 51 +++++++++++++++++++ .../components/ThemePickerListItem.kt | 13 +++-- .../screens/AppearanceSettings.kt | 23 ++++++--- app/src/main/res/values/strings.xml | 2 + 7 files changed, 101 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/PlusDivider.kt diff --git a/app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt b/app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt index e406f57..6df4870 100644 --- a/app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt +++ b/app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt @@ -35,6 +35,7 @@ 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 @@ -63,14 +64,14 @@ fun TomatoPlusPaywallDialog( ) Spacer(Modifier.height(16.dp)) Text( - "Tomato FOSS", + stringResource(R.string.tomato_foss), style = typography.headlineSmall, fontFamily = robotoFlexTopBar, color = colorScheme.onSurface ) Spacer(Modifier.height(8.dp)) Text( - "All features are unlocked in this version. If my app made a difference in your life, please consider supporting me by donating on ${"BuyMeACoffee"}.", + stringResource(R.string.tomato_foss_desc, "BuyMeACoffee"), textAlign = TextAlign.Center, color = colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = 24.dp) 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 af64b04..2d6243a 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 @@ -285,9 +285,11 @@ private fun SettingsScreen( entry { AppearanceSettings( preferencesState = preferencesState, + isPlus = isPlus, onBlackThemeChange = onBlackThemeChange, onThemeChange = onThemeChange, onColorSchemeChange = onColorSchemeChange, + setShowPaywall = setShowPaywall, onBack = backStack::removeLastOrNull ) } 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 2587400..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 @@ -20,7 +20,6 @@ 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 @@ -60,6 +59,7 @@ fun ColorSchemePickerListItem( color: Color, items: Int, index: Int, + isPlus: Boolean, onColorChange: (Color) -> Unit, modifier: Modifier = Modifier ) { @@ -99,6 +99,7 @@ fun ColorSchemePickerListItem( if (it) onColorChange(colorSchemes.last()) else onColorChange(colorSchemes.first()) }, + enabled = isPlus, thumbContent = { if (checked) { Icon( @@ -149,21 +150,21 @@ fun ColorSchemePickerListItem( ) ) - 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) } } } @@ -175,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/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 e4f44bf..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,7 +56,6 @@ 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 @@ -63,9 +63,11 @@ import org.nsh07.pomodoro.utils.toColor @Composable fun AppearanceSettings( preferencesState: PreferencesState, + isPlus: Boolean, onBlackThemeChange: (Boolean) -> Unit, onThemeChange: (String) -> Unit, onColorSchemeChange: (Color) -> Unit, + setShowPaywall: (Boolean) -> Unit, onBack: () -> Unit, modifier: Modifier = Modifier ) { @@ -105,18 +107,22 @@ fun AppearanceSettings( ThemePickerListItem( theme = preferencesState.theme, onThemeChange = onThemeChange, - items = 3, - index = 0, - modifier = Modifier - .clip(middleListItemShape) + items = if (isPlus) 3 else 1, + index = 0 ) } + + if (!isPlus) { + item { PlusDivider(setShowPaywall) } + } + item { ColorSchemePickerListItem( color = preferencesState.colorScheme.toColor(), items = 3, - index = 1, - onColorChange = onColorSchemeChange + index = if (isPlus) 1 else 0, + isPlus = isPlus, + onColorChange = onColorSchemeChange, ) } item { @@ -137,6 +143,7 @@ fun AppearanceSettings( Switch( checked = item.checked, onCheckedChange = { item.onClick(it) }, + enabled = isPlus, thumbContent = { if (item.checked) { Icon( @@ -172,9 +179,11 @@ fun AppearanceSettingsPreview() { TomatoTheme { AppearanceSettings( preferencesState = preferencesState, + isPlus = false, onBlackThemeChange = {}, onThemeChange = {}, onColorSchemeChange = {}, + setShowPaywall = {}, onBack = {} ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d03d14c..79d3d99 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -85,4 +85,6 @@ 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 From 509bcdab6d2bccd7f22c3e7d705463de2b7b65e2 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Mon, 27 Oct 2025 21:32:37 +0530 Subject: [PATCH 10/11] feat(settings): add paywall restriction in Timer settings --- .../ui/settingsScreen/SettingsScreen.kt | 4 +- .../settingsScreen/screens/TimerSettings.kt | 65 +++++++++++++++++-- 2 files changed, 61 insertions(+), 8 deletions(-) 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 2d6243a..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 @@ -295,6 +295,7 @@ private fun SettingsScreen( } entry { TimerSettings( + isPlus = isPlus, aodEnabled = preferencesState.aodEnabled, dndEnabled = dndEnabled, focusTimeInputFieldState = focusTimeInputFieldState, @@ -303,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/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 From de6550d6ab94995eca1ea0b7e81787c2c3d76788 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Tue, 28 Oct 2025 11:01:29 +0530 Subject: [PATCH 11/11] feat(settings): reset settings when subscription expires --- .../pomodoro/billing/FossBillingManager.kt | 1 + .../java/org/nsh07/pomodoro/MainActivity.kt | 12 +++++ .../nsh07/pomodoro/billing/BillingManager.kt | 1 + .../viewModel/SettingsViewModel.kt | 53 +++++++++++++------ .../pomodoro/billing/PlayBillingManager.kt | 5 ++ 5 files changed, 55 insertions(+), 17 deletions(-) diff --git a/app/src/foss/java/org/nsh07/pomodoro/billing/FossBillingManager.kt b/app/src/foss/java/org/nsh07/pomodoro/billing/FossBillingManager.kt index 4a155a3..529b58c 100644 --- a/app/src/foss/java/org/nsh07/pomodoro/billing/FossBillingManager.kt +++ b/app/src/foss/java/org/nsh07/pomodoro/billing/FossBillingManager.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.asStateFlow */ class FossBillingManager : BillingManager { override val isPlus = MutableStateFlow(true).asStateFlow() + override val isLoaded = MutableStateFlow(true).asStateFlow() } object BillingManagerProvider { diff --git a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt index 4f34773..1917159 100644 --- a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt +++ b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt @@ -61,6 +61,18 @@ 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, diff --git a/app/src/main/java/org/nsh07/pomodoro/billing/BillingManager.kt b/app/src/main/java/org/nsh07/pomodoro/billing/BillingManager.kt index 66b1659..4cdc106 100644 --- a/app/src/main/java/org/nsh07/pomodoro/billing/BillingManager.kt +++ b/app/src/main/java/org/nsh07/pomodoro/billing/BillingManager.kt @@ -21,4 +21,5 @@ 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/ui/settingsScreen/viewModel/SettingsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt index 6f9041d..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 @@ -54,6 +54,10 @@ class SettingsViewModel( 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() @@ -94,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 } } @@ -235,6 +224,36 @@ 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 { diff --git a/app/src/play/java/org/nsh07/pomodoro/billing/PlayBillingManager.kt b/app/src/play/java/org/nsh07/pomodoro/billing/PlayBillingManager.kt index 94bfcb2..a658884 100644 --- a/app/src/play/java/org/nsh07/pomodoro/billing/PlayBillingManager.kt +++ b/app/src/play/java/org/nsh07/pomodoro/billing/PlayBillingManager.kt @@ -33,6 +33,9 @@ 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 { @@ -45,9 +48,11 @@ class PlayBillingManager : BillingManager { 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 } ) }