Replace monthly badge expires with cancellation dialogs.

This commit is contained in:
Cody Henthorne
2023-11-16 10:22:01 -05:00
committed by GitHub
parent 62bf5abd8d
commit df4bd1fa4a
16 changed files with 384 additions and 301 deletions

View File

@@ -1,52 +0,0 @@
package org.thoughtcrime.securesms.badges.self.expired
import androidx.core.content.ContextCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.models.SplashImage
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.CommunicationActions
class CantProcessSubscriptionPaymentBottomSheetDialogFragment : DSLSettingsBottomSheetFragment() {
override fun bindAdapter(adapter: DSLSettingsAdapter) {
SplashImage.register(adapter)
adapter.submitList(getConfiguration().toMappingModelList())
}
private fun getConfiguration(): DSLConfiguration {
return configure {
customPref(SplashImage.Model(R.drawable.ic_card_process))
sectionHeaderPref(
title = DSLSettingsText.from(R.string.CantProcessSubscriptionPaymentBottomSheetDialogFragment__cant_process_subscription_payment, DSLSettingsText.CenterModifier)
)
textPref(
summary = DSLSettingsText.from(
requireContext().getString(R.string.CantProcessSubscriptionPaymentBottomSheetDialogFragment__were_having_trouble),
DSLSettingsText.LearnMoreModifier(ContextCompat.getColor(requireContext(), R.color.signal_accent_primary)) {
CommunicationActions.openBrowserLink(requireContext(), requireContext().getString(R.string.donation_decline_code_error_url))
},
DSLSettingsText.CenterModifier
)
)
primaryButton(
text = DSLSettingsText.from(android.R.string.ok)
) {
dismissAllowingStateLoss()
}
secondaryButtonNoOutline(
text = DSLSettingsText.from(R.string.CantProcessSubscriptionPaymentBottomSheetDialogFragment__dont_show_this_again)
) {
SignalStore.donationsValues().showCantProcessDialog = false
dismissAllowingStateLoss()
}
}
}
}

View File

@@ -1,167 +0,0 @@
package org.thoughtcrime.securesms.badges.self.expired
import androidx.fragment.app.FragmentManager
import org.signal.core.util.DimensionUnit
import org.signal.core.util.logging.Log
import org.signal.donations.StripeDeclineCode
import org.signal.donations.StripeFailureCode
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.models.ExpiredBadge
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.mapToErrorStringResource
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.shouldRouteToGooglePay
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.CommunicationActions
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
/**
* Bottom sheet displaying a fading badge with a notice and action for becoming a subscriber again.
*/
class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
peekHeightPercentage = 1f
) {
override fun bindAdapter(adapter: DSLSettingsAdapter) {
ExpiredBadge.register(adapter)
adapter.submitList(getConfiguration().toMappingModelList())
}
private fun getConfiguration(): DSLConfiguration {
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.fromBundle(requireArguments())
val badge: Badge = args.badge
val cancellationReason = UnexpectedSubscriptionCancellation.fromStatus(args.cancelationReason)
val declineCode: StripeDeclineCode? = args.chargeFailure?.let { StripeDeclineCode.getFromCode(it) }
val failureCode: StripeFailureCode? = args.chargeFailure?.let { StripeFailureCode.getFromCode(it) }
val isLikelyASustainer = SignalStore.donationsValues().isLikelyASustainer()
val inactive = cancellationReason == UnexpectedSubscriptionCancellation.INACTIVE
Log.d(TAG, "Displaying Expired Badge Fragment with bundle: ${requireArguments()}", true)
return configure {
customPref(ExpiredBadge.Model(badge))
sectionHeaderPref(
DSLSettingsText.from(
if (badge.isBoost()) {
R.string.ExpiredBadgeBottomSheetDialogFragment__boost_badge_expired
} else {
R.string.ExpiredBadgeBottomSheetDialogFragment__monthly_donation_cancelled
},
DSLSettingsText.CenterModifier
)
)
space(DimensionUnit.DP.toPixels(4f).toInt())
noPadTextPref(
DSLSettingsText.from(
if (badge.isBoost()) {
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_boost_badge_has_expired_and)
} else if (declineCode != null) {
getString(
R.string.ExpiredBadgeBottomSheetDialogFragment__your_recurring_monthly_donation_was_canceled_s,
getString(declineCode.mapToErrorStringResource()),
badge.name
)
} else if (failureCode != null) {
getString(
R.string.ExpiredBadgeBottomSheetDialogFragment__your_recurring_monthly_donation_was_canceled_s,
getString(failureCode.mapToErrorStringResource()),
badge.name
)
} else if (inactive) {
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_recurring_monthly_donation_was_automatically, badge.name)
} else {
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_recurring_monthly_donation_was_canceled)
},
DSLSettingsText.CenterModifier
)
)
space(DimensionUnit.DP.toPixels(16f).toInt())
if (badge.isSubscription() && declineCode?.shouldRouteToGooglePay() == true) {
space(DimensionUnit.DP.toPixels(68f).toInt())
secondaryButtonNoOutline(
text = DSLSettingsText.from(R.string.ExpiredBadgeBottomSheetDialogFragment__go_to_google_pay),
onClick = {
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.google_pay_url))
}
)
} else {
noPadTextPref(
DSLSettingsText.from(
if (badge.isBoost()) {
if (isLikelyASustainer) {
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can_reactivate
} else {
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can_keep
}
} else {
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can
},
DSLSettingsText.CenterModifier
)
)
space(DimensionUnit.DP.toPixels(92f).toInt())
}
primaryButton(
text = DSLSettingsText.from(
if (badge.isBoost()) {
if (isLikelyASustainer) {
R.string.ExpiredBadgeBottomSheetDialogFragment__add_a_boost
} else {
R.string.ExpiredBadgeBottomSheetDialogFragment__become_a_sustainer
}
} else {
R.string.ExpiredBadgeBottomSheetDialogFragment__renew_subscription
}
),
onClick = {
dismiss()
if (isLikelyASustainer) {
requireActivity().startActivity(AppSettingsActivity.boost(requireContext()))
} else {
requireActivity().startActivity(AppSettingsActivity.subscriptions(requireContext()))
}
}
)
secondaryButtonNoOutline(
text = DSLSettingsText.from(R.string.ExpiredBadgeBottomSheetDialogFragment__not_now),
onClick = {
dismiss()
}
)
}
}
companion object {
private val TAG = Log.tag(ExpiredBadgeBottomSheetDialogFragment::class.java)
@JvmStatic
fun show(
badge: Badge,
cancellationReason: UnexpectedSubscriptionCancellation?,
chargeFailure: ActiveSubscription.ChargeFailure?,
fragmentManager: FragmentManager
) {
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.Builder(badge, cancellationReason?.status, chargeFailure?.code).build()
val fragment = ExpiredBadgeBottomSheetDialogFragment()
fragment.arguments = args.toBundle()
fragment.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
}

View File

@@ -0,0 +1,121 @@
package org.thoughtcrime.securesms.badges.self.expired
import androidx.fragment.app.FragmentManager
import org.signal.core.util.DimensionUnit
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.models.ExpiredBadge
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
/**
* Bottom sheet displaying a fading badge with a notice and action for becoming a subscriber again.
*/
class ExpiredOneTimeBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
peekHeightPercentage = 1f
) {
override fun bindAdapter(adapter: DSLSettingsAdapter) {
ExpiredBadge.register(adapter)
adapter.submitList(getConfiguration().toMappingModelList())
}
private fun getConfiguration(): DSLConfiguration {
val args = ExpiredOneTimeBadgeBottomSheetDialogFragmentArgs.fromBundle(requireArguments())
val badge: Badge = args.badge
val isLikelyASustainer = SignalStore.donationsValues().isLikelyASustainer()
Log.d(TAG, "Displaying Expired Badge Fragment with bundle: ${requireArguments()}", true)
return configure {
customPref(ExpiredBadge.Model(badge))
sectionHeaderPref(
DSLSettingsText.from(
if (badge.isBoost()) {
R.string.ExpiredBadgeBottomSheetDialogFragment__boost_badge_expired
} else {
R.string.ExpiredBadgeBottomSheetDialogFragment__monthly_donation_cancelled
},
DSLSettingsText.CenterModifier
)
)
space(DimensionUnit.DP.toPixels(4f).toInt())
noPadTextPref(
DSLSettingsText.from(
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_boost_badge_has_expired_and),
DSLSettingsText.CenterModifier
)
)
space(DimensionUnit.DP.toPixels(16f).toInt())
noPadTextPref(
DSLSettingsText.from(
if (isLikelyASustainer) {
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can_reactivate
} else {
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can_keep
},
DSLSettingsText.CenterModifier
)
)
space(DimensionUnit.DP.toPixels(92f).toInt())
primaryButton(
text = DSLSettingsText.from(
if (isLikelyASustainer) {
R.string.ExpiredBadgeBottomSheetDialogFragment__add_a_boost
} else {
R.string.ExpiredBadgeBottomSheetDialogFragment__become_a_sustainer
}
),
onClick = {
dismiss()
if (isLikelyASustainer) {
requireActivity().startActivity(AppSettingsActivity.boost(requireContext()))
} else {
requireActivity().startActivity(AppSettingsActivity.subscriptions(requireContext()))
}
}
)
secondaryButtonNoOutline(
text = DSLSettingsText.from(R.string.ExpiredBadgeBottomSheetDialogFragment__not_now),
onClick = {
dismiss()
}
)
}
}
companion object {
private val TAG = Log.tag(ExpiredOneTimeBadgeBottomSheetDialogFragment::class.java)
@JvmStatic
fun show(
badge: Badge,
cancellationReason: UnexpectedSubscriptionCancellation?,
chargeFailure: ActiveSubscription.ChargeFailure?,
fragmentManager: FragmentManager
) {
val args = ExpiredOneTimeBadgeBottomSheetDialogFragmentArgs.Builder(badge, cancellationReason?.status, chargeFailure?.code).build()
val fragment = ExpiredOneTimeBadgeBottomSheetDialogFragment()
fragment.arguments = args.toBundle()
fragment.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
}

View File

@@ -0,0 +1,191 @@
package org.thoughtcrime.securesms.badges.self.expired
import android.content.res.Configuration
import android.net.Uri
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentManager
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Texts
import org.signal.core.ui.theme.SignalTheme
import org.signal.donations.StripeDeclineCode
import org.signal.donations.StripeFailureCode
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImage112
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.mapToErrorStringResource
import org.thoughtcrime.securesms.components.settings.app.subscription.manage.ManageDonationsFragment
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.SpanUtil
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
class MonthlyDonationCanceledBottomSheetDialogFragment : ComposeBottomSheetDialogFragment() {
override val peekHeightPercentage: Float = 1f
@Composable
override fun SheetContent() {
val chargeFailure: ActiveSubscription.ChargeFailure? = SignalStore.donationsValues().getUnexpectedSubscriptionCancelationChargeFailure()
val declineCode: StripeDeclineCode = StripeDeclineCode.getFromCode(chargeFailure?.outcomeNetworkReason)
val failureCode: StripeFailureCode = StripeFailureCode.getFromCode(chargeFailure?.code)
val errorMessage = if (declineCode.isKnown()) {
declineCode.mapToErrorStringResource()
} else if (failureCode.isKnown) {
failureCode.mapToErrorStringResource()
} else {
declineCode.mapToErrorStringResource()
}
MonthlyDonationCanceled(
badge = SignalStore.donationsValues().getExpiredBadge(),
errorMessageRes = errorMessage,
onRenewClicked = {
startActivity(AppSettingsActivity.subscriptions(requireContext()))
dismissAllowingStateLoss()
},
onNotNowClicked = {
SignalStore.donationsValues().showMonthlyDonationCanceledDialog = false
dismissAllowingStateLoss()
}
)
}
companion object {
@JvmStatic
fun show(fragmentManager: FragmentManager) {
val fragment = MonthlyDonationCanceledBottomSheetDialogFragment()
fragment.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
}
@Preview(name = "Light Theme", group = "ShortName", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "ShortName", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun MonthlyDonationCanceledPreview() {
SignalTheme {
Surface {
MonthlyDonationCanceled(
badge = Badge(
id = "",
category = Badge.Category.Donor,
name = "Signal Star",
description = "",
imageUrl = Uri.EMPTY,
imageDensity = "",
expirationTimestamp = 0L,
visible = true,
duration = 0L
),
errorMessageRes = R.string.StripeFailureCode__verify_your_bank_details_are_correct,
onRenewClicked = {},
onNotNowClicked = {}
)
}
}
}
@Composable
private fun MonthlyDonationCanceled(
badge: Badge?,
@StringRes errorMessageRes: Int,
onRenewClicked: () -> Unit,
onNotNowClicked: () -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(horizontal = 34.dp)
) {
BottomSheets.Handle()
if (badge != null) {
Box(modifier = Modifier.padding(top = 21.dp, bottom = 16.dp)) {
BadgeImage112(
badge = badge,
modifier = Modifier
.size(80.dp)
)
Image(
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_error_circle_fill_24),
contentScale = ContentScale.Inside,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error),
modifier = Modifier
.size(24.dp)
.align(Alignment.TopEnd)
.background(
color = SignalTheme.colors.colorSurface1,
shape = CircleShape
)
)
}
}
Text(
text = stringResource(id = R.string.MonthlyDonationCanceled__title),
style = MaterialTheme.typography.titleLarge.copy(textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurface),
modifier = Modifier.padding(bottom = 20.dp)
)
val context = LocalContext.current
val learnMore = stringResource(id = R.string.MonthlyDonationCanceled__learn_more)
val errorMessage = stringResource(id = errorMessageRes)
val fullString = stringResource(id = R.string.MonthlyDonationCanceled__message, errorMessage, learnMore)
val spanned = SpanUtil.urlSubsequence(fullString, learnMore, ManageDonationsFragment.DONATE_TROUBLESHOOTING_URL)
Texts.LinkifiedText(
textWithUrlSpans = spanned,
onUrlClick = { CommunicationActions.openBrowserLink(context, it) },
style = MaterialTheme.typography.bodyLarge.copy(textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant),
modifier = Modifier.padding(bottom = 36.dp)
)
Buttons.LargeTonal(
onClick = onRenewClicked,
modifier = Modifier
.defaultMinSize(minWidth = 220.dp)
.padding(bottom = 16.dp)
) {
Text(text = stringResource(id = R.string.MonthlyDonationCanceled__renew_button))
}
TextButton(
onClick = onNotNowClicked,
modifier = Modifier
.defaultMinSize(minWidth = 220.dp)
.padding(bottom = 56.dp)
) {
Text(text = stringResource(id = R.string.MonthlyDonationCanceled__not_now_button))
}
}
}