diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/CantProcessSubscriptionPaymentBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/CantProcessSubscriptionPaymentBottomSheetDialogFragment.kt deleted file mode 100644 index ca86348146..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/CantProcessSubscriptionPaymentBottomSheetDialogFragment.kt +++ /dev/null @@ -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() - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/ExpiredBadgeBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/ExpiredBadgeBottomSheetDialogFragment.kt deleted file mode 100644 index 48b0d4c29e..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/ExpiredBadgeBottomSheetDialogFragment.kt +++ /dev/null @@ -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) - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/ExpiredOneTimeBadgeBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/ExpiredOneTimeBadgeBottomSheetDialogFragment.kt new file mode 100644 index 0000000000..4e72e13589 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/ExpiredOneTimeBadgeBottomSheetDialogFragment.kt @@ -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) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/MonthlyDonationCanceledBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/MonthlyDonationCanceledBottomSheetDialogFragment.kt new file mode 100644 index 0000000000..0c3812c71e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/MonthlyDonationCanceledBottomSheetDialogFragment.kt @@ -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)) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/donor/InternalDonorErrorConfigurationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/donor/InternalDonorErrorConfigurationViewModel.kt index 045ad46d16..5b5555ed30 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/donor/InternalDonorErrorConfigurationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/donor/InternalDonorErrorConfigurationViewModel.kt @@ -144,19 +144,21 @@ class InternalDonorErrorConfigurationViewModel : ViewModel() { private fun handleSubscriptionExpiration(state: InternalDonorErrorConfigurationState) { SignalStore.donationsValues().setExpiredBadge(state.selectedBadge) + SignalStore.donationsValues().clearUserManuallyCancelled() handleSubscriptionPaymentFailure(state) } private fun handleSubscriptionPaymentFailure(state: InternalDonorErrorConfigurationState) { SignalStore.donationsValues().unexpectedSubscriptionCancelationReason = state.selectedUnexpectedSubscriptionCancellation?.status SignalStore.donationsValues().unexpectedSubscriptionCancelationTimestamp = System.currentTimeMillis() + SignalStore.donationsValues().showMonthlyDonationCanceledDialog = true SignalStore.donationsValues().setUnexpectedSubscriptionCancelationChargeFailure( state.selectedStripeDeclineCode?.let { ActiveSubscription.ChargeFailure( it.code, "Test Charge Failure", "Test Network Status", - "Test Network Reason", + it.code, "Test" ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrors.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrors.kt index b0c5ba54aa..0c1137cc66 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrors.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrors.kt @@ -31,17 +31,17 @@ fun StripeFailureCode.mapToErrorStringResource(): Int { 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.APPROVE_WITH_ID -> R.string.DeclineCode__verify_your_card_details_are_correct_and_try_again + StripeDeclineCode.Code.CALL_ISSUER -> R.string.DeclineCode__verify_your_card_details_are_correct_and_try_again_if_the_problem_continues 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.EXPIRED_CARD -> R.string.DeclineCode__your_card_has_expired_verify_your_card_details + StripeDeclineCode.Code.INCORRECT_NUMBER -> R.string.DeclineCode__your_card_number_is_incorrect_verify_your_card_details + StripeDeclineCode.Code.INCORRECT_CVC -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect_verify_your_card_details 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.INVALID_CVC -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect_verify_your_card_details + StripeDeclineCode.Code.INVALID_EXPIRY_MONTH -> R.string.DeclineCode__the_expiration_month_on_your_card_is_incorrect + StripeDeclineCode.Code.INVALID_EXPIRY_YEAR -> R.string.DeclineCode__the_expiration_year_on_your_card_is_incorrect + StripeDeclineCode.Code.INVALID_NUMBER -> R.string.DeclineCode__your_card_number_is_incorrect_verify_your_card_details 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 @@ -50,26 +50,3 @@ fun StripeDeclineCode.mapToErrorStringResource(): Int { 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 - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt index ecb733aa92..ad5d657be1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt @@ -55,7 +55,7 @@ class ManageDonationsFragment : companion object { private val alertedIdealDonations = mutableSetOf() - private const val DONATE_TROUBLESHOOTING_URL = "https://support.signal.org/hc/articles/360031949872#fix" + const val DONATE_TROUBLESHOOTING_URL = "https://support.signal.org/hc/articles/360031949872#fix" } private val supportTechSummary: CharSequence by lazy { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsState.kt index 03c31bb8c8..107f944644 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsState.kt @@ -27,7 +27,7 @@ data class ManageDonationsState( private fun getStateFromActiveSubscription(activeSubscription: ActiveSubscription): RedemptionState? { return when { - activeSubscription.isFailedPayment -> RedemptionState.FAILED + activeSubscription.isFailedPayment && !activeSubscription.isPastDue -> RedemptionState.FAILED activeSubscription.isPendingBankTransfer -> RedemptionState.IS_PENDING_BANK_TRANSFER activeSubscription.isInProgress -> RedemptionState.IN_PROGRESS else -> null diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index 06c76e5015..e65603934f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -90,8 +90,8 @@ import org.thoughtcrime.securesms.MuteDialog; import org.thoughtcrime.securesms.NewConversationActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.badges.models.Badge; -import org.thoughtcrime.securesms.badges.self.expired.CantProcessSubscriptionPaymentBottomSheetDialogFragment; -import org.thoughtcrime.securesms.badges.self.expired.ExpiredBadgeBottomSheetDialogFragment; +import org.thoughtcrime.securesms.badges.self.expired.MonthlyDonationCanceledBottomSheetDialogFragment; +import org.thoughtcrime.securesms.badges.self.expired.ExpiredOneTimeBadgeBottomSheetDialogFragment; import org.thoughtcrime.securesms.components.Material3SearchToolbar; import org.thoughtcrime.securesms.components.RatingManager; import org.thoughtcrime.securesms.components.SignalProgressDialog; @@ -167,7 +167,6 @@ import org.thoughtcrime.securesms.stories.tabs.ConversationListTab; import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel; import org.thoughtcrime.securesms.util.AppForegroundObserver; import org.thoughtcrime.securesms.util.AppStartup; -import org.thoughtcrime.securesms.util.BottomSheetUtil; import org.thoughtcrime.securesms.util.CachedInflater; import org.thoughtcrime.securesms.util.ConversationUtil; import org.thoughtcrime.securesms.util.PlayStoreUtil; @@ -491,7 +490,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode Badge expiredBadge = SignalStore.donationsValues().getExpiredBadge(); String subscriptionCancellationReason = SignalStore.donationsValues().getUnexpectedSubscriptionCancelationReason(); UnexpectedSubscriptionCancellation unexpectedSubscriptionCancellation = UnexpectedSubscriptionCancellation.fromStatus(subscriptionCancellationReason); - boolean isDisplayingSubscriptionFailure = false; long subscriptionFailureTimestamp = SignalStore.donationsValues().getUnexpectedSubscriptionCancelationTimestamp(); long subscriptionFailureWatermark = SignalStore.donationsValues().getUnexpectedSubscriptionCancelationWatermark(); boolean isWatermarkPriorToTimestamp = subscriptionFailureWatermark < subscriptionFailureTimestamp; @@ -502,9 +500,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode isWatermarkPriorToTimestamp) { Log.w(TAG, "Displaying bottom sheet for unexpected cancellation: " + unexpectedSubscriptionCancellation, true); - new CantProcessSubscriptionPaymentBottomSheetDialogFragment().show(getChildFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG); + MonthlyDonationCanceledBottomSheetDialogFragment.show(getChildFragmentManager()); SignalStore.donationsValues().setUnexpectedSubscriptionCancelationWatermark(subscriptionFailureTimestamp); - isDisplayingSubscriptionFailure = true; } else if (unexpectedSubscriptionCancellation != null && SignalStore.donationsValues().isUserManuallyCancelled()) { Log.w(TAG, "Unexpected cancellation detected but not displaying dialog because user manually cancelled their subscription: " + unexpectedSubscriptionCancellation, true); SignalStore.donationsValues().setUnexpectedSubscriptionCancelationWatermark(subscriptionFailureTimestamp); @@ -513,17 +510,16 @@ public class ConversationListFragment extends MainFragment implements ActionMode SignalStore.donationsValues().setUnexpectedSubscriptionCancelationWatermark(subscriptionFailureTimestamp); } - if (expiredBadge != null && !isDisplayingSubscriptionFailure) { + if (expiredBadge != null && expiredBadge.isBoost()) { SignalStore.donationsValues().setExpiredBadge(null); - if (expiredBadge.isBoost() || !SignalStore.donationsValues().isUserManuallyCancelled()) { - Log.w(TAG, "Displaying bottom sheet for an expired badge", true); - ExpiredBadgeBottomSheetDialogFragment.show( - expiredBadge, - unexpectedSubscriptionCancellation, - SignalStore.donationsValues().getUnexpectedSubscriptionCancelationChargeFailure(), - getParentFragmentManager()); - } + Log.w(TAG, "Displaying bottom sheet for an expired badge", true); + ExpiredOneTimeBadgeBottomSheetDialogFragment.show( + expiredBadge, + unexpectedSubscriptionCancellation, + SignalStore.donationsValues().getUnexpectedSubscriptionCancelationChargeFailure(), + getParentFragmentManager() + ); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java index 6f43327080..c7824d8164 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java @@ -72,7 +72,6 @@ public class DonationReceiptRedemptionJob extends BaseJob { .addConstraint(NetworkConstraint.KEY) .setQueue(SUBSCRIPTION_QUEUE + (isLongRunningDonationPaymentType ? LONG_RUNNING_QUEUE_SUFFIX : "")) .setMaxAttempts(Parameters.UNLIMITED) - .setMaxInstancesForQueue(1) .setLifespan(TimeUnit.DAYS.toMillis(1)) .build()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java index 1c77e4b95f..76e7b296ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java @@ -109,19 +109,6 @@ public class SubscriptionKeepAliveJob extends BaseJob { return; } - if (activeSubscription.isFailedPayment()) { - Log.i(TAG, "User has a subscription with a failed payment. Marking the payment failure. Status message: " + activeSubscription.getActiveSubscription().getStatus(), true); - SignalStore.donationsValues().setUnexpectedSubscriptionCancelationChargeFailure(activeSubscription.getChargeFailure()); - SignalStore.donationsValues().setUnexpectedSubscriptionCancelationReason(activeSubscription.getActiveSubscription().getStatus()); - SignalStore.donationsValues().setUnexpectedSubscriptionCancelationTimestamp(activeSubscription.getActiveSubscription().getEndOfCurrentPeriod()); - return; - } - - if (!activeSubscription.getActiveSubscription().isActive()) { - Log.i(TAG, "User has an inactive subscription. Status message: " + activeSubscription.getActiveSubscription().getStatus() + " Exiting.", true); - return; - } - DonationRedemptionJobStatus status = DonationRedemptionJobWatcher.getSubscriptionRedemptionJobStatus(); if (status != DonationRedemptionJobStatus.None.INSTANCE && status != DonationRedemptionJobStatus.FailedSubscription.INSTANCE) { Log.i(TAG, "Already trying to redeem donation, current status: " + status.getClass().getSimpleName(), true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java index 044a6c9b7e..0cfe6180fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java @@ -15,6 +15,7 @@ import org.signal.libsignal.zkgroup.receipts.ReceiptCredential; import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation; import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequestContext; import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse; +import org.thoughtcrime.securesms.components.settings.app.subscription.DonationsConfigurationExtensionsKt; import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError; import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource; import org.thoughtcrime.securesms.components.settings.app.subscription.errors.PayPalDeclineCode; @@ -30,11 +31,13 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.subscription.Subscriber; import org.signal.core.util.Base64; +import org.whispersystems.signalservice.api.services.DonationsService; import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription; import org.whispersystems.signalservice.api.subscriptions.SubscriberId; import org.whispersystems.signalservice.internal.ServiceResponse; import java.io.IOException; +import java.util.Locale; import java.util.concurrent.TimeUnit; import okio.ByteString; @@ -72,7 +75,6 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { .Builder() .addConstraint(NetworkConstraint.KEY) .setQueue("ReceiptRedemption") - .setMaxInstancesForQueue(1) .setLifespan(terminalDonation.isLongRunningPaymentMethod ? TimeUnit.DAYS.toMillis(30) : TimeUnit.DAYS.toMillis(1)) .setMaxAttempts(Parameters.UNLIMITED) .build(), @@ -174,12 +176,11 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { } if (isForKeepAlive) { - Log.w(TAG, "Subscription payment failure in active subscription response (status = " + subscription.getStatus() + ").", true); - onPaymentFailure(subscription.getStatus(), subscription.getProcessor(), chargeFailure, subscription.getEndOfCurrentPeriod(), true); - throw new Exception("Active subscription hit a payment failure: " + subscription.getStatus()); + Log.w(TAG, "Subscription payment failure in active subscription response (status = " + subscription.getStatus() + "). Payment could still be retried by processor.", true); + throw new Exception("Payment renewal is in retry state, let keep-alive job restart process"); } else { Log.w(TAG, "New subscription has hit a payment failure. (status = " + subscription.getStatus() + ").", true); - onPaymentFailure(subscription.getStatus(), subscription.getProcessor(), chargeFailure, subscription.getEndOfCurrentPeriod(), false); + onPaymentFailure(subscription, chargeFailure, false); throw new Exception("New subscription has hit a payment failure: " + subscription.getStatus()); } } else if (!subscription.isActive()) { @@ -189,16 +190,19 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { if (!isForKeepAlive) { Log.w(TAG, "Initial subscription payment failed, treating as a permanent failure."); - onPaymentFailure(subscription.getStatus(), subscription.getProcessor(), chargeFailure, subscription.getEndOfCurrentPeriod(), false); + onPaymentFailure(subscription, chargeFailure, false); throw new Exception("New subscription has hit a payment failure."); } } + if (isForKeepAlive && subscription.isCanceled()) { + Log.w(TAG, "Permanent payment failure in renewing subscription. (status = " + subscription.getStatus() + ").", true); + onPaymentFailure(subscription, chargeFailure, true); + throw new Exception(); + } + Log.w(TAG, "Subscription is not yet active. Status: " + subscription.getStatus(), true); throw new RetryableException(); - } else if (subscription.isCanceled()) { - Log.w(TAG, "Subscription is marked as cancelled, but it's possible that the user cancelled and then later tried to resubscribe. Scheduling a retry.", true); - throw new RetryableException(); } else { Log.i(TAG, "Subscription is valid, proceeding with request for ReceiptCredentialResponse", true); long storedEndOfPeriod = SignalStore.donationsValues().getLastEndOfPeriod(); @@ -353,15 +357,21 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { * 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, @NonNull ActiveSubscription.Processor processor, @Nullable ActiveSubscription.ChargeFailure chargeFailure, long timestamp, boolean isForKeepAlive) { + private void onPaymentFailure(@NonNull ActiveSubscription.Subscription subscription, @Nullable ActiveSubscription.ChargeFailure chargeFailure, boolean isForKeepAlive) { SignalStore.donationsValues().setShouldCancelSubscriptionBeforeNextSubscribeAttempt(true); if (isForKeepAlive) { - Log.d(TAG, "Is for a keep-alive and we have a status. Setting UnexpectedSubscriptionCancelation state...", true); + Log.d(TAG, "Subscription canceled during keep-alive. Setting UnexpectedSubscriptionCancelation state...", true); SignalStore.donationsValues().setUnexpectedSubscriptionCancelationChargeFailure(chargeFailure); - SignalStore.donationsValues().setUnexpectedSubscriptionCancelationReason(status); - SignalStore.donationsValues().setUnexpectedSubscriptionCancelationTimestamp(timestamp); + SignalStore.donationsValues().setUnexpectedSubscriptionCancelationReason(subscription.getStatus()); + SignalStore.donationsValues().setUnexpectedSubscriptionCancelationTimestamp(subscription.getEndOfCurrentPeriod()); + SignalStore.donationsValues().setShowMonthlyDonationCanceledDialog(true); + + ApplicationDependencies.getDonationsService().getDonationsConfiguration(Locale.getDefault()).getResult().ifPresent(config -> { + SignalStore.donationsValues().setExpiredBadge(DonationsConfigurationExtensionsKt.getBadge(config, subscription.getLevel())); + }); + MultiDeviceSubscriptionSyncRequestJob.enqueue(); - } else if (chargeFailure != null && processor == ActiveSubscription.Processor.STRIPE) { + } else if (chargeFailure != null && subscription.getProcessor() == ActiveSubscription.Processor.STRIPE) { Log.d(TAG, "Stripe charge failure detected: " + chargeFailure, true); StripeDeclineCode declineCode = StripeDeclineCode.Companion.getFromCode(chargeFailure.getOutcomeNetworkReason()); @@ -399,7 +409,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { Log.w(TAG, "Not for a keep-alive and we have a charge failure. Routing a payment setup error...", true); onPaymentFailedError(paymentSetupError); - } else if (chargeFailure != null && processor == ActiveSubscription.Processor.BRAINTREE) { + } else if (chargeFailure != null && subscription.getProcessor() == ActiveSubscription.Processor.BRAINTREE) { Log.d(TAG, "PayPal charge failure detected: " + chargeFailure, true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt index 1e2d2add15..4774c2b5d6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt @@ -387,7 +387,7 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign var unexpectedSubscriptionCancelationWatermark: Long by longValue(SUBSCRIPTION_CANCELATION_WATERMARK, 0L) @get:JvmName("showCantProcessDialog") - var showCantProcessDialog: Boolean by booleanValue(SHOW_CANT_PROCESS_DIALOG, true) + var showMonthlyDonationCanceledDialog: Boolean by booleanValue(SHOW_CANT_PROCESS_DIALOG, true) /** * Denotes that the previous attempt to subscribe failed in some way. Either an diff --git a/app/src/main/res/navigation/manage_badges.xml b/app/src/main/res/navigation/manage_badges.xml index 853cd68fbb..c4ab71cd8c 100644 --- a/app/src/main/res/navigation/manage_badges.xml +++ b/app/src/main/res/navigation/manage_badges.xml @@ -28,7 +28,7 @@ 2. %1$s Allow full screen notifications + + Monthly donation canceled + + Your recurring monthly donation was canceled. %1$s\n\nYour badge will no longer be visible on your profile. %2$s + + Learn more + + Renew subscription + + Not now + diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/subscriptions/ActiveSubscription.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/subscriptions/ActiveSubscription.java index fee8d1fe3b..3bfb782431 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/subscriptions/ActiveSubscription.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/subscriptions/ActiveSubscription.java @@ -136,7 +136,11 @@ public final class ActiveSubscription { } public boolean isInProgress() { - return activeSubscription != null && !isActive() && !activeSubscription.isFailedPayment(); + return activeSubscription != null && !isActive() && (!activeSubscription.isFailedPayment() || activeSubscription.isPastDue()); + } + + public boolean isPastDue() { + return activeSubscription != null && activeSubscription.isPastDue(); } public boolean isFailedPayment() { @@ -249,6 +253,10 @@ public final class ActiveSubscription { return Status.isPaymentFailed(getStatus()); } + public boolean isPastDue() { + return Status.getStatus(getStatus()) == Status.PAST_DUE; + } + public boolean isCanceled() { return Status.getStatus(getStatus()) == Status.CANCELED; }