Update bank mandate CTA UX.

This commit is contained in:
Cody Henthorne
2023-11-01 16:38:06 -04:00
committed by Greyson Parrelli
parent 0659edb762
commit 6e856a7648
3 changed files with 191 additions and 80 deletions

View File

@@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.mandate
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import androidx.compose.animation.AnimatedVisibility
@@ -12,22 +13,33 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
@@ -37,16 +49,16 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import kotlinx.coroutines.launch
import org.signal.core.ui.Buttons
import org.signal.core.ui.Dividers
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.Texts
import org.signal.core.ui.theme.SignalTheme
import org.signal.donations.PaymentSourceType
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection
import org.thoughtcrime.securesms.compose.StatusBarColorAnimator
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.viewModel
@@ -61,15 +73,15 @@ class BankTransferMandateFragment : ComposeFragment() {
BankTransferMandateViewModel(PaymentSourceType.Stripe.SEPADebit)
}
private lateinit var statusBarColorNestedScrollConnection: StatusBarColorNestedScrollConnection
private lateinit var statusBarColorAnimator: StatusBarColorAnimator
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
statusBarColorNestedScrollConnection = StatusBarColorNestedScrollConnection(requireActivity())
statusBarColorAnimator = StatusBarColorAnimator(requireActivity())
}
override fun onResume() {
super.onResume()
statusBarColorNestedScrollConnection.setColorImmediate()
statusBarColorAnimator.setColorImmediate()
}
@Composable
@@ -83,7 +95,7 @@ class BankTransferMandateFragment : ComposeFragment() {
onNavigationClick = this::onNavigationClick,
onContinueClick = this::onContinueClick,
onLearnMoreClick = this::onLearnMoreClick,
modifier = Modifier.nestedScroll(statusBarColorNestedScrollConnection)
onCanScrollUp = statusBarColorAnimator::setCanScrollUp
)
}
@@ -119,11 +131,14 @@ fun BankTransferScreenPreview() {
failedToLoadMandate = false,
onNavigationClick = {},
onContinueClick = {},
onLearnMoreClick = {}
onLearnMoreClick = {},
onCanScrollUp = {}
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun BankTransferScreen(
bankMandate: String,
@@ -131,91 +146,123 @@ fun BankTransferScreen(
onNavigationClick: () -> Unit,
onContinueClick: () -> Unit,
onLearnMoreClick: () -> Unit,
modifier: Modifier = Modifier
onCanScrollUp: (Boolean) -> Unit
) {
Scaffolds.Settings(
title = stringResource(id = R.string.BankTransferMandateFragment__bank_transfer),
onNavigationClick = onNavigationClick,
navigationIconPainter = rememberVectorPainter(ImageVector.vectorResource(id = R.drawable.symbol_arrow_left_24)),
titleContent = { contentOffset, title ->
AnimatedVisibility(
visible = contentOffset < 0f,
enter = fadeIn(),
exit = fadeOut()
) {
Text(text = title, style = MaterialTheme.typography.titleLarge)
}
},
modifier = modifier
) {
LazyColumn(
horizontalAlignment = CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(top = 64.dp)
) {
item {
Image(
painter = painterResource(id = R.drawable.bank_transfer),
contentScale = ContentScale.Inside,
contentDescription = null,
modifier = Modifier
.size(72.dp)
.background(
SignalTheme.colors.colorSurface2,
CircleShape
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
Scaffold(
topBar = {
TopAppBar(
title = {
AnimatedVisibility(
visible = listState.canScrollBackward,
enter = fadeIn(),
exit = fadeOut()
) {
Text(text = stringResource(id = R.string.BankTransferMandateFragment__bank_transfer), style = MaterialTheme.typography.titleLarge)
}
},
navigationIcon = {
IconButton(
onClick = onNavigationClick,
Modifier.padding(end = 16.dp)
) {
Icon(
painter = rememberVectorPainter(ImageVector.vectorResource(id = R.drawable.symbol_arrow_left_24)),
contentDescription = null
)
)
}
}
},
colors = if (listState.canScrollBackward) TopAppBarDefaults.topAppBarColors(containerColor = SignalTheme.colors.colorSurface2) else TopAppBarDefaults.topAppBarColors()
)
}
) {
onCanScrollUp(listState.canScrollBackward)
item {
Text(
text = stringResource(id = R.string.BankTransferMandateFragment__bank_transfer),
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(top = 12.dp, bottom = 15.dp)
)
}
Column(horizontalAlignment = CenterHorizontally, modifier = Modifier.fillMaxSize()) {
LazyColumn(
state = listState,
horizontalAlignment = CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.weight(1f, true)
.padding(top = 64.dp)
) {
item {
Image(
painter = painterResource(id = R.drawable.bank_transfer),
contentScale = ContentScale.Inside,
contentDescription = null,
modifier = Modifier
.size(72.dp)
.background(
SignalTheme.colors.colorSurface2,
CircleShape
)
)
}
item {
val learnMore = stringResource(id = R.string.BankTransferMandateFragment__learn_more)
val fullString = stringResource(id = R.string.BankTransferMandateFragment__stripe_processes_donations, learnMore)
item {
Text(
text = stringResource(id = R.string.BankTransferMandateFragment__bank_transfer),
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(top = 12.dp, bottom = 15.dp)
)
}
Texts.LinkifiedText(
textWithUrlSpans = SpanUtil.urlSubsequence(fullString, learnMore, stringResource(id = R.string.donate_url)), // TODO [alex] -- final URL
onUrlClick = {
onLearnMoreClick()
},
style = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant
),
modifier = Modifier
.padding(bottom = 12.dp)
.padding(horizontal = dimensionResource(id = R.dimen.bank_transfer_mandate_gutter))
)
}
item {
val learnMore = stringResource(id = R.string.BankTransferMandateFragment__learn_more)
val fullString = stringResource(id = R.string.BankTransferMandateFragment__stripe_processes_donations, learnMore)
item {
Dividers.Default()
}
Texts.LinkifiedText(
textWithUrlSpans = SpanUtil.urlSubsequence(fullString, learnMore, stringResource(id = R.string.donate_url)), // TODO [alex] -- final URL
onUrlClick = {
onLearnMoreClick()
},
style = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant
),
modifier = Modifier
.padding(bottom = 12.dp)
.padding(horizontal = dimensionResource(id = R.dimen.bank_transfer_mandate_gutter))
)
}
item {
Text(
text = if (failedToLoadMandate) stringResource(id = R.string.BankTransferMandateFragment__failed_to_load_mandate) else bankMandate,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.bank_transfer_mandate_gutter), vertical = 16.dp)
)
item {
Dividers.Default()
}
item {
Text(
text = if (failedToLoadMandate) stringResource(id = R.string.BankTransferMandateFragment__failed_to_load_mandate) else bankMandate,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.bank_transfer_mandate_gutter), vertical = 16.dp)
)
}
}
if (!failedToLoadMandate) {
item {
Surface(
shadowElevation = if (listState.canScrollForward) 8.dp else 0.dp,
modifier = Modifier.fillMaxWidth()
) {
Buttons.LargeTonal(
onClick = onContinueClick,
onClick = {
if (!listState.canScrollForward) {
onContinueClick()
} else {
scope.launch {
listState.animateScrollBy(value = 1000f)
}
}
},
modifier = Modifier
.padding(top = 16.dp, bottom = 46.dp)
.wrapContentWidth()
.padding(top = 16.dp, bottom = 16.dp)
.defaultMinSize(minWidth = 220.dp)
) {
Text(text = stringResource(id = R.string.BankTransferMandateFragment__continue))
Text(text = if (listState.canScrollForward) stringResource(id = R.string.BankTransferMandateFragment__read_more) else stringResource(id = R.string.BankTransferMandateFragment__continue))
}
}
}

View File

@@ -0,0 +1,62 @@
package org.thoughtcrime.securesms.compose
import android.animation.ValueAnimator
import android.app.Activity
import androidx.core.content.ContextCompat
import com.google.android.material.animation.ArgbEvaluatorCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.WindowUtil
/**
* Controls status-bar color based off ability to scroll up
*/
class StatusBarColorAnimator(
private val activity: Activity
) {
private var animator: ValueAnimator? = null
private var previousCanScrollUp: Boolean = false
private val normalColor = ContextCompat.getColor(activity, R.color.signal_colorBackground)
private val scrollColor = ContextCompat.getColor(activity, R.color.signal_colorSurface2)
fun setCanScrollUp(canScrollUp: Boolean) {
if (previousCanScrollUp == canScrollUp) {
return
}
previousCanScrollUp = canScrollUp
applyState(canScrollUp)
}
fun setColorImmediate() {
val end = when {
previousCanScrollUp -> scrollColor
else -> normalColor
}
animator?.cancel()
WindowUtil.setStatusBarColor(
activity.window,
end
)
}
private fun applyState(canScrollUp: Boolean) {
val (start, end) = when {
canScrollUp -> normalColor to scrollColor
else -> scrollColor to normalColor
}
animator?.cancel()
animator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 200
addUpdateListener {
WindowUtil.setStatusBarColor(
activity.window,
ArgbEvaluatorCompat.getInstance().evaluate(it.animatedFraction, start, end)
)
}
start()
}
}
}