feat(billing): implement a simple RevenueCat billing system
Only for the Google Play version. The FOSS version remains free.
This commit is contained in:
@@ -116,7 +116,8 @@ dependencies {
|
|||||||
implementation(libs.androidx.room.ktx)
|
implementation(libs.androidx.room.ktx)
|
||||||
ksp(libs.androidx.room.compiler)
|
ksp(libs.androidx.room.compiler)
|
||||||
|
|
||||||
"playImplementation"(libs.purchases)
|
"playImplementation"(libs.revenuecat.purchases)
|
||||||
|
"playImplementation"(libs.revenuecat.purchases.ui)
|
||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
|
|
||||||
|
|||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.billing
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TomatoPlusPaywallDialog(
|
||||||
|
isPlus: Boolean,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.billing
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
fun initializePurchases(context: Context) {}
|
||||||
@@ -30,12 +30,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import org.nsh07.pomodoro.ui.AppScreen
|
import org.nsh07.pomodoro.ui.AppScreen
|
||||||
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
|
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
|
||||||
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
||||||
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
|
|
||||||
import org.nsh07.pomodoro.utils.toColor
|
import org.nsh07.pomodoro.utils.toColor
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
private val timerViewModel: TimerViewModel by viewModels(factoryProducer = { TimerViewModel.Factory })
|
|
||||||
private val settingsViewModel: SettingsViewModel by viewModels(factoryProducer = { SettingsViewModel.Factory })
|
private val settingsViewModel: SettingsViewModel by viewModels(factoryProducer = { SettingsViewModel.Factory })
|
||||||
|
|
||||||
private val appContainer by lazy {
|
private val appContainer by lazy {
|
||||||
@@ -62,6 +60,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
val seed = preferencesState.colorScheme.toColor()
|
val seed = preferencesState.colorScheme.toColor()
|
||||||
|
|
||||||
|
val isPlus by settingsViewModel.isPlus.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
TomatoTheme(
|
TomatoTheme(
|
||||||
darkTheme = darkTheme,
|
darkTheme = darkTheme,
|
||||||
seedColor = seed,
|
seedColor = seed,
|
||||||
@@ -73,7 +73,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AppScreen(
|
AppScreen(
|
||||||
timerViewModel = timerViewModel,
|
isPlus = isPlus,
|
||||||
isAODEnabled = preferencesState.aodEnabled,
|
isAODEnabled = preferencesState.aodEnabled,
|
||||||
setTimerFrequency = {
|
setTimerFrequency = {
|
||||||
appContainer.appTimerRepository.timerFrequency = it
|
appContainer.appTimerRepository.timerFrequency = it
|
||||||
|
|||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.nsh07.pomodoro
|
package org.nsh07.pomodoro
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
|
import org.nsh07.pomodoro.billing.initializePurchases
|
||||||
import org.nsh07.pomodoro.data.AppContainer
|
import org.nsh07.pomodoro.data.AppContainer
|
||||||
import org.nsh07.pomodoro.data.DefaultAppContainer
|
import org.nsh07.pomodoro.data.DefaultAppContainer
|
||||||
|
|
||||||
@@ -12,6 +30,8 @@ class TomatoApplication : Application() {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
container = DefaultAppContainer(this)
|
container = DefaultAppContainer(this)
|
||||||
|
|
||||||
|
initializePurchases(this)
|
||||||
|
|
||||||
val notificationChannel = NotificationChannel(
|
val notificationChannel = NotificationChannel(
|
||||||
"timer",
|
"timer",
|
||||||
getString(R.string.timer_progress),
|
getString(R.string.timer_progress),
|
||||||
|
|||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.billing
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
interface BillingManager {
|
||||||
|
val isPlus: StateFlow<Boolean>
|
||||||
|
}
|
||||||
@@ -26,6 +26,8 @@ import androidx.core.app.NotificationCompat
|
|||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import org.nsh07.pomodoro.R
|
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.service.addTimerActions
|
||||||
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
|
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
|
||||||
import org.nsh07.pomodoro.utils.millisecondsToStr
|
import org.nsh07.pomodoro.utils.millisecondsToStr
|
||||||
@@ -34,6 +36,7 @@ interface AppContainer {
|
|||||||
val appPreferenceRepository: AppPreferenceRepository
|
val appPreferenceRepository: AppPreferenceRepository
|
||||||
val appStatRepository: AppStatRepository
|
val appStatRepository: AppStatRepository
|
||||||
val appTimerRepository: AppTimerRepository
|
val appTimerRepository: AppTimerRepository
|
||||||
|
val billingManager: BillingManager
|
||||||
val notificationManager: NotificationManagerCompat
|
val notificationManager: NotificationManagerCompat
|
||||||
val notificationManagerService: NotificationManager
|
val notificationManagerService: NotificationManager
|
||||||
val notificationBuilder: NotificationCompat.Builder
|
val notificationBuilder: NotificationCompat.Builder
|
||||||
@@ -54,6 +57,8 @@ class DefaultAppContainer(context: Context) : AppContainer {
|
|||||||
|
|
||||||
override val appTimerRepository: AppTimerRepository by lazy { AppTimerRepository() }
|
override val appTimerRepository: AppTimerRepository by lazy { AppTimerRepository() }
|
||||||
|
|
||||||
|
override val billingManager: BillingManager by lazy { BillingManagerProvider.manager }
|
||||||
|
|
||||||
override val notificationManager: NotificationManagerCompat by lazy {
|
override val notificationManager: NotificationManagerCompat by lazy {
|
||||||
NotificationManagerCompat.from(context)
|
NotificationManagerCompat.from(context)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,8 +41,10 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
@@ -54,6 +56,7 @@ import androidx.navigation3.runtime.entryProvider
|
|||||||
import androidx.navigation3.runtime.rememberNavBackStack
|
import androidx.navigation3.runtime.rememberNavBackStack
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
import androidx.window.core.layout.WindowSizeClass
|
import androidx.window.core.layout.WindowSizeClass
|
||||||
|
import org.nsh07.pomodoro.billing.TomatoPlusPaywallDialog
|
||||||
import org.nsh07.pomodoro.service.TimerService
|
import org.nsh07.pomodoro.service.TimerService
|
||||||
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
|
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
|
||||||
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
|
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
|
||||||
@@ -65,10 +68,11 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
|
|||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AppScreen(
|
fun AppScreen(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory),
|
|
||||||
isAODEnabled: Boolean,
|
isAODEnabled: Boolean,
|
||||||
setTimerFrequency: (Float) -> Unit
|
isPlus: Boolean,
|
||||||
|
setTimerFrequency: (Float) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory)
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@@ -91,6 +95,7 @@ fun AppScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var showPaywall by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
@@ -218,6 +223,7 @@ fun AppScreen(
|
|||||||
|
|
||||||
entry<Screen.Settings.Main> {
|
entry<Screen.Settings.Main> {
|
||||||
SettingsScreenRoot(
|
SettingsScreenRoot(
|
||||||
|
setShowPaywall = { showPaywall = it },
|
||||||
modifier = modifier.padding(
|
modifier = modifier.padding(
|
||||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||||
@@ -240,4 +246,5 @@ fun AppScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (showPaywall) TomatoPlusPaywallDialog(isPlus = isPlus) { showPaywall = false }
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package org.nsh07.pomodoro.ui.settingsScreen
|
package org.nsh07.pomodoro.ui.settingsScreen
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
@@ -25,19 +26,26 @@ import androidx.compose.animation.slideInHorizontally
|
|||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
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.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.text.input.TextFieldState
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LocalTextStyle
|
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.SliderState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
@@ -50,6 +58,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -80,6 +89,7 @@ import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreenRoot(
|
fun SettingsScreenRoot(
|
||||||
|
setShowPaywall: (Boolean) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory)
|
viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory)
|
||||||
) {
|
) {
|
||||||
@@ -102,6 +112,7 @@ fun SettingsScreenRoot(
|
|||||||
viewModel.longBreakTimeTextFieldState
|
viewModel.longBreakTimeTextFieldState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isPlus by viewModel.isPlus.collectAsStateWithLifecycle()
|
||||||
val alarmEnabled by viewModel.alarmEnabled.collectAsStateWithLifecycle(true)
|
val alarmEnabled by viewModel.alarmEnabled.collectAsStateWithLifecycle(true)
|
||||||
val vibrateEnabled by viewModel.vibrateEnabled.collectAsStateWithLifecycle(true)
|
val vibrateEnabled by viewModel.vibrateEnabled.collectAsStateWithLifecycle(true)
|
||||||
val dndEnabled by viewModel.dndEnabled.collectAsStateWithLifecycle(false)
|
val dndEnabled by viewModel.dndEnabled.collectAsStateWithLifecycle(false)
|
||||||
@@ -119,6 +130,7 @@ fun SettingsScreenRoot(
|
|||||||
}
|
}
|
||||||
|
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
|
isPlus = isPlus,
|
||||||
preferencesState = preferencesState,
|
preferencesState = preferencesState,
|
||||||
backStack = backStack,
|
backStack = backStack,
|
||||||
focusTimeInputFieldState = focusTimeInputFieldState,
|
focusTimeInputFieldState = focusTimeInputFieldState,
|
||||||
@@ -143,13 +155,16 @@ fun SettingsScreenRoot(
|
|||||||
},
|
},
|
||||||
onThemeChange = viewModel::saveTheme,
|
onThemeChange = viewModel::saveTheme,
|
||||||
onColorSchemeChange = viewModel::saveColorScheme,
|
onColorSchemeChange = viewModel::saveColorScheme,
|
||||||
|
setShowPaywall = setShowPaywall,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("LocalContextGetResourceValueCall")
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsScreen(
|
private fun SettingsScreen(
|
||||||
|
isPlus: Boolean,
|
||||||
preferencesState: PreferencesState,
|
preferencesState: PreferencesState,
|
||||||
backStack: SnapshotStateList<Screen.Settings>,
|
backStack: SnapshotStateList<Screen.Settings>,
|
||||||
focusTimeInputFieldState: TextFieldState,
|
focusTimeInputFieldState: TextFieldState,
|
||||||
@@ -168,6 +183,7 @@ private fun SettingsScreen(
|
|||||||
onAlarmSoundChanged: (Uri?) -> Unit,
|
onAlarmSoundChanged: (Uri?) -> Unit,
|
||||||
onThemeChange: (String) -> Unit,
|
onThemeChange: (String) -> Unit,
|
||||||
onColorSchemeChange: (Color) -> Unit,
|
onColorSchemeChange: (Color) -> Unit,
|
||||||
|
setShowPaywall: (Boolean) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -217,6 +233,39 @@ private fun SettingsScreen(
|
|||||||
) {
|
) {
|
||||||
item { Spacer(Modifier.height(12.dp)) }
|
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 { AboutCard() }
|
||||||
|
|
||||||
item { Spacer(Modifier.height(12.dp)) }
|
item { Spacer(Modifier.height(12.dp)) }
|
||||||
|
|||||||
@@ -40,17 +40,21 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.nsh07.pomodoro.TomatoApplication
|
import org.nsh07.pomodoro.TomatoApplication
|
||||||
|
import org.nsh07.pomodoro.billing.BillingManager
|
||||||
import org.nsh07.pomodoro.data.AppPreferenceRepository
|
import org.nsh07.pomodoro.data.AppPreferenceRepository
|
||||||
import org.nsh07.pomodoro.data.TimerRepository
|
import org.nsh07.pomodoro.data.TimerRepository
|
||||||
import org.nsh07.pomodoro.ui.Screen
|
import org.nsh07.pomodoro.ui.Screen
|
||||||
|
|
||||||
@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
|
@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
|
||||||
class SettingsViewModel(
|
class SettingsViewModel(
|
||||||
|
private val billingManager: BillingManager,
|
||||||
private val preferenceRepository: AppPreferenceRepository,
|
private val preferenceRepository: AppPreferenceRepository,
|
||||||
private val timerRepository: TimerRepository,
|
private val timerRepository: TimerRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main)
|
val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main)
|
||||||
|
|
||||||
|
val isPlus = billingManager.isPlus
|
||||||
|
|
||||||
private val _preferencesState = MutableStateFlow(PreferencesState())
|
private val _preferencesState = MutableStateFlow(PreferencesState())
|
||||||
val preferencesState = _preferencesState.asStateFlow()
|
val preferencesState = _preferencesState.asStateFlow()
|
||||||
|
|
||||||
@@ -237,8 +241,10 @@ class SettingsViewModel(
|
|||||||
val application = (this[APPLICATION_KEY] as TomatoApplication)
|
val application = (this[APPLICATION_KEY] as TomatoApplication)
|
||||||
val appPreferenceRepository = application.container.appPreferenceRepository
|
val appPreferenceRepository = application.container.appPreferenceRepository
|
||||||
val appTimerRepository = application.container.appTimerRepository
|
val appTimerRepository = application.container.appTimerRepository
|
||||||
|
val appBillingManager = application.container.billingManager
|
||||||
|
|
||||||
SettingsViewModel(
|
SettingsViewModel(
|
||||||
|
billingManager = appBillingManager,
|
||||||
preferenceRepository = appPreferenceRepository,
|
preferenceRepository = appPreferenceRepository,
|
||||||
timerRepository = appTimerRepository,
|
timerRepository = appTimerRepository,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ ksp = "2.2.20-2.0.4"
|
|||||||
lifecycleRuntimeKtx = "2.9.4"
|
lifecycleRuntimeKtx = "2.9.4"
|
||||||
materialKolor = "3.0.1"
|
materialKolor = "3.0.1"
|
||||||
navigation3 = "1.0.0-beta01"
|
navigation3 = "1.0.0-beta01"
|
||||||
purchases = "9.12.0"
|
revenuecat = "9.12.0"
|
||||||
room = "2.8.3"
|
room = "2.8.3"
|
||||||
vico = "2.2.1"
|
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" }
|
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
material-kolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" }
|
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" }
|
vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
|||||||
Reference in New Issue
Block a user