Update payment menu design

This commit is contained in:
Aleksandras Kostarevas 2024-06-14 15:19:14 +03:00
parent 513c08d39f
commit 13f81556ad
5 changed files with 287 additions and 115 deletions

View File

@ -0,0 +1,13 @@
<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="M22,12l-4,0l-3,9l-6,-18l-3,9l-4,0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,47 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<group>
<clip-path
android:pathData="M0,0h128v128h-128z"/>
<path
android:pathData="M114.5,49.5C114.5,56.96 108.58,63 101.28,63C93.98,63 88.06,56.96 88.06,49.5C88.06,42.04 93.98,36 101.28,36C108.58,36 114.5,42.04 114.5,49.5ZZM97.43,52.22C96.08,50.84 95.4,50.15 95.4,49.29C95.4,48.43 96.08,47.73 97.43,46.35L98.54,45.22C99.89,43.84 100.57,43.14 101.41,43.14C102.25,43.14 102.93,43.84 104.29,45.22L105.39,46.35C106.75,47.73 107.43,48.43 107.43,49.29C107.43,50.15 106.75,50.84 105.39,52.22L104.29,53.35C102.93,54.74 102.25,55.43 101.41,55.43C100.57,55.43 99.89,54.74 98.54,53.35L97.43,52.22ZZM32.14,43.96C32.51,43.96 32.82,43.65 32.82,43.27L32.82,38.08C32.82,37.69 32.51,37.38 32.14,37.38L14.18,37.38C13.8,37.38 13.5,37.69 13.5,38.08L13.5,60.92C13.5,61.31 13.8,61.62 14.18,61.62L20.28,61.62C20.65,61.62 20.96,61.31 20.96,60.92L20.96,54.55C20.96,54.17 21.26,53.86 21.63,53.86L29.84,53.86C30.21,53.86 30.51,53.55 30.51,53.17L30.51,47.98C30.51,47.6 30.21,47.28 29.84,47.28L21.63,47.28C21.26,47.28 20.96,46.97 20.96,46.59L20.96,44.65C20.96,44.27 21.26,43.96 21.63,43.96L32.14,43.96ZZM47.9,62.03L48,62.03C55.25,62.03 59.69,58.08 59.69,50.57L59.69,38.08C59.69,37.69 59.39,37.38 59.01,37.38L52.91,37.38C52.54,37.38 52.24,37.69 52.24,38.08L52.24,49.88C52.24,52.44 51.15,54.69 48,54.69L47.9,54.69C44.78,54.69 43.66,52.44 43.66,49.88L43.66,38.08C43.66,37.69 43.36,37.38 42.98,37.38L36.88,37.38C36.51,37.38 36.21,37.69 36.21,38.08L36.21,50.57C36.21,58.08 40.65,62.03 47.9,62.03ZZM63.08,38.08C63.08,37.69 63.38,37.38 63.76,37.38L85.65,37.38C86.03,37.38 86.33,37.69 86.33,38.08L86.33,43.37C86.33,43.76 86.03,44.06 85.65,44.06L79.11,44.06C78.74,44.06 78.43,44.38 78.43,44.76L78.43,60.92C78.43,61.31 78.13,61.62 77.76,61.62L71.66,61.62C71.28,61.62 70.98,61.31 70.98,60.92L70.98,44.76C70.98,44.38 70.68,44.06 70.3,44.06L63.76,44.06C63.38,44.06 63.08,43.76 63.08,43.37L63.08,38.08ZZ"
android:fillColor="#FFFFFF"
android:fillType="evenOdd"/>
<path
android:pathData="M35,72L40.54,72A2,2 0,0 1,42.54 74L42.54,79.6A2,2 0,0 1,40.54 81.6L35,81.6A2,2 0,0 1,33 79.6L33,74A2,2 0,0 1,35 72z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M35,85.2L40.54,85.2A2,2 0,0 1,42.54 87.2L42.54,92.8A2,2 0,0 1,40.54 94.8L35,94.8A2,2 0,0 1,33 92.8L33,87.2A2,2 0,0 1,35 85.2z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M48.12,72L53.65,72A2,2 0,0 1,55.65 74L55.65,79.6A2,2 0,0 1,53.65 81.6L48.12,81.6A2,2 0,0 1,46.12 79.6L46.12,74A2,2 0,0 1,48.12 72z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M48.12,85.2L53.65,85.2A2,2 0,0 1,55.65 87.2L55.65,92.8A2,2 0,0 1,53.65 94.8L48.12,94.8A2,2 0,0 1,46.12 92.8L46.12,87.2A2,2 0,0 1,48.12 85.2z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M61.23,72L66.77,72A2,2 0,0 1,68.77 74L68.77,79.6A2,2 0,0 1,66.77 81.6L61.23,81.6A2,2 0,0 1,59.23 79.6L59.23,74A2,2 0,0 1,61.23 72z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M61.23,85.2L66.77,85.2A2,2 0,0 1,68.77 87.2L68.77,92.8A2,2 0,0 1,66.77 94.8L61.23,94.8A2,2 0,0 1,59.23 92.8L59.23,87.2A2,2 0,0 1,61.23 85.2z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M45.73,98.4L82.27,98.4A2,2 0,0 1,84.27 100.4L84.27,106A2,2 0,0 1,82.27 108L45.73,108A2,2 0,0 1,43.73 106L43.73,100.4A2,2 0,0 1,45.73 98.4z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M74.35,72L79.88,72A2,2 0,0 1,81.88 74L81.88,79.6A2,2 0,0 1,79.88 81.6L74.35,81.6A2,2 0,0 1,72.35 79.6L72.35,74A2,2 0,0 1,74.35 72z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M74.35,85.2L79.88,85.2A2,2 0,0 1,81.88 87.2L81.88,92.8A2,2 0,0 1,79.88 94.8L74.35,94.8A2,2 0,0 1,72.35 92.8L72.35,87.2A2,2 0,0 1,74.35 85.2z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M87.46,72L93,72A2,2 0,0 1,95 74L95,79.6A2,2 0,0 1,93 81.6L87.46,81.6A2,2 0,0 1,85.46 79.6L85.46,74A2,2 0,0 1,87.46 72z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M87.46,85.2L93,85.2A2,2 0,0 1,95 87.2L95,92.8A2,2 0,0 1,93 94.8L87.46,94.8A2,2 0,0 1,85.46 92.8L85.46,87.2A2,2 0,0 1,87.46 85.2z"
android:fillColor="#FFFFFF"/>
</group>
</vector>

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="M5,11L19,11A2,2 0,0 1,21 13L21,20A2,2 0,0 1,19 22L5,22A2,2 0,0 1,3 20L3,13A2,2 0,0 1,5 11z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M7,11V7a5,5 0,0 1,9.9 -1"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -68,9 +68,9 @@
<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="pay">Pay (about $4.99 + tax)</string>
<string name="i_already_paid">I already paid</string>
<string name="i_already_paid_2">I already paid (tap again)</string>
<string name="i_already_paid_2">I already paid (tap again to confirm)</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>

View File

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