Add payment menu

This commit is contained in:
Aleksandras Kostarevas 2024-04-28 21:52:42 -04:00
parent 5c9bada7ae
commit 7d5b12feaf
15 changed files with 842 additions and 13 deletions

View File

@ -169,5 +169,27 @@
android:value="Neural network model training"/>
</service>
<activity
android:name=".payment.PaymentCompleteActivity"
android:exported="true"
android:label="@string/payment_complete"
android:clearTaskOnLaunch="false"
android:launchMode="singleInstance">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="futo-voice-input" />
<data android:scheme="futo-keyboard" />
</intent-filter>
</activity>
<activity
android:name=".payment.PaymentActivity"
android:exported="false"
android:label="@string/payment"
android:clearTaskOnLaunch="false"
android:launchMode="singleInstance" />
</application>
</manifest>

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,1L12,23"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M17,5H9.5a3.5,3.5 0,0 0,0 7h5a3.5,3.5 0,0 1,0 7H6"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -45,4 +45,31 @@
<string name="try_typing">Try typing here…</string>
<string name="externally_imported_model">Externally imported model</string>
<string name="spacebar_default_text">Pre-Alpha</string>
<string name="payment_text_1">You\'ve been using FUTO Keyboard for <xliff:g name="days" example="30">%d</xliff:g> days. If you find this app useful, please consider paying to support future development of FUTO software.</string>
<string name="payment_text_1_alt">Thank you for trying out FUTO Keyboard! If you find this app useful, please consider paying to support future development of FUTO software.</string>
<string name="payment_text_2">FUTO is dedicated to making good software that doesn\'t abuse you. This app will never serve you ads or collect your personal data.</string>
<string name="thank_you_for_purchasing_keyboard">Thank you for purchasing FUTO Keyboard!</string>
<string name="payment_pending_body">Your payment is still pending, but it should clear soon.</string>
<string name="purchase_will_help_body">Your purchase will help continued development of FUTO Keyboard, and other FUTO projects.</string>
<string name="thank_you">Thank you</string>
<string name="payment_pending">Payment Pending</string>
<string name="payment_failed_body">Unfortunately, your payment has failed for one reason or another. Please contact us if you need help. You can reach us at keyboard@futo.org, with the button below, or with Send Feedback in Help</string>
<string name="payment_failed_body_ex">There may have been a problem with your payment. If it did go through, you should receive an email with a license key. Please contact us if you need help. You can reach us at keyboard@futo.org, with the button below, or with Share Feedback at the main screen.</string>
<string name="pay_via_x">Pay via <xliff:g name="paymentMethod" example="Play Store">%s</xliff:g></string>
<string name="pay">Pay</string>
<string name="i_already_paid">I already paid</string>
<string name="i_already_paid_2">I already paid (tap again)</string>
<string name="remind_me_in_x">"Remind me in "</string>
<string name="in_x_days">" days"</string>
<string name="developer_mode_payment_methods">You are on the Developer release, so you are seeing all payment methods</string>
<string name="pay_now">Pay Now</string>
<string name="unpaid_indicator">Unpaid</string>
<string name="unpaid_title">Unpaid FUTO Keyboard</string>
<string name="payment_error">Payment Error</string>
<string name="payment_complete">Payment Complete</string>
<string name="payment_title">Payment</string>
<string name="payment">Payment</string>
<string name="license_check_failed">Failed to verify license. If you believe this to be an error, please contact us.</string>
<string name="thank_you_for_using_paid">Thank you for using the paid version of FUTO Keyboard!</string>
</resources>

View File

@ -0,0 +1,43 @@
package org.futo.inputmethod.latin.payment
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
import org.futo.inputmethod.latin.uix.settings.pages.PaymentScreenSwitch
import org.futo.inputmethod.latin.uix.theme.UixThemeAuto
class PaymentActivity : ComponentActivity() {
private fun updateContent() {
setContent {
UixThemeAuto {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
PaymentScreenSwitch(onExit = {
finish()
})
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
updateContent()
}
}
}
}

View File

@ -0,0 +1,109 @@
package org.futo.inputmethod.latin.payment
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.datastore.preferences.core.edit
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.uix.dataStore
import org.futo.inputmethod.latin.uix.getSetting
import org.futo.inputmethod.latin.uix.settings.NavigationItem
import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle
import org.futo.inputmethod.latin.uix.settings.SettingsActivity
import org.futo.inputmethod.latin.uix.settings.pages.EXT_LICENSE_KEY
import org.futo.inputmethod.latin.uix.settings.pages.IS_ALREADY_PAID
import org.futo.inputmethod.latin.uix.settings.pages.IS_PAYMENT_PENDING
import org.futo.inputmethod.latin.uix.settings.pages.PaymentThankYouScreen
import org.futo.inputmethod.latin.uix.settings.pages.startAppActivity
import org.futo.inputmethod.latin.uix.theme.UixThemeAuto
import org.futo.inputmethod.updates.openURI
class PaymentCompleteActivity : ComponentActivity() {
private fun updateContent() {
setContent {
UixThemeAuto {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
PaymentThankYouScreen(onExit = {
startAppActivity(SettingsActivity::class.java, clearTop = true)
finish()
})
}
}
}
}
private fun onPaid(license: String) {
lifecycleScope.launch {
dataStore.edit {
it[IS_ALREADY_PAID.key] = true
it[IS_PAYMENT_PENDING.key] = false
it[EXT_LICENSE_KEY.key] = license
}
repeatOnLifecycle(Lifecycle.State.STARTED) {
updateContent()
}
}
}
private fun onInvalidKey() {
lifecycleScope.launch {
if(applicationContext.getSetting(IS_ALREADY_PAID)) {
finish()
} else {
setContent {
UixThemeAuto {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column {
Text(
getString(R.string.license_check_failed),
modifier = Modifier.padding(8.dp)
)
NavigationItem(title = "Email keyboard@futo.org", style = NavigationItemStyle.Mail, navigate = {
openURI("mailto:keyboard@futo.org")
})
}
}
}
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val targetData = intent.dataString
if((targetData?.startsWith("futo-keyboard://license/") == true) || (targetData?.startsWith("futo-voice-input://license/") == true)) {
/*if(StatePayment.instance.setPaymentLicenseUrl(targetData)) {
onPaid(targetData)
} else {
onInvalidKey()
}*/
TODO()
} else {
Log.e("PaymentCompleteActivity", "futo-keyboard launched with invalid targetData $targetData")
finish()
}
}
}

View File

@ -22,6 +22,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material.icons.filled.Send
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
@ -397,7 +398,8 @@ enum class NavigationItemStyle {
HomeSecondary,
HomeTertiary,
MiscNoArrow,
Misc
Misc,
Mail
}
@Composable
@ -412,15 +414,19 @@ fun NavigationItem(title: String, style: NavigationItemStyle, navigate: () -> Un
NavigationItemStyle.HomePrimary -> MaterialTheme.colorScheme.primaryContainer
NavigationItemStyle.HomeSecondary -> MaterialTheme.colorScheme.secondaryContainer
NavigationItemStyle.HomeTertiary -> MaterialTheme.colorScheme.tertiaryContainer
NavigationItemStyle.MiscNoArrow -> Color.Transparent
NavigationItemStyle.Misc -> Color.Transparent
NavigationItemStyle.MiscNoArrow,
NavigationItemStyle.Misc,
NavigationItemStyle.Mail -> Color.Transparent
}
val iconColor = when(style) {
NavigationItemStyle.HomePrimary -> MaterialTheme.colorScheme.onPrimaryContainer
NavigationItemStyle.HomeSecondary -> MaterialTheme.colorScheme.onSecondaryContainer
NavigationItemStyle.HomeTertiary -> MaterialTheme.colorScheme.onTertiaryContainer
NavigationItemStyle.MiscNoArrow -> MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f)
NavigationItemStyle.MiscNoArrow,
NavigationItemStyle.Mail,
NavigationItemStyle.Misc -> MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f)
}
@ -440,6 +446,7 @@ fun NavigationItem(title: String, style: NavigationItemStyle, navigate: () -> Un
) {
when(style) {
NavigationItemStyle.Misc -> Icon(Icons.Default.ArrowForward, contentDescription = "Go")
NavigationItemStyle.Mail -> Icon(Icons.Default.Send, contentDescription = "Send")
else -> {}
}
}

View File

@ -24,13 +24,14 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.futo.inputmethod.latin.uix.SettingsKey
import org.futo.inputmethod.latin.uix.dataStore
import org.futo.inputmethod.latin.uix.getSetting
data class DataStoreItem<T>(val value: T, val setValue: (T) -> Job)
@Composable
fun <T> useDataStoreValueNullable(key: Preferences.Key<T>, default: T): T? {
fun <T> useDataStoreValueNullable(key: Preferences.Key<T>, default: T): T {
val context = LocalContext.current
val initialValue = remember {
@ -48,6 +49,11 @@ fun <T> useDataStoreValueNullable(key: Preferences.Key<T>, default: T): T? {
return valueFlow.collectAsState(initial = initialValue).value
}
@Composable
fun <T> useDataStoreValueNullable(v: SettingsKey<T>): T {
return useDataStoreValueNullable(key = v.key, default = v.default)
}
@Composable
fun <T> useDataStore(key: Preferences.Key<T>, default: T): DataStoreItem<T> {
val context = LocalContext.current
@ -72,6 +78,12 @@ fun <T> useDataStore(key: Preferences.Key<T>, default: T): DataStoreItem<T> {
return DataStoreItem(value, setValue)
}
@Composable
fun <T> useDataStore(key: SettingsKey<T>): DataStoreItem<T> {
return useDataStore(key.key, key.default)
}
@Composable
fun useSharedPrefsBool(key: String, default: Boolean): DataStoreItem<Boolean> {
val coroutineScope = rememberCoroutineScope()

View File

@ -17,6 +17,8 @@ import org.futo.inputmethod.latin.uix.settings.pages.DeveloperScreen
import org.futo.inputmethod.latin.uix.settings.pages.HelpScreen
import org.futo.inputmethod.latin.uix.settings.pages.HomeScreen
import org.futo.inputmethod.latin.uix.settings.pages.LanguagesScreen
import org.futo.inputmethod.latin.uix.settings.pages.PaymentScreen
import org.futo.inputmethod.latin.uix.settings.pages.PaymentThankYouScreen
import org.futo.inputmethod.latin.uix.settings.pages.PredictiveTextScreen
import org.futo.inputmethod.latin.uix.settings.pages.ThemeScreen
import org.futo.inputmethod.latin.uix.settings.pages.TypingScreen
@ -54,6 +56,8 @@ fun SettingsNavigator(
composable("help") { HelpScreen(navController) }
composable("developer") { DeveloperScreen(navController) }
composable("blacklist") { BlacklistScreen(navController) }
composable("payment") { PaymentScreen(navController) { navController.navigateUp() } }
composable("paid") { PaymentThankYouScreen { navController.navigateUp() } }
dialog("error/{title}/{body}") {
ErrorDialog(
it.arguments?.getString("title")?.urlDecode() ?: stringResource(R.string.unknown_error),

View File

@ -17,7 +17,7 @@ fun AdvancedParametersScreen(navController: NavHostController = rememberNavContr
ScrollableList {
ScreenTitle("Advanced Parameters", showBack = true, navController)
Tip("Options below are experimental and may be removed or changed in the future as internal workings of the app change. Changing these values may have an adverse impact on your experience.")
Tip("Options below are experimental and may be removed or changed in the future as internal workings of the app change. Changing these values may have an adverse impact on your experience.\n\nNote: These only affect English")
SettingSlider(
title = "Transformer LM strength",

View File

@ -21,6 +21,8 @@ import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle
import org.futo.inputmethod.latin.uix.settings.ScreenTitle
import org.futo.inputmethod.latin.uix.settings.ScrollableList
import org.futo.inputmethod.latin.uix.settings.SettingToggleDataStore
import org.futo.inputmethod.latin.uix.settings.SettingToggleRaw
import org.futo.inputmethod.latin.uix.settings.useDataStore
val IS_DEVELOPER = booleanPreferencesKey("isDeveloperMode")
@ -53,5 +55,49 @@ fun DeveloperScreen(navController: NavHostController = rememberNavController())
},
icon = painterResource(id = R.drawable.close)
)
ScreenTitle(title = "Payment stuff")
SettingToggleDataStore(title = "Is paid", setting = IS_ALREADY_PAID)
SettingToggleDataStore(title = "Is payment pending", setting = IS_PAYMENT_PENDING)
SettingToggleDataStore(title = "Has seen paid notice", setting = HAS_SEEN_PAID_NOTICE)
SettingToggleDataStore(title = "Force show notice", setting = FORCE_SHOW_NOTICE)
val reminder = useDataStore(NOTICE_REMINDER_TIME)
val currTime = System.currentTimeMillis() / 1000L
val subtitleValue = if (reminder.value > currTime) {
val diffDays = (reminder.value - currTime) / 60.0 / 60.0 / 24.0
"Reminding in ${"%.2f".format(diffDays)} days"
} else {
"Reminder unset"
}
SettingToggleRaw(
"Reminder Time",
reminder.value > currTime,
{
if (!it) {
reminder.setValue(0L)
}
},
subtitleValue,
reminder.value <= currTime,
{ }
)
val licenseKey = useDataStore(EXT_LICENSE_KEY)
SettingToggleRaw(
"Ext License Key",
licenseKey.value != EXT_LICENSE_KEY.default,
{
if(!it) {
licenseKey.setValue(EXT_LICENSE_KEY.default)
}
},
licenseKey.value,
licenseKey.value == EXT_LICENSE_KEY.default,
{ }
)
}
}

View File

@ -78,7 +78,7 @@ fun HelpScreen(navController: NavHostController = rememberNavController()) {
NavigationItem(title = "FUTO Chat", style = NavigationItemStyle.Misc, navigate = {
context.openURI("https://chat.futo.org/")
})
NavigationItem(title = "Email keyboard@futo.org", style = NavigationItemStyle.Misc, navigate = {
NavigationItem(title = "Email keyboard@futo.org", style = NavigationItemStyle.Mail, navigate = {
context.openURI("mailto:keyboard@futo.org")
})
}

View File

@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -28,7 +29,7 @@ import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.uix.settings.NavigationItem
import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle
import org.futo.inputmethod.latin.uix.settings.ScreenTitle
import org.futo.inputmethod.latin.uix.settings.useDataStore
import org.futo.inputmethod.latin.uix.settings.useDataStoreValueNullable
import org.futo.inputmethod.latin.uix.theme.Typography
import org.futo.inputmethod.updates.ConditionalUpdate
@ -58,7 +59,9 @@ fun AndroidTextInput() {
fun HomeScreen(navController: NavHostController = rememberNavController()) {
val context = LocalContext.current
val scrollState = rememberScrollState()
val isDeveloper = useDataStore(key = IS_DEVELOPER, default = false)
val isDeveloper = useDataStoreValueNullable(key = IS_DEVELOPER, default = false)
val isPaid = useDataStoreValueNullable(IS_ALREADY_PAID)
Column {
Column(
@ -71,6 +74,7 @@ fun HomeScreen(navController: NavHostController = rememberNavController()) {
ScreenTitle("FUTO Keyboard Settings")
ConditionalUpdate(navController)
ConditionalUnpaidNoticeWithNav(navController)
NavigationItem(
title = "Languages",
@ -107,6 +111,16 @@ fun HomeScreen(navController: NavHostController = rememberNavController()) {
icon = painterResource(id = R.drawable.eye)
)
UnpaidNoticeCondition(showOnlyIfReminder = true) {
NavigationItem(
title = stringResource(R.string.payment),
style = NavigationItemStyle.HomePrimary,
navigate = { navController.navigate("payment") },
icon = painterResource(R.drawable.dollar_sign)
)
}
NavigationItem(
title = "Help & Feedback",
style = NavigationItemStyle.HomePrimary,
@ -114,7 +128,7 @@ fun HomeScreen(navController: NavHostController = rememberNavController()) {
icon = painterResource(id = R.drawable.help_circle)
)
if(isDeveloper.value || LocalInspectionMode.current) {
if(isDeveloper || LocalInspectionMode.current) {
NavigationItem(
title = "Developer Settings",
style = NavigationItemStyle.HomeSecondary,
@ -124,7 +138,18 @@ fun HomeScreen(navController: NavHostController = rememberNavController()) {
}
Spacer(modifier = Modifier.height(32.dp))
Spacer(modifier = Modifier.height(16.dp))
if(isPaid || LocalInspectionMode.current) {
Text(
stringResource(R.string.thank_you_for_using_paid),
style = Typography.bodyMedium,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(12.dp))
}
Text(
"v${BuildConfig.VERSION_NAME}",
style = Typography.labelSmall,

View File

@ -0,0 +1,483 @@
package org.futo.inputmethod.latin.uix.settings.pages
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableIntState
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment.Companion.Center
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.NavOptions
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.launch
import org.futo.inputmethod.latin.BuildConfig
import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.payment.PaymentActivity
import org.futo.inputmethod.latin.uix.SettingsKey
import org.futo.inputmethod.latin.uix.setSetting
import org.futo.inputmethod.latin.uix.settings.NavigationItem
import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle
import org.futo.inputmethod.latin.uix.settings.ScreenTitle
import org.futo.inputmethod.latin.uix.settings.ScrollableList
import org.futo.inputmethod.latin.uix.settings.useDataStore
import org.futo.inputmethod.latin.uix.settings.useDataStoreValueNullable
import org.futo.inputmethod.latin.uix.theme.Typography
import org.futo.inputmethod.updates.openURI
import kotlin.math.absoluteValue
val IS_ALREADY_PAID = SettingsKey(booleanPreferencesKey("already_paid"), false)
val IS_PAYMENT_PENDING = SettingsKey(booleanPreferencesKey("payment_pending"), false)
val HAS_SEEN_PAID_NOTICE = SettingsKey(booleanPreferencesKey("seen_paid_notice"), false)
val FORCE_SHOW_NOTICE = SettingsKey(booleanPreferencesKey("force_show_notice"), false)
val NOTICE_REMINDER_TIME = SettingsKey(longPreferencesKey("notice_reminder_time"), 0L)
val EXT_LICENSE_KEY = SettingsKey(stringPreferencesKey("license_key"), "")
fun <T> Context.startAppActivity(activity: Class<T>, clearTop: Boolean = false) {
val intent = Intent(this, activity)
if(this !is Activity) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
if(clearTop) {
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
startActivity(intent)
}
@Composable
fun useNumberOfDaysInstalled(): MutableIntState {
if (LocalInspectionMode.current) {
return remember { mutableIntStateOf(55) }
}
val dayCount = remember { mutableIntStateOf(-1) }
val context = LocalContext.current
LaunchedEffect(Unit) {
val packageManager = context.packageManager
val packageInfo = packageManager.getPackageInfo(context.packageName, 0)
val firstInstallTime = packageInfo.firstInstallTime
val currentTime = System.currentTimeMillis()
val diff = (currentTime - firstInstallTime) / (1000 * 60 * 60 * 24)
dayCount.intValue = diff.toInt()
}
return dayCount
}
@Composable
fun ParagraphText(it: String) {
Text(it, modifier = Modifier.padding(16.dp, 8.dp), style = Typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground)
}
@Composable
fun PaymentText() {
val numDaysInstalled = useNumberOfDaysInstalled()
// Doesn't make sense to say "You've been using for ... days" if it's less than seven days
if(numDaysInstalled.intValue >= 7) {
ParagraphText(stringResource(R.string.payment_text_1, numDaysInstalled.value))
} else {
ParagraphText(stringResource(R.string.payment_text_1_alt))
}
ParagraphText(stringResource(R.string.payment_text_2))
}
suspend fun pushNoticeReminderTime(context: Context, days: Float) {
// If the user types in a crazy high number, the long can't store such a large value and it won't suppress the reminder
// 21x the age of the universe ought to be enough for a payment notice reminder
// Also take the absolute value in the case of a negative number
val clampedDays = if (days.absoluteValue >= 1.06751991E14f) {
1.06751991E14f
} else {
days.absoluteValue
}
context.setSetting(NOTICE_REMINDER_TIME,
System.currentTimeMillis() / 1000L + (clampedDays * 60.0 * 60.0 * 24.0).toLong())
}
const val TRIAL_PERIOD_DAYS = 30
@Composable
fun UnpaidNoticeCondition(
force: Boolean = LocalInspectionMode.current,
showOnlyIfReminder: Boolean = false,
inner: @Composable () -> Unit
) {
val numDaysInstalled = useNumberOfDaysInstalled()
val forceShowNotice = useDataStoreValueNullable(FORCE_SHOW_NOTICE)
val isAlreadyPaid = useDataStoreValueNullable(IS_ALREADY_PAID)
val pushReminderTime = useDataStoreValueNullable(NOTICE_REMINDER_TIME)
val currentTime = System.currentTimeMillis() / 1000L
val reminderTimeIsUp = (currentTime >= pushReminderTime)
val displayCondition = if(showOnlyIfReminder) {
// Either the reminder time is not up, or we're not past the trial period
(!isAlreadyPaid) && ((!reminderTimeIsUp) || (!forceShowNotice && numDaysInstalled.intValue < TRIAL_PERIOD_DAYS))
} else {
// The trial period time is over
(forceShowNotice || (numDaysInstalled.intValue >= TRIAL_PERIOD_DAYS))
// and the current time is past the reminder time
&& reminderTimeIsUp
// and we have not already paid
&& (!isAlreadyPaid)
}
if (force || displayCondition) {
inner()
}
}
@Composable
@Preview
fun ConditionalUnpaidNoticeInVoiceInputWindow(onClose: (() -> Unit)? = null) {
val context = LocalContext.current
UnpaidNoticeCondition {
TextButton(onClick = {
context.startAppActivity(PaymentActivity::class.java)
if (onClose != null) onClose()
}) {
Text(stringResource(R.string.unpaid_indicator), color = MaterialTheme.colorScheme.onSurface)
}
}
}
@Composable
@Preview
fun UnpaidNotice(onPay: () -> Unit = { }, onAlreadyPaid: () -> Unit = { }) {
Surface(
color = MaterialTheme.colorScheme.surfaceVariant, modifier = Modifier
.fillMaxWidth()
.padding(24.dp, 8.dp), shape = RoundedCornerShape(4.dp)
) {
Column(modifier = Modifier.padding(8.dp, 0.dp)) {
Text(
stringResource(R.string.unpaid_title),
modifier = Modifier.padding(8.dp),
style = Typography.titleMedium,
color = MaterialTheme.colorScheme.onBackground
)
PaymentText()
Row(
modifier = Modifier
.padding(8.dp)
.align(CenterHorizontally)
) {
Box(modifier = Modifier.weight(1.0f)) {
Button(onClick = onPay, modifier = Modifier.align(Center)) {
Text(stringResource(R.string.pay_now))
}
}
Box(modifier = Modifier.weight(1.0f)) {
Button(
onClick = onAlreadyPaid, colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary,
contentColor = MaterialTheme.colorScheme.onSecondary
), modifier = Modifier.align(Center)
) {
Text(stringResource(R.string.i_already_paid))
}
}
}
}
}
}
@Composable
@Preview
fun ConditionalUnpaidNoticeWithNav(navController: NavController = rememberNavController()) {
UnpaidNoticeCondition {
UnpaidNotice(onPay = {
navController.navigate("payment")
}, onAlreadyPaid = {
navController.navigate("payment")
})
}
}
@Composable
@Preview
fun PaymentThankYouScreen(onExit: () -> Unit = { }) {
val hasSeenPaidNotice = useDataStore(HAS_SEEN_PAID_NOTICE)
val isPending = useDataStore(IS_PAYMENT_PENDING)
ScrollableList {
ScreenTitle(
if (isPending.value) {
stringResource(R.string.payment_pending)
} else {
stringResource(R.string.thank_you)
},
showBack = false
)
ParagraphText(stringResource(R.string.thank_you_for_purchasing_keyboard))
if (isPending.value) {
ParagraphText(stringResource(R.string.payment_pending_body))
}
ParagraphText(stringResource(R.string.purchase_will_help_body))
Box(modifier = Modifier
.fillMaxWidth()
.padding(16.dp, 8.dp)) {
Button(
onClick = {
hasSeenPaidNotice.setValue(true)
onExit()
},
modifier = Modifier
.align(Center)
.fillMaxWidth()
) {
Text(stringResource(R.string.continue_))
}
}
}
}
@Composable
@Preview
fun PaymentFailedScreen(onExit: () -> Unit = { }) {
val hasSeenPaidNotice = useDataStore(HAS_SEEN_PAID_NOTICE.key, default = true)
val context = LocalContext.current
ScrollableList {
ScreenTitle(stringResource(R.string.payment_error), showBack = false)
@Suppress("KotlinConstantConditions")
(ParagraphText(
when (BuildConfig.FLAVOR) {
"fDroid" -> stringResource(R.string.payment_failed_body_ex)
"dev" -> stringResource(R.string.payment_failed_body_ex)
"standalone" -> stringResource(R.string.payment_failed_body_ex)
else -> stringResource(R.string.payment_failed_body)
}
))
NavigationItem(title = "Email keyboard@futo.org", style = NavigationItemStyle.Mail, navigate = {
context.openURI("mailto:keyboard@futo.org")
})
Box(modifier = Modifier.fillMaxWidth()) {
val coroutineScope = rememberCoroutineScope()
Button(
onClick = {
// It would be rude to immediately annoy the user again about paying, so delay the notice forever
coroutineScope.launch {
pushNoticeReminderTime(context, Float.MAX_VALUE)
}
hasSeenPaidNotice.setValue(false)
onExit()
},
modifier = Modifier.align(Center)
) {
Text(stringResource(R.string.continue_))
}
}
}
}
@Composable
@Preview(showBackground = true)
fun PaymentScreen(
navController: NavHostController = rememberNavController(),
onExit: () -> Unit = { }
) {
val isAlreadyPaid = useDataStore(IS_ALREADY_PAID)
val pushReminderTime = useDataStore(NOTICE_REMINDER_TIME)
val numDaysInstalled = useNumberOfDaysInstalled()
val forceShowNotice = useDataStore(FORCE_SHOW_NOTICE)
val currentTime = System.currentTimeMillis() / 1000L
val reminderTimeIsUp = (currentTime >= pushReminderTime.value) && ((numDaysInstalled.intValue >= TRIAL_PERIOD_DAYS) || forceShowNotice.value)
val onAlreadyPaid = {
isAlreadyPaid.setValue(true)
navController.navigateUp()
navController.navigate("paid", NavOptions.Builder().setLaunchSingleTop(true).build())
}
val counter = remember { mutableIntStateOf(0) }
ScrollableList {
ScreenTitle(stringResource(R.string.payment_title), showBack = true, navController = navController)
PaymentText()
val context = LocalContext.current
Column(modifier = Modifier.fillMaxWidth()) {
Row(modifier = Modifier.padding(8.dp, 0.dp)) {
Button(
onClick = {
TODO()
}, modifier = Modifier
.weight(1.0f)
.padding(8.dp)
) {
Text(stringResource(R.string.pay))
}
Button(
onClick = {
counter.intValue += 1
if(counter.intValue == 2) {
onAlreadyPaid()
}
}, colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary,
contentColor = MaterialTheme.colorScheme.onSecondary
), modifier = Modifier
.weight(1.0f)
.padding(8.dp)
) {
Text(stringResource(
when(counter.intValue) {
0 -> R.string.i_already_paid
else -> R.string.i_already_paid_2
})
)
}
}
if (reminderTimeIsUp) {
val lastValidRemindValue = remember { mutableFloatStateOf(5.0f) }
val remindDays = remember { mutableStateOf("5") }
Row(
modifier = Modifier
.align(CenterHorizontally)
.padding(16.dp, 0.dp)
.fillMaxWidth()
) {
val coroutineScope = rememberCoroutineScope()
OutlinedButton(
onClick = {
coroutineScope.launch {
pushNoticeReminderTime(context, lastValidRemindValue.floatValue)
}
onExit()
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.remind_me_in_x))
Surface(color = MaterialTheme.colorScheme.primaryContainer) {
BasicTextField(
value = remindDays.value,
onValueChange = {
remindDays.value = it
it.toFloatOrNull()?.let { lastValidRemindValue.floatValue = it }
},
modifier = Modifier
.width(32.dp)
.background(MaterialTheme.colorScheme.primaryContainer)
.padding(4.dp),
textStyle = Typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface),
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface),
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number)
)
}
Text(stringResource(R.string.in_x_days))
}
}
}
}
}
}
@Composable
fun PaymentScreenSwitch(
navController: NavHostController = rememberNavController(),
onExit: () -> Unit = { },
startDestination: String = "payment"
) {
val isAlreadyPaid = useDataStore(IS_ALREADY_PAID)
val hasSeenNotice = useDataStore(HAS_SEEN_PAID_NOTICE)
val paymentDest = if (!isAlreadyPaid.value && hasSeenNotice.value) {
"error"
} else if (isAlreadyPaid.value && !hasSeenNotice.value) {
"paid"
} else {
"payment"
}
LaunchedEffect(paymentDest) {
if (paymentDest != "payment") {
navController.navigate(
paymentDest,
NavOptions.Builder().setLaunchSingleTop(true).build()
)
}
}
NavHost(
navController = navController,
startDestination = startDestination
) {
composable("payment") {
PaymentScreen(navController, onExit)
}
composable("paid") {
PaymentThankYouScreen(onExit)
}
composable("error") {
PaymentFailedScreen(onExit)
}
}
}

View File

@ -1,6 +1,7 @@
package org.futo.inputmethod.latin.uix.theme
import android.app.Activity
import android.content.Context
import android.os.Build
import android.view.View
import android.view.Window
@ -10,8 +11,12 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import org.futo.inputmethod.latin.uix.THEME_KEY
import org.futo.inputmethod.latin.uix.settings.useDataStoreValueNullable
import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme
import kotlin.math.sqrt
val DarkColorScheme = darkColorScheme(
@ -85,4 +90,30 @@ fun UixThemeWrapper(colorScheme: ColorScheme, content: @Composable () -> Unit) {
typography = Typography,
content = content,
)
}
fun ThemeOption?.ensureAvailable(context: Context): ThemeOption? {
return if(this == null) {
null
} else {
if(!this.available(context)) {
null
} else {
this
}
}
}
@Composable
fun UixThemeAuto(content: @Composable () -> Unit) {
val context = LocalContext.current
val themeIdx = useDataStoreValueNullable(THEME_KEY.key, THEME_KEY.default)
val theme: ThemeOption = themeIdx?.let { ThemeOptions[it].ensureAvailable(context) }
?: VoiceInputTheme
val colors = remember(theme.key) { theme.obtainColors(context) }
UixThemeWrapper(colorScheme = colors, content)
}

View File

@ -175,8 +175,8 @@ static inline void showStackTrace() {
#endif // defined(FLAG_DO_PROFILE) || defined(FLAG_DBG)
#ifdef FLAG_DBG
#define DEBUG_DICT true
//#define DEBUG_DICT false
//#define DEBUG_DICT true
#define DEBUG_DICT false
#define DEBUG_DICT_FULL false
#define DEBUG_EDIT_DISTANCE false
#define DEBUG_NODE DEBUG_DICT_FULL