feat(billing): implement a simple RevenueCat billing system

Only for the Google Play version. The FOSS version remains free.
This commit is contained in:
Nishant Mishra
2025-10-27 11:31:07 +05:30
parent 7aed88d1c5
commit 16b4a9f125
15 changed files with 354 additions and 9 deletions

View File

@@ -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)

View File

@@ -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()
}

View File

@@ -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
) {
}

View File

@@ -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) {}

View File

@@ -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

View File

@@ -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),

View File

@@ -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>
}

View File

@@ -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)
} }

View File

@@ -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 }
} }

View File

@@ -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)) }

View File

@@ -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,
) )

View File

@@ -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()
}

View File

@@ -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)
}
}
}

View File

@@ -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()
)
}

View File

@@ -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]