kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add decline code messages into expiration sheet.
rodzic
4d8faffb75
commit
6dec6cef27
|
@ -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)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -542,7 +542,16 @@
|
|||
<fragment
|
||||
android:id="@+id/internalSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.InternalSettingsFragment"
|
||||
android:label="internal_settings_fragment" />
|
||||
android:label="internal_settings_fragment" >
|
||||
<action
|
||||
android:id="@+id/action_internalSettingsFragment_to_donorErrorConfigurationFragment"
|
||||
app:destination="@id/donorErrorConfigurationFragment" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/donorErrorConfigurationFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.donor.DonorErrorConfigurationFragment"
|
||||
android:label="donor_error_configuration_fragment" />
|
||||
|
||||
<!-- endregion -->
|
||||
|
||||
|
|
|
@ -41,5 +41,10 @@
|
|||
app:argType="string"
|
||||
app:nullable="true" />
|
||||
|
||||
<argument
|
||||
android:name="charge_failure"
|
||||
app:argType="string"
|
||||
app:nullable="true" />
|
||||
|
||||
</dialog>
|
||||
</navigation>
|
|
@ -2739,6 +2739,7 @@
|
|||
<string name="preferences__internal_badges" translatable="false">Badges</string>
|
||||
<string name="preferences__internal_badges_enqueue_redemption" translatable="false">Enqueue redemption.</string>
|
||||
<string name="preferences__internal_badges_enqueue_keep_alive" translatable="false">Enqueue keep-alive.</string>
|
||||
<string name="preferences__internal_badges_set_error_state" translatable="false">Set error state.</string>
|
||||
<string name="preferences__internal_release_channel" translatable="false">Release channel</string>
|
||||
<string name="preferences__internal_fetch_release_channel" translatable="false">Fetch release channel</string>
|
||||
<string name="preferences__internal_release_channel_set_last_version" translatable="false">Set last version seen back 10 versions</string>
|
||||
|
@ -2751,6 +2752,11 @@
|
|||
<string name="preferences__internal_clear_all_service_ids_description" translatable="false">Clears all known service IDs (except your own). Do not use on your personal device!</string>
|
||||
<string name="preferences__internal_clear_all_profile_keys" translatable="false">Clear all profile keys</string>
|
||||
<string name="preferences__internal_clear_all_profile_keys_description" translatable="false">Clears all known profile keys (except your own). Do not use on your personal device!</string>
|
||||
<string name="preferences__internal_donor_error_expired_badge" translatable="false">Expired Badge</string>
|
||||
<string name="preferences__internal_donor_error_charge_failure" translatable="false">Charge Failure</string>
|
||||
<string name="preferences__internal_donor_error_cancelation_reason" translatable="false">Cancelation Reason</string>
|
||||
<string name="preferences__internal_donor_error_save_and_finish" translatable="false">Save and Finish</string>
|
||||
<string name="preferences__internal_donor_error_clear" translatable="false">Clear</string>
|
||||
|
||||
|
||||
<!-- Payments -->
|
||||
|
@ -4226,8 +4232,12 @@
|
|||
<string name="ExpiredBadgeBottomSheetDialogFragment__your_recurring_monthly_donation_was_automatically">Your recurring monthly donation was automatically cancelled because you were inactive for too long. Your %1$s badge is no longer visible on your profile.</string>
|
||||
<!-- Copy displayed when badge expires after payment failure -->
|
||||
<string name="ExpiredBadgeBottomSheetDialogFragment__your_recurring_monthly_donation_was_canceled">Your recurring monthly donation was cancelled because we couldn\'t process your payment. Your badge is no longer visible on your profile.</string>
|
||||
<!-- Copy displayed when badge expires after a payment failure and we have a displayable charge failure reason -->
|
||||
<string name="ExpiredBadgeBottomSheetDialogFragment__your_recurring_monthly_donation_was_canceled_s">Your recurring monthly donation was cancelled. %1$s Your %2$s badge is no longer visible on your profile.</string>
|
||||
<string name="ExpiredBadgeBottomSheetDialogFragment__you_can">You can keep using Signal but to support the app and reactivate your badge, renew now.</string>
|
||||
<string name="ExpiredBadgeBottomSheetDialogFragment__renew_subscription">Renew subscription</string>
|
||||
<!-- Button label to send user to Google Pay website -->
|
||||
<string name="ExpiredBadgeBottomSheetDialogFragment__go_to_google_pay">Go to Google Pay</string>
|
||||
|
||||
<string name="CantProcessSubscriptionPaymentBottomSheetDialogFragment__cant_process_subscription_payment">Can\'t process subscription payment</string>
|
||||
<string name="CantProcessSubscriptionPaymentBottomSheetDialogFragment__were_having_trouble">We\'re having trouble collecting your Signal Sustainer payment. Make sure your payment method is up to date. If it isn\'t, update it in Google Pay. Signal will try to process the payment again in a few days.</string>
|
||||
|
|
Ładowanie…
Reference in New Issue