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]