diff --git a/java/res/drawable/activity.xml b/java/res/drawable/activity.xml new file mode 100644 index 000000000..22d41dadf --- /dev/null +++ b/java/res/drawable/activity.xml @@ -0,0 +1,13 @@ + + + diff --git a/java/res/drawable/keyboard_icon.xml b/java/res/drawable/keyboard_icon.xml new file mode 100644 index 000000000..5a546e80a --- /dev/null +++ b/java/res/drawable/keyboard_icon.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + diff --git a/java/res/drawable/unlock.xml b/java/res/drawable/unlock.xml new file mode 100644 index 000000000..c469af0e0 --- /dev/null +++ b/java/res/drawable/unlock.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/res/values/strings-uix.xml b/java/res/values/strings-uix.xml index b89a3fd4c..a599b3eee 100644 --- a/java/res/values/strings-uix.xml +++ b/java/res/values/strings-uix.xml @@ -68,9 +68,9 @@ 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 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. Pay via %s - Pay + Pay (about $4.99 + tax) I already paid - I already paid (tap again) + I already paid (tap again to confirm) "Remind me in " " days" You are on the Developer release, so you are seeing all payment methods diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Payment.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Payment.kt index 5b30e88ea..5b68a09cf 100644 --- a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Payment.kt +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Payment.kt @@ -4,7 +4,8 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.widget.Toast -import androidx.compose.foundation.background +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -13,18 +14,22 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn 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.Icon +import androidx.compose.material3.LocalContentColor 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.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableIntState import androidx.compose.runtime.mutableFloatStateOf @@ -32,15 +37,19 @@ 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 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.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.painterResource 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.compose.ui.unit.dp import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey @@ -120,7 +129,22 @@ fun useNumberOfDaysInstalled(): MutableIntState { @Composable fun ParagraphText(it: String, modifier: Modifier = Modifier) { Text(it, modifier = modifier.padding(16.dp, 8.dp), style = Typography.bodyMedium, - color = MaterialTheme.colorScheme.onBackground) + color = LocalContentColor.current) +} + +@Composable +fun IconText(icon: Painter, title: String, body: String) { + Row(modifier = Modifier.padding(8.dp)) { + Icon(icon, contentDescription = null, modifier = Modifier + .align(Alignment.Top) + .padding(8.dp, 10.dp) + .size(with(LocalDensity.current) { Typography.titleMedium.fontSize.toDp() })) + Column(modifier = Modifier.padding(6.dp)) { + Text(title, style = Typography.titleMedium) + Spacer(modifier = Modifier.height(4.dp)) + Text(body, style = Typography.bodySmall, color = LocalContentColor.current.copy(alpha = 0.8f)) + } + } } @Composable @@ -134,10 +158,28 @@ fun PaymentText(verbose: Boolean) { ParagraphText(stringResource(R.string.payment_text_1_alt)) } - ParagraphText(stringResource(R.string.payment_text_2)) - if(verbose) { - ParagraphText(stringResource(R.string.payment_text_3)) + IconText( + icon = painterResource(id = R.drawable.activity), + title = "Sustainable Development", + body = "FUTO's mission is for open-source software and non-malicious software business practices to become a sustainable income source for projects and their developers. For this reason, we are in favor of users actually paying for software." + ) + + IconText( + icon = painterResource(id = R.drawable.unlock), + title = "Commitment to Privacy", + body = "This app will never serve you ads or sell your data. We are not in the business of doing that." + ) + + /* + IconText( + icon = painterResource(id = R.drawable.code), + title = "Ongoing Work", + body = "Creating and maintaining great software requires significant resources. Your support will help us keep development going." + ) + */ + } else { + ParagraphText(stringResource(R.string.payment_text_2)) } } @@ -208,47 +250,42 @@ fun ConditionalUnpaidNoticeInVoiceInputWindow(onClose: (() -> Unit)? = null) { } +@Composable +fun MediumTitle(text: String) { + Text( + text, + modifier = Modifier.padding(8.dp), + style = Typography.titleMedium, + color = LocalContentColor.current + ) +} + @Composable @Preview fun UnpaidNotice(openMenu: () -> Unit = { }) { - Surface( - color = MaterialTheme.colorScheme.surfaceVariant, modifier = Modifier - .clickable { openMenu() } - .fillMaxWidth() - .padding(24.dp, 8.dp), shape = RoundedCornerShape(24.dp) - ) { - Column(modifier = Modifier.padding(8.dp, 0.dp)) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - stringResource(R.string.unpaid_title), - modifier = Modifier.padding(8.dp), - style = Typography.titleMedium, - color = MaterialTheme.colorScheme.onBackground - ) + PaymentSurface(isPrimary = true, title = stringResource(R.string.unpaid_title), onClick = openMenu) { + PaymentText(false) - PaymentText(false) + Row( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth() + ) { - Row( - modifier = Modifier - .padding(8.dp) - .align(CenterHorizontally) - ) { - - Box(modifier = Modifier.weight(1.0f)) { - Button(onClick = openMenu, modifier = Modifier.align(Center)) { - Text(stringResource(R.string.pay_now)) - } + Box(modifier = Modifier.weight(1.0f)) { + Button(onClick = openMenu, modifier = Modifier.align(Center)) { + Text(stringResource(R.string.pay_now)) } + } - Box(modifier = Modifier.weight(1.0f)) { - Button( - onClick = openMenu, colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.secondary, - contentColor = MaterialTheme.colorScheme.onSecondary - ), modifier = Modifier.align(Center) - ) { - Text(stringResource(R.string.i_already_paid)) - } + Box(modifier = Modifier.weight(1.0f)) { + Button( + onClick = openMenu, colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary + ), modifier = Modifier.align(Center) + ) { + Text(stringResource(R.string.i_already_paid)) } } } @@ -350,7 +387,47 @@ fun PaymentFailedScreen(onExit: () -> Unit = { }) { } @Composable -@Preview(showBackground = true) +fun PaymentSurface(isPrimary: Boolean, title: String, onClick: (() -> Unit)? = null, content: @Composable () -> Unit) { + val containerColor = if (isPrimary) { + MaterialTheme.colorScheme.surfaceVariant + } else { + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + } + + val contentColor = if (isPrimary) { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Center) { + Surface( + color = containerColor, + border = BorderStroke(2.dp, contentColor.copy(alpha = 0.33f)), + shape = RoundedCornerShape(24.dp), + modifier = Modifier + .padding(16.dp) + .widthIn(Dp.Unspecified, 400.dp) + .let { + if (onClick != null) { + it.clickable { onClick() } + } else { + it + } + } + ) { + Column(modifier = Modifier.padding(8.dp)) { + CompositionLocalProvider(LocalContentColor provides contentColor) { + MediumTitle(title) + content() + } + } + } + } +} + +@Composable +@Preview(showBackground = true, heightDp = 10000) fun PaymentScreen( navController: NavHostController = rememberNavController(), onExit: () -> Unit = { } @@ -377,92 +454,107 @@ fun PaymentScreen( ScrollableList { ScreenTitle(stringResource(R.string.payment_title), showBack = true, navController = navController) - PaymentText(true) + + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Center) { + Icon(painterResource(id = R.drawable.keyboard_icon), contentDescription = null, tint = MaterialTheme.colorScheme.onBackground) + } val context = LocalContext.current - Column(modifier = Modifier.fillMaxWidth()) { - Row(modifier = Modifier.padding(8.dp, 0.dp)) { - Button( - onClick = { - val url = runBlocking { context.getSetting(TMP_PAYMENT_URL) } - if(url.isNotBlank()) { - context.openURI(url) - } else { - val toast = Toast.makeText(context, "Payment is unsupported on this build (still WIP)", Toast.LENGTH_SHORT) - toast.show() - } - }, modifier = Modifier - .weight(1.0f) - .padding(8.dp) - ) { - Text(stringResource(R.string.pay)) - } + PaymentSurface(isPrimary = true, title = "Pay for FUTO Keyboard") { + PaymentText(true) - 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) { + Button( + onClick = { + val url = runBlocking { context.getSetting(TMP_PAYMENT_URL) } + if(url.isNotBlank()) { + context.openURI(url) + } else { + val toast = Toast.makeText(context, "Payment is unsupported on this build", Toast.LENGTH_SHORT) + toast.show() + } + }, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + Text(stringResource(R.string.pay)) + } + } + + PaymentSurface(isPrimary = false, title = "Already paid?") { + ParagraphText(it = "If you already paid for FUTO Keyboard or FUTO Voice Input, tap below.") + Button( + onClick = { + counter.intValue += 1 + if (counter.intValue == 2) { + onAlreadyPaid() + } + }, colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer + ), modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + Text( + stringResource( + when (counter.intValue) { 0 -> R.string.i_already_paid else -> R.string.i_already_paid_2 - }) + } ) - } + ) } + } + + if (reminderTimeIsUp) { + PaymentSurface(isPrimary = false, title = "Remind later") { + ParagraphText("This will hide the reminder in the settings screen for the period of days entered.") - 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) - ) + val coroutineScope = rememberCoroutineScope() + Button( + onClick = { + coroutineScope.launch { + pushNoticeReminderTime(context, lastValidRemindValue.floatValue) } - Text(stringResource(R.string.in_x_days)) - } + onExit() + }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer + ), + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + Text(stringResource(R.string.remind_me_in_x)) + BasicTextField( + value = remindDays.value, + onValueChange = { + remindDays.value = it + + it.toFloatOrNull() + ?.let { lastValidRemindValue.floatValue = it } + }, + modifier = Modifier + .width(32.dp) + .border(Dp.Hairline, LocalContentColor.current) + .padding(4.dp), + textStyle = Typography.bodyMedium.copy(color = LocalContentColor.current), + cursorBrush = SolidColor(LocalContentColor.current), + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) + ) + Text(stringResource(R.string.in_x_days)) } } } + + NavigationItem(title = "Help", subtitle = "Need help? Visit our website", style = NavigationItemStyle.Misc, navigate = { + context.openURI("https://keyboard.futo.org/") + }) } }