docs: Revamp README and add APK naming convention This commit introduces a complete overhaul of the `README.md` to improve project presentation. Key changes include: * Adds a new header with the app icon and a Google Play badge. * Updates the feature graphic and includes a note distinguishing the OSS version from the Play Store version. * Refines the "Open Source Libraries" section to be more structured. * Moves the main preview image into the `docs` directory. Additionally, the `app/build.gradle.kts` file is updated to configure a standardized naming convention for output APK files, including flavor, version, and build type. * refactor copyright headers and fix README icon alignment This commit introduces two main changes across the codebase: The `spotless/copyright.kt` template have been updated for consistency: * The author name is simplified from "Episteme Authors" to "Episteme". * The contact email prefix is shortened from "Electronic mail:" to "mail:". Second, the `README.md` file is adjusted to correct the vertical alignment of the app icon with the project title.
729 lines
No EOL
31 KiB
Kotlin
729 lines
No EOL
31 KiB
Kotlin
/*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*
|
|
* 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") }
|
|
}
|
|
)
|
|
} |