/* * Episteme Reader - A native Android document reader. * Copyright (C) 2026 Episteme * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * mail: epistemereader@gmail.com */ package com.aryan.reader import android.app.Activity import timber.log.Timber import android.widget.Toast import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Info import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.launch import java.text.NumberFormat import java.util.Currency @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun ProScreen( viewModel: MainViewModel, onNavigateBack: () -> Unit ) { val context = LocalContext.current val proUpgradeState by viewModel.proUpgradeState.collectAsState() val uiState by viewModel.uiState.collectAsState() var showExistingPurchaseDialog by remember { mutableStateOf(false) } var showEarlyAccessInfoDialog by remember { mutableStateOf(false) } var showSignInRequiredDialog by remember { mutableStateOf(false) } val pagerState = rememberPagerState(initialPage = 1, pageCount = { 2 }) var selectedTabIndex by remember { mutableIntStateOf(1) } val scope = rememberCoroutineScope() LaunchedEffect(pagerState.currentPage) { selectedTabIndex = pagerState.currentPage } LaunchedEffect(selectedTabIndex) { scope.launch { pagerState.animateScrollToPage(selectedTabIndex) } } LaunchedEffect(uiState.isProUser) { if (uiState.isProUser) { selectedTabIndex = 1 } } LaunchedEffect(proUpgradeState.error) { proUpgradeState.error?.let { Toast.makeText(context, it, Toast.LENGTH_LONG).show() viewModel.clearBillingError() } } if (showExistingPurchaseDialog) { ExistingPurchaseDialog(onDismiss = { showExistingPurchaseDialog = false }) } if (showEarlyAccessInfoDialog) { EarlyAccessInfoDialog(onDismiss = { showEarlyAccessInfoDialog = false }) } if (showSignInRequiredDialog) { SignInRequiredDialog( onSignInClick = { scope.launch { context.findActivity()?.let { activity -> viewModel.signIn(activity) } } showSignInRequiredDialog = false }, onDismiss = { showSignInRequiredDialog = false } ) } Scaffold( topBar = { TopAppBar( title = { }, // Removed header content navigationIcon = { IconButton(onClick = onNavigateBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") } }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.background ) ) } ) { paddingValues -> Column( modifier = Modifier .fillMaxSize() .padding(top = paddingValues.calculateTopPadding(), start = 16.dp, end = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top ) { Spacer(modifier = Modifier.height(16.dp)) // Tabs TabRow( selectedTabIndex = selectedTabIndex, modifier = Modifier .fillMaxWidth() .clip(MaterialTheme.shapes.extraLarge) .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), indicator = {}, divider = {} ) { Tab( selected = selectedTabIndex == 0, onClick = { selectedTabIndex = 0 }, modifier = Modifier .height(56.dp) .clip(CircleShape) .background( if (selectedTabIndex == 0) MaterialTheme.colorScheme.surface else Color.Transparent ) .border( // Border for selected Free tab width = if (selectedTabIndex == 0) 2.dp else 0.dp, color = if (selectedTabIndex == 0) MaterialTheme.colorScheme.primary else Color.Transparent, shape = CircleShape ), text = { AutoSizeText( "Free", style = LocalTextStyle.current.copy( color = if (selectedTabIndex == 0) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, fontWeight = FontWeight.SemiBold ) ) }, selectedContentColor = MaterialTheme.colorScheme.primary, unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant ) Tab( selected = selectedTabIndex == 1, onClick = { selectedTabIndex = 1 }, modifier = Modifier .height(56.dp) .clip(CircleShape) .background( if (selectedTabIndex == 1) MaterialTheme.colorScheme.surface else Color.Transparent ) .border( // Border for selected Pro tab width = if (selectedTabIndex == 1) 2.dp else 0.dp, color = if (selectedTabIndex == 1) MaterialTheme.colorScheme.primary else Color.Transparent, shape = CircleShape ), text = { Row(verticalAlignment = Alignment.CenterVertically) { Icon( painter = painterResource(id = R.drawable.crown), contentDescription = "Pro", modifier = Modifier.size(16.dp), tint = if (selectedTabIndex == 1) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.width(4.dp)) AutoSizeText( "Episteme Pro", style = LocalTextStyle.current.copy( color = if (selectedTabIndex == 1) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, fontWeight = FontWeight.SemiBold ) ) } }, selectedContentColor = MaterialTheme.colorScheme.primary, unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant ) } Spacer(modifier = Modifier.height(16.dp)) HorizontalPager( state = pagerState, modifier = Modifier.fillMaxWidth().fillMaxHeight(), userScrollEnabled = true ) { page -> if (page == 0) { FreeTierCard() } else { ProTierCard( isProUser = uiState.isProUser, isUserSignedIn = uiState.currentUser != null, proUpgradeState = proUpgradeState, onUpgradeClick = { (context as? Activity)?.let { viewModel.launchPurchaseFlow(it) } }, onShowExistingPurchaseDialog = { showExistingPurchaseDialog = true }, onShowEarlyAccessInfo = { showEarlyAccessInfoDialog = true }, onSignInRequiredClick = { showSignInRequiredDialog = true } ) } } } } } @Composable private fun FreeTierCard() { Card( modifier = Modifier.fillMaxWidth().fillMaxHeight(), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface ) ) { Column( modifier = Modifier .padding(16.dp) .fillMaxWidth() .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally ) { Text( text = "Free Plan", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(8.dp)) Text( text = "$0", style = MaterialTheme.typography.displaySmall.copy(fontSize = 48.sp), fontWeight = FontWeight.Bold ) Text( text = "Forever free", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.height(24.dp)) Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start ) { FeatureListItem(iconRes = R.drawable.library_books, text = "Multiple Formats") Text( text = "Supports PDF, EPUB, MOBI, AZW3", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) ) FeatureListItem(iconRes = R.drawable.text_to_speech, text = "Android Text-to-Speech") Text( text = "Listen to your books with built-in TTS", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) ) FeatureListItem(iconRes = R.drawable.dictionary, text = "Basic Dictionary") Text( text = "Look up single words quickly", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) ) } Spacer(modifier = Modifier.height(16.dp)) Button( onClick = { /* Do nothing, it's the current plan */ }, modifier = Modifier .fillMaxWidth() .height(48.dp), shape = MaterialTheme.shapes.medium, enabled = false, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f), contentColor = MaterialTheme.colorScheme.onPrimaryContainer ) ) { Text("Current Plan", fontSize = 16.sp, fontWeight = FontWeight.SemiBold) } } } } @Composable private fun ProTierCard( isProUser: Boolean, isUserSignedIn: Boolean, proUpgradeState: ProUpgradeState, onUpgradeClick: () -> Unit, onShowExistingPurchaseDialog: () -> Unit, onShowEarlyAccessInfo: () -> Unit, onSignInRequiredClick: () -> Unit ) { val productDetails = proUpgradeState.productDetails val billingClientReady = proUpgradeState.billingClientReady val localPurchaseExistsForOtherAccount = !isProUser && proUpgradeState.hasValidPurchase var originalFormattedPrice by remember { mutableStateOf("$9.99") } LaunchedEffect(productDetails) { productDetails?.let { details -> val priceAmountMicros = details.priceAmountMicros val priceCurrencyCode = details.currencyCode val originalPriceMicros = priceAmountMicros * 2 val currencyFormatter = NumberFormat.getCurrencyInstance().apply { try { currency = Currency.getInstance(priceCurrencyCode) } catch (e: IllegalArgumentException) { Timber.e(e, "Invalid currency code: $priceCurrencyCode") } } originalFormattedPrice = currencyFormatter.format(originalPriceMicros / 1_000_000.0) } } Card( modifier = Modifier.fillMaxWidth().fillMaxHeight(), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface ) ) { Column( modifier = Modifier .padding(16.dp) .fillMaxWidth() .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally ) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( painter = painterResource(id = R.drawable.crown), contentDescription = "Pro Badge", modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.primary ) Spacer(modifier = Modifier.width(8.dp)) Text( text = "Episteme Pro", style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold ) } Spacer(modifier = Modifier.height(8.dp)) if (!isProUser) { val formattedPrice = productDetails?.formattedPrice if (formattedPrice != null) { Text( text = buildAnnotatedString { withStyle(style = SpanStyle(textDecoration = TextDecoration.LineThrough)) { append(originalFormattedPrice) } append(" 50% OFF") }, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.height(4.dp)) Text( text = formattedPrice, style = MaterialTheme.typography.displaySmall.copy(fontSize = 48.sp), fontWeight = FontWeight.Bold ) } else { Text( text = "Loading price...", style = MaterialTheme.typography.displaySmall.copy(fontSize = 32.sp), fontWeight = FontWeight.Bold ) } Row(verticalAlignment = Alignment.CenterVertically) { Text( text = "One-time payment", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.width(8.dp)) Surface( shape = MaterialTheme.shapes.extraSmall, color = MaterialTheme.colorScheme.primaryContainer, contentColor = MaterialTheme.colorScheme.onPrimaryContainer ) { Text( text = "Lifetime Access", style = MaterialTheme.typography.labelSmall, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) ) } } Spacer(modifier = Modifier.height(16.dp)) OutlinedButton( onClick = onShowEarlyAccessInfo, modifier = Modifier .height(40.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary), colors = ButtonDefaults.outlinedButtonColors( contentColor = MaterialTheme.colorScheme.primary ), shape = MaterialTheme.shapes.small, contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) ) { Icon( imageVector = Icons.Default.Info, contentDescription = "Info", modifier = Modifier.size(20.dp) ) Spacer(Modifier.size(ButtonDefaults.IconSpacing)) Text("Early Access Sale", style = MaterialTheme.typography.labelLarge) } Spacer(modifier = Modifier.height(16.dp)) } Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start ) { Text( text = "Everything in Free, plus:", style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(bottom = 8.dp) ) FeatureListItem(iconRes = R.drawable.cloud_sync, text = "Cloud Sync Across Devices") Text( text = "Keep your entire library, including book files and reading progress, synced across up to 4 devices.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) ) FeatureListItem(iconRes = R.drawable.summarize, text = "Summarization") Text( text = "Get quick summaries of chapters or pages", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) ) FeatureListItem(iconRes = R.drawable.dictionary, text = "Smart Dictionary") Text( text = "Search phrases and even paragraphs, not just single words", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) ) FeatureListItem(iconRes = R.drawable.chat_bubble, text = "Priority Feature Requests") Text( text = "Your suggestions get prioritized", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) ) } Spacer(modifier = Modifier.height(16.dp)) if (isProUser) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, modifier = Modifier .fillMaxWidth() .height(48.dp) .background( MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f), MaterialTheme.shapes.medium ) ) { Icon( painter = painterResource(id = R.drawable.crown), contentDescription = "Unlocked", tint = MaterialTheme.colorScheme.primary ) Spacer(modifier = Modifier.width(8.dp)) Text( text = "Pro Features Unlocked!", style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.primary ) } } else { when { !isUserSignedIn -> { Button( onClick = onSignInRequiredClick, modifier = Modifier .fillMaxWidth() .height(48.dp), shape = MaterialTheme.shapes.medium, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f), contentColor = MaterialTheme.colorScheme.onPrimary ) ) { AutoSizeText("Sign in Required", style = LocalTextStyle.current.copy(fontSize = 16.sp, fontWeight = FontWeight.SemiBold)) } } proUpgradeState.isVerifying -> { Button( onClick = {}, enabled = false, modifier = Modifier .fillMaxWidth() .height(48.dp), shape = MaterialTheme.shapes.medium, ) { CircularProgressIndicator( modifier = Modifier.size(24.dp), strokeWidth = 2.dp, color = LocalContentColor.current ) Spacer(Modifier.size(ButtonDefaults.IconSpacing)) Text("Verifying purchase...") } } localPurchaseExistsForOtherAccount -> { OutlinedButton( onClick = onShowExistingPurchaseDialog, modifier = Modifier .fillMaxWidth() .height(48.dp), shape = MaterialTheme.shapes.medium ) { Icon( imageVector = Icons.Default.Info, contentDescription = "Info", modifier = Modifier.size(20.dp) ) Spacer(Modifier.size(ButtonDefaults.IconSpacing)) AutoSizeText("Existing Purchase Found") } } productDetails != null -> { Button( onClick = { Timber.d("Upgrade button clicked.") onUpgradeClick() }, modifier = Modifier .fillMaxWidth() .height(48.dp), shape = MaterialTheme.shapes.medium, enabled = billingClientReady ) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( painter = painterResource(id = R.drawable.crown), contentDescription = "Pro", modifier = Modifier.size(20.dp) ) Spacer(Modifier.size(ButtonDefaults.IconSpacing)) AutoSizeText("Get Lifetime Access", style = LocalTextStyle.current.copy(fontSize = 16.sp, fontWeight = FontWeight.SemiBold)) } } } !billingClientReady -> { Box(modifier = Modifier.height(48.dp), contentAlignment = Alignment.Center) { CircularProgressIndicator() } } else -> { Text( text = "Upgrade currently unavailable. Please check your internet and try again.", style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.height(48.dp) ) } } // Text area below button Spacer(modifier = Modifier.height(16.dp)) // Increased spacing when { !isUserSignedIn -> { Text( text = "Please sign in to your Google account to purchase Episteme Pro.", style = MaterialTheme.typography.bodySmall, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant ) } proUpgradeState.isVerifying -> { Text( text = "This may take a few moments. Your Pro status will be updated automatically.", style = MaterialTheme.typography.bodySmall, textAlign = TextAlign.Center ) } else -> { LegalText(prefixText = "By purchasing,") } } } } } } @Composable private fun FeatureListItem(@androidx.annotation.DrawableRes iconRes: Int? = null, icon: ImageVector? = null, text: String) { Row( modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically ) { if (iconRes != null) { Icon( painter = painterResource(id = iconRes), contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(20.dp) ) } else if (icon != null) { Icon( imageVector = icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(20.dp) ) } else { Icon( imageVector = Icons.Default.Info, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(20.dp) ) } Spacer(modifier = Modifier.width(16.dp)) Text(text = text, style = MaterialTheme.typography.bodyLarge) } } @Composable fun ExistingPurchaseDialog(onDismiss: () -> Unit) { AlertDialog( onDismissRequest = onDismiss, icon = { Icon(Icons.Default.Info, contentDescription = null) }, title = { Text("Existing Purchase Found") }, text = { Text("This device already has a Pro purchase, but it's linked to a different account. Please sign in to the account that was used for the original purchase to restore your Pro features.") }, confirmButton = { TextButton(onClick = onDismiss) { Text("OK") } } ) } @Composable fun EarlyAccessInfoDialog(onDismiss: () -> Unit) { AlertDialog( onDismissRequest = onDismiss, icon = { Icon(Icons.Default.Info, contentDescription = null) }, title = { Text("Early Access Sale") }, text = { Text("You're getting Episteme Pro at a special discounted price during our early access period! This is a limited-time offer.") }, confirmButton = { TextButton(onClick = onDismiss) { Text("Got It!") } } ) } @Composable fun SignInRequiredDialog(onSignInClick: () -> Unit, onDismiss: () -> Unit) { AlertDialog( onDismissRequest = onDismiss, icon = { Icon(painter = painterResource(id = R.drawable.crown), contentDescription = null) }, // Using crown icon for Pro title = { Text("Sign In Required") }, text = { Text("Please sign in to your Google account to purchase Episteme Pro and unlock all premium features.") }, confirmButton = { TextButton(onClick = onSignInClick) { Text("Sign In") } }, dismissButton = { TextButton(onClick = onDismiss) { Text("Not Now") } } ) }