mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-25 12:17:22 +00:00
Add decline code messages into expiration sheet.
This commit is contained in:
@@ -2,6 +2,8 @@ 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.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.ExpiredBadge
|
||||
@@ -11,9 +13,12 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFrag
|
||||
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
|
||||
|
||||
/**
|
||||
@@ -32,11 +37,12 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.fromBundle(requireArguments())
|
||||
val badge: Badge = args.badge
|
||||
val cancellationReason = UnexpectedSubscriptionCancellation.fromStatus(args.cancelationReason)
|
||||
val chargeFailure: ActiveSubscription.ChargeFailure? = SignalStore.donationsValues().getUnexpectedSubscriptionCancelationChargeFailure()
|
||||
val declineCode: StripeDeclineCode? = args.chargeFailure?.let { StripeDeclineCode.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))
|
||||
|
||||
@@ -57,6 +63,12 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||
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 (inactive) {
|
||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_recurring_monthly_donation_was_automatically, badge.name)
|
||||
} else {
|
||||
@@ -68,22 +80,33 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||
|
||||
space(DimensionUnit.DP.toPixels(16f).toInt())
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
if (badge.isSubscription() && declineCode?.shouldRouteToGooglePay() == true) {
|
||||
space(DimensionUnit.DP.toPixels(68f).toInt())
|
||||
|
||||
space(DimensionUnit.DP.toPixels(92f).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(
|
||||
@@ -117,9 +140,16 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ExpiredBadgeBottomSheetDialogFragment::class.java)
|
||||
|
||||
@JvmStatic
|
||||
fun show(badge: Badge, cancellationReason: UnexpectedSubscriptionCancellation?, fragmentManager: FragmentManager) {
|
||||
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.Builder(badge, cancellationReason?.status).build()
|
||||
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()
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class BadgesOverviewFragment : DSLSettingsFragment(
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
Badge.register(adapter) { badge, _, isFaded ->
|
||||
if (badge.isExpired() || isFaded) {
|
||||
findNavController().safeNavigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToExpiredBadgeDialog(badge, null))
|
||||
findNavController().safeNavigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToExpiredBadgeDialog(badge, null, null))
|
||||
} else {
|
||||
ViewBadgeBottomSheetDialogFragment.show(parentFragmentManager, Recipient.self().id, badge)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.signal.core.util.AppUtil
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
@@ -38,6 +39,7 @@ import org.thoughtcrime.securesms.payments.DataExportUtil
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.max
|
||||
@@ -409,6 +411,13 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
enqueueSubscriptionKeepAlive()
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_badges_set_error_state),
|
||||
onClick = {
|
||||
findNavController().safeNavigate(InternalSettingsFragmentDirections.actionInternalSettingsFragmentToDonorErrorConfigurationFragment())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
dividerPref()
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.donor
|
||||
|
||||
import androidx.fragment.app.viewModels
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
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.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
|
||||
class DonorErrorConfigurationFragment : DSLSettingsFragment() {
|
||||
|
||||
private val viewModel: DonorErrorConfigurationViewModel by viewModels()
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: DonorErrorConfigurationState): DSLConfiguration {
|
||||
return configure {
|
||||
radioListPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_donor_error_expired_badge),
|
||||
selected = state.badges.indexOf(state.selectedBadge),
|
||||
listItems = state.badges.map { it.name }.toTypedArray(),
|
||||
onSelected = { viewModel.setSelectedBadge(it) }
|
||||
)
|
||||
|
||||
radioListPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_donor_error_cancelation_reason),
|
||||
selected = UnexpectedSubscriptionCancellation.values().indexOf(state.selectedUnexpectedSubscriptionCancellation),
|
||||
listItems = UnexpectedSubscriptionCancellation.values().map { it.status }.toTypedArray(),
|
||||
onSelected = { viewModel.setSelectedUnexpectedSubscriptionCancellation(it) },
|
||||
isEnabled = state.selectedBadge == null || state.selectedBadge.isSubscription()
|
||||
)
|
||||
|
||||
radioListPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_donor_error_charge_failure),
|
||||
selected = StripeDeclineCode.Code.values().indexOf(state.selectedStripeDeclineCode),
|
||||
listItems = StripeDeclineCode.Code.values().map { it.code }.toTypedArray(),
|
||||
onSelected = { viewModel.setStripeDeclineCode(it) },
|
||||
isEnabled = state.selectedBadge == null || state.selectedBadge.isSubscription()
|
||||
)
|
||||
|
||||
primaryButton(
|
||||
text = DSLSettingsText.from(R.string.preferences__internal_donor_error_save_and_finish),
|
||||
onClick = {
|
||||
lifecycleDisposable += viewModel.save().subscribe { requireActivity().finish() }
|
||||
}
|
||||
)
|
||||
|
||||
secondaryButtonNoOutline(
|
||||
text = DSLSettingsText.from(R.string.preferences__internal_donor_error_clear),
|
||||
onClick = {
|
||||
lifecycleDisposable += viewModel.clear().subscribe()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.donor
|
||||
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation
|
||||
|
||||
data class DonorErrorConfigurationState(
|
||||
val badges: List<Badge> = emptyList(),
|
||||
val selectedBadge: Badge? = null,
|
||||
val selectedUnexpectedSubscriptionCancellation: UnexpectedSubscriptionCancellation? = null,
|
||||
val selectedStripeDeclineCode: StripeDeclineCode.Code? = null
|
||||
)
|
||||
@@ -0,0 +1,152 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.donor
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import java.util.Locale
|
||||
|
||||
class DonorErrorConfigurationViewModel : ViewModel() {
|
||||
|
||||
private val store = RxStore(DonorErrorConfigurationState())
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
val state: Flowable<DonorErrorConfigurationState> = store.stateFlowable
|
||||
|
||||
init {
|
||||
val giftBadges: Single<List<Badge>> = ApplicationDependencies.getDonationsService()
|
||||
.getGiftBadges(Locale.getDefault())
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { results -> results.values.map { Badges.fromServiceBadge(it) } }
|
||||
.subscribeOn(Schedulers.io())
|
||||
|
||||
val boostBadges: Single<List<Badge>> = ApplicationDependencies.getDonationsService()
|
||||
.getBoostBadge(Locale.getDefault())
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { listOf(Badges.fromServiceBadge(it)) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
|
||||
val subscriptionBadges: Single<List<Badge>> = ApplicationDependencies.getDonationsService()
|
||||
.getSubscriptionLevels(Locale.getDefault())
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { levels -> levels.levels.values.map { Badges.fromServiceBadge(it.badge) } }
|
||||
.subscribeOn(Schedulers.io())
|
||||
|
||||
disposables += Single.zip(giftBadges, boostBadges, subscriptionBadges) { g, b, s ->
|
||||
g + b + s
|
||||
}.subscribe { badges ->
|
||||
store.update { it.copy(badges = badges) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun setSelectedBadge(badgeIndex: Int) {
|
||||
store.update {
|
||||
it.copy(selectedBadge = if (badgeIndex in it.badges.indices) it.badges[badgeIndex] else null)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSelectedUnexpectedSubscriptionCancellation(unexpectedSubscriptionCancellationIndex: Int) {
|
||||
store.update {
|
||||
it.copy(
|
||||
selectedUnexpectedSubscriptionCancellation = if (unexpectedSubscriptionCancellationIndex in UnexpectedSubscriptionCancellation.values().indices) {
|
||||
UnexpectedSubscriptionCancellation.values()[unexpectedSubscriptionCancellationIndex]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setStripeDeclineCode(stripeDeclineCodeIndex: Int) {
|
||||
store.update {
|
||||
it.copy(
|
||||
selectedStripeDeclineCode = if (stripeDeclineCodeIndex in StripeDeclineCode.Code.values().indices) {
|
||||
StripeDeclineCode.Code.values()[stripeDeclineCodeIndex]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun save(): Completable {
|
||||
val snapshot = store.state
|
||||
val saveState = Completable.fromAction {
|
||||
synchronized(SubscriptionReceiptRequestResponseJob.MUTEX) {
|
||||
when {
|
||||
snapshot.selectedBadge?.isGift() == true -> handleGiftExpiration(snapshot)
|
||||
snapshot.selectedBadge?.isBoost() == true -> handleBoostExpiration(snapshot)
|
||||
snapshot.selectedBadge?.isSubscription() == true -> handleSubscriptionExpiration(snapshot)
|
||||
else -> handleSubscriptionPaymentFailure(snapshot)
|
||||
}
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
|
||||
return clear().andThen(saveState)
|
||||
}
|
||||
|
||||
fun clear(): Completable {
|
||||
return Completable.fromAction {
|
||||
synchronized(SubscriptionReceiptRequestResponseJob.MUTEX) {
|
||||
SignalStore.donationsValues().setExpiredBadge(null)
|
||||
SignalStore.donationsValues().setExpiredGiftBadge(null)
|
||||
SignalStore.donationsValues().unexpectedSubscriptionCancelationReason = null
|
||||
SignalStore.donationsValues().unexpectedSubscriptionCancelationTimestamp = 0L
|
||||
SignalStore.donationsValues().setUnexpectedSubscriptionCancelationChargeFailure(null)
|
||||
}
|
||||
|
||||
store.update {
|
||||
it.copy(
|
||||
selectedStripeDeclineCode = null,
|
||||
selectedUnexpectedSubscriptionCancellation = null,
|
||||
selectedBadge = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBoostExpiration(state: DonorErrorConfigurationState) {
|
||||
SignalStore.donationsValues().setExpiredBadge(state.selectedBadge)
|
||||
}
|
||||
|
||||
private fun handleGiftExpiration(state: DonorErrorConfigurationState) {
|
||||
SignalStore.donationsValues().setExpiredGiftBadge(state.selectedBadge)
|
||||
}
|
||||
|
||||
private fun handleSubscriptionExpiration(state: DonorErrorConfigurationState) {
|
||||
SignalStore.donationsValues().setExpiredBadge(state.selectedBadge)
|
||||
handleSubscriptionPaymentFailure(state)
|
||||
}
|
||||
|
||||
private fun handleSubscriptionPaymentFailure(state: DonorErrorConfigurationState) {
|
||||
SignalStore.donationsValues().unexpectedSubscriptionCancelationReason = state.selectedUnexpectedSubscriptionCancellation?.status
|
||||
SignalStore.donationsValues().unexpectedSubscriptionCancelationTimestamp = System.currentTimeMillis()
|
||||
SignalStore.donationsValues().setUnexpectedSubscriptionCancelationChargeFailure(
|
||||
state.selectedStripeDeclineCode?.let {
|
||||
ActiveSubscription.ChargeFailure(
|
||||
it.code,
|
||||
"Test Charge Failure",
|
||||
"Test Network Status",
|
||||
"Test Network Reason",
|
||||
"Test"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.errors
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
@StringRes
|
||||
fun StripeDeclineCode.mapToErrorStringResource(): Int {
|
||||
return when (this) {
|
||||
is StripeDeclineCode.Known -> when (this.code) {
|
||||
StripeDeclineCode.Code.APPROVE_WITH_ID -> R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again
|
||||
StripeDeclineCode.Code.CALL_ISSUER -> R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again_if_the_problem
|
||||
StripeDeclineCode.Code.CARD_NOT_SUPPORTED -> R.string.DeclineCode__your_card_does_not_support_this_type_of_purchase
|
||||
StripeDeclineCode.Code.EXPIRED_CARD -> R.string.DeclineCode__your_card_has_expired
|
||||
StripeDeclineCode.Code.INCORRECT_NUMBER -> R.string.DeclineCode__your_card_number_is_incorrect
|
||||
StripeDeclineCode.Code.INCORRECT_CVC -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect
|
||||
StripeDeclineCode.Code.INSUFFICIENT_FUNDS -> R.string.DeclineCode__your_card_does_not_have_sufficient_funds
|
||||
StripeDeclineCode.Code.INVALID_CVC -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect
|
||||
StripeDeclineCode.Code.INVALID_EXPIRY_MONTH -> R.string.DeclineCode__the_expiration_month
|
||||
StripeDeclineCode.Code.INVALID_EXPIRY_YEAR -> R.string.DeclineCode__the_expiration_year
|
||||
StripeDeclineCode.Code.INVALID_NUMBER -> R.string.DeclineCode__your_card_number_is_incorrect
|
||||
StripeDeclineCode.Code.ISSUER_NOT_AVAILABLE -> R.string.DeclineCode__try_completing_the_payment_again
|
||||
StripeDeclineCode.Code.PROCESSING_ERROR -> R.string.DeclineCode__try_again
|
||||
StripeDeclineCode.Code.REENTER_TRANSACTION -> R.string.DeclineCode__try_again
|
||||
else -> R.string.DeclineCode__try_another_payment_method_or_contact_your_bank
|
||||
}
|
||||
else -> R.string.DeclineCode__try_another_payment_method_or_contact_your_bank
|
||||
}
|
||||
}
|
||||
|
||||
fun StripeDeclineCode.shouldRouteToGooglePay(): Boolean {
|
||||
return when (this) {
|
||||
is StripeDeclineCode.Known -> when (this.code) {
|
||||
StripeDeclineCode.Code.APPROVE_WITH_ID -> true
|
||||
StripeDeclineCode.Code.CALL_ISSUER -> true
|
||||
StripeDeclineCode.Code.CARD_NOT_SUPPORTED -> false
|
||||
StripeDeclineCode.Code.EXPIRED_CARD -> true
|
||||
StripeDeclineCode.Code.INCORRECT_NUMBER -> true
|
||||
StripeDeclineCode.Code.INCORRECT_CVC -> true
|
||||
StripeDeclineCode.Code.INSUFFICIENT_FUNDS -> false
|
||||
StripeDeclineCode.Code.INVALID_CVC -> true
|
||||
StripeDeclineCode.Code.INVALID_EXPIRY_MONTH -> true
|
||||
StripeDeclineCode.Code.INVALID_EXPIRY_YEAR -> true
|
||||
StripeDeclineCode.Code.INVALID_NUMBER -> true
|
||||
StripeDeclineCode.Code.ISSUER_NOT_AVAILABLE -> false
|
||||
StripeDeclineCode.Code.PROCESSING_ERROR -> false
|
||||
StripeDeclineCode.Code.REENTER_TRANSACTION -> false
|
||||
else -> false
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.errors
|
||||
/**
|
||||
* Error states that can occur if we detect that a user's subscription has been cancelled and the manual
|
||||
* cancellation flag is not set.
|
||||
*
|
||||
* This status is taken directly from the ActiveSubscription object, and is set in the Subscription's
|
||||
* keep-alive and subscription receipt redemption jobs.
|
||||
*/
|
||||
enum class UnexpectedSubscriptionCancellation(val status: String) {
|
||||
PAST_DUE("past_due"),
|
||||
|
||||
@@ -68,7 +68,7 @@ class ManageDonationsFragment : DSLSettingsFragment(), ExpiredGiftSheet.Callback
|
||||
|
||||
val expiredGiftBadge = SignalStore.donationsValues().getExpiredGiftBadge()
|
||||
if (expiredGiftBadge != null) {
|
||||
SignalStore.donationsValues().setExpiredBadge(null)
|
||||
SignalStore.donationsValues().setExpiredGiftBadge(null)
|
||||
ExpiredGiftSheet.show(childFragmentManager, expiredGiftBadge)
|
||||
}
|
||||
|
||||
|
||||
@@ -396,7 +396,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
|
||||
if (expiredBadge.isBoost() || !SignalStore.donationsValues().isUserManuallyCancelled()) {
|
||||
Log.w(TAG, "Displaying bottom sheet for an expired badge", true);
|
||||
ExpiredBadgeBottomSheetDialogFragment.show(expiredBadge, unexpectedSubscriptionCancellation, getParentFragmentManager());
|
||||
ExpiredBadgeBottomSheetDialogFragment.show(
|
||||
expiredBadge,
|
||||
unexpectedSubscriptionCancellation,
|
||||
SignalStore.donationsValues().getUnexpectedSubscriptionCancelationChargeFailure(),
|
||||
getParentFragmentManager());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.donations.StripeDeclineCode;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations;
|
||||
@@ -152,9 +153,15 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
||||
Log.w(TAG, "Subscription payment charge failure code: " + chargeFailure.getCode() + ", message: " + chargeFailure.getMessage(), true);
|
||||
}
|
||||
|
||||
Log.w(TAG, "Subscription payment failure in active subscription response (status = " + subscription.getStatus() + ").", true);
|
||||
onPaymentFailure(subscription.getStatus(), chargeFailure, subscription.getEndOfCurrentPeriod());
|
||||
throw new Exception("Subscription has a payment failure: " + subscription.getStatus());
|
||||
if (isForKeepAlive) {
|
||||
Log.w(TAG, "Subscription payment failure in active subscription response (status = " + subscription.getStatus() + ").", true);
|
||||
onPaymentFailure(subscription.getStatus(), chargeFailure, subscription.getEndOfCurrentPeriod(), true);
|
||||
throw new Exception("Active subscription hit a payment failure: " + subscription.getStatus());
|
||||
} else {
|
||||
Log.w(TAG, "New subscription has hit a payment failure. (status = " + subscription.getStatus() + ").", true);
|
||||
onPaymentFailure(subscription.getStatus(), chargeFailure, subscription.getEndOfCurrentPeriod(), false);
|
||||
throw new Exception("New subscription has hit a payment failure: " + subscription.getStatus());
|
||||
}
|
||||
} else if (!subscription.isActive()) {
|
||||
ActiveSubscription.ChargeFailure chargeFailure = activeSubscription.getChargeFailure();
|
||||
if (chargeFailure != null) {
|
||||
@@ -269,15 +276,31 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
||||
}
|
||||
}
|
||||
|
||||
private void onPaymentFailure(@Nullable String status, @Nullable ActiveSubscription.ChargeFailure chargeFailure, long timestamp) {
|
||||
/**
|
||||
* Handles state updates and error routing for a payment failure.
|
||||
*
|
||||
* There are two ways this could go, depending on whether the job was created for a keep-alive chain.
|
||||
*
|
||||
* 1. In the case of a normal chain (new subscription) We simply route the error out to the user. The payment failure would have occurred while trying to
|
||||
* charge for the first month of their subscription, and are likely still on the "Subscribe" screen, so we can just display a dialog.
|
||||
* 1. In the case of a keep-alive event, we want to book-keep the error to show the user on a subsequent launch, and we want to sync our failure state to
|
||||
* linked devices.
|
||||
*/
|
||||
private void onPaymentFailure(@NonNull String status, @Nullable ActiveSubscription.ChargeFailure chargeFailure, long timestamp, boolean isForKeepAlive) {
|
||||
SignalStore.donationsValues().setShouldCancelSubscriptionBeforeNextSubscribeAttempt(true);
|
||||
if (status == null) {
|
||||
DonationError.routeDonationError(context, DonationError.genericPaymentFailure(getErrorSource()));
|
||||
} else {
|
||||
if (isForKeepAlive){
|
||||
Log.d(TAG, "Is for a keep-alive and we have a status. Setting UnexpectedSubscriptionCancelation state...", true);
|
||||
SignalStore.donationsValues().setUnexpectedSubscriptionCancelationChargeFailure(chargeFailure);
|
||||
SignalStore.donationsValues().setUnexpectedSubscriptionCancelationReason(status);
|
||||
SignalStore.donationsValues().setUnexpectedSubscriptionCancelationTimestamp(timestamp);
|
||||
MultiDeviceSubscriptionSyncRequestJob.enqueue();
|
||||
} else {
|
||||
Log.d(TAG, "Not for a keep-alive and we have a status. Routing a payment setup error...", true);
|
||||
DonationError.routeDonationError(context, new DonationError.PaymentSetupError.DeclinedError(
|
||||
getErrorSource(),
|
||||
new Exception("Got a failure status from the subscription object."),
|
||||
StripeDeclineCode.Companion.getFromCode(status)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user