mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 18:30:20 +01:00
Add decline code messages into expiration sheet.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user