mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-03-02 15:36:32 +00:00
Handle donation-driven 440 errors more gracefully.
This commit is contained in:
@@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.Do
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
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.toDonationError
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceSubscriptionSyncRequestJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -28,6 +29,7 @@ import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentInt
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentMethodResponse
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
import org.whispersystems.signalservice.internal.push.DonationProcessor
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.DonationProcessorError
|
||||
|
||||
class PayPalPaymentInProgressViewModel(
|
||||
private val payPalRepository: PayPalRepository,
|
||||
@@ -89,10 +91,10 @@ class PayPalPaymentInProgressViewModel(
|
||||
},
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Failed to update subscription", throwable, true)
|
||||
val donationError: DonationError = if (throwable is DonationError) {
|
||||
throwable
|
||||
} else {
|
||||
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.SUBSCRIPTION)
|
||||
val donationError: DonationError = when (throwable) {
|
||||
is DonationError -> throwable
|
||||
is DonationProcessorError -> throwable.toDonationError(DonationErrorSource.SUBSCRIPTION, PaymentSourceType.PayPal)
|
||||
else -> DonationError.genericBadgeRedemptionFailure(DonationErrorSource.SUBSCRIPTION)
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
|
||||
@@ -166,10 +168,10 @@ class PayPalPaymentInProgressViewModel(
|
||||
Log.w(TAG, "Failure in one-time payment pipeline...", throwable, true)
|
||||
store.update { DonationProcessorStage.FAILED }
|
||||
|
||||
val donationError: DonationError = if (throwable is DonationError) {
|
||||
throwable
|
||||
} else {
|
||||
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.BOOST)
|
||||
val donationError: DonationError = when (throwable) {
|
||||
is DonationError -> throwable
|
||||
is DonationProcessorError -> throwable.toDonationError(request.donateToSignalType.toErrorSource(), PaymentSourceType.PayPal)
|
||||
else -> DonationError.genericBadgeRedemptionFailure(request.donateToSignalType.toErrorSource())
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
},
|
||||
@@ -196,10 +198,10 @@ class PayPalPaymentInProgressViewModel(
|
||||
Log.w(TAG, "Failure in monthly payment pipeline...", throwable, true)
|
||||
store.update { DonationProcessorStage.FAILED }
|
||||
|
||||
val donationError: DonationError = if (throwable is DonationError) {
|
||||
throwable
|
||||
} else {
|
||||
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.SUBSCRIPTION)
|
||||
val donationError: DonationError = when (throwable) {
|
||||
is DonationError -> throwable
|
||||
is DonationProcessorError -> throwable.toDonationError(DonationErrorSource.SUBSCRIPTION, PaymentSourceType.PayPal)
|
||||
else -> DonationError.genericBadgeRedemptionFailure(DonationErrorSource.SUBSCRIPTION)
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
},
|
||||
|
||||
@@ -23,12 +23,14 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.Do
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
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.toDonationError
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceSubscriptionSyncRequestJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
import org.whispersystems.signalservice.internal.push.DonationProcessor
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.DonationProcessorError
|
||||
|
||||
class StripePaymentInProgressViewModel(
|
||||
private val stripeRepository: StripeRepository,
|
||||
@@ -148,10 +150,10 @@ class StripePaymentInProgressViewModel(
|
||||
}
|
||||
.flatMapCompletable { stripeRepository.setDefaultPaymentMethod(it, paymentSourceProvider.paymentSourceType) }
|
||||
.onErrorResumeNext {
|
||||
if (it is DonationError) {
|
||||
Completable.error(it)
|
||||
} else {
|
||||
Completable.error(DonationError.getPaymentSetupError(DonationErrorSource.SUBSCRIPTION, it, paymentSourceProvider.paymentSourceType))
|
||||
when {
|
||||
it is DonationError -> Completable.error(it)
|
||||
it is DonationProcessorError -> Completable.error(it.toDonationError(DonationErrorSource.SUBSCRIPTION, paymentSourceProvider.paymentSourceType))
|
||||
else -> Completable.error(DonationError.getPaymentSetupError(DonationErrorSource.SUBSCRIPTION, it, paymentSourceProvider.paymentSourceType))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,10 +213,10 @@ class StripePaymentInProgressViewModel(
|
||||
Log.w(TAG, "Failure in one-time payment pipeline...", throwable, true)
|
||||
store.update { DonationProcessorStage.FAILED }
|
||||
|
||||
val donationError: DonationError = if (throwable is DonationError) {
|
||||
throwable
|
||||
} else {
|
||||
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.BOOST)
|
||||
val donationError: DonationError = when (throwable) {
|
||||
is DonationError -> throwable
|
||||
is DonationProcessorError -> throwable.toDonationError(request.donateToSignalType.toErrorSource(), paymentSourceProvider.paymentSourceType)
|
||||
else -> DonationError.genericBadgeRedemptionFailure(request.donateToSignalType.toErrorSource())
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
},
|
||||
@@ -256,10 +258,10 @@ class StripePaymentInProgressViewModel(
|
||||
},
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Failed to update subscription", throwable, true)
|
||||
val donationError: DonationError = if (throwable is DonationError) {
|
||||
throwable
|
||||
} else {
|
||||
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.SUBSCRIPTION)
|
||||
val donationError: DonationError = when (throwable) {
|
||||
is DonationError -> throwable
|
||||
is DonationProcessorError -> throwable.toDonationError(DonationErrorSource.SUBSCRIPTION, PaymentSourceType.Stripe.GooglePay)
|
||||
else -> DonationError.genericBadgeRedemptionFailure(DonationErrorSource.SUBSCRIPTION)
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.errors
|
||||
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.DonationProcessorError
|
||||
|
||||
fun DonationProcessorError.toDonationError(
|
||||
source: DonationErrorSource,
|
||||
method: PaymentSourceType
|
||||
): DonationError {
|
||||
return when (processor) {
|
||||
ActiveSubscription.Processor.STRIPE -> {
|
||||
check(method is PaymentSourceType.Stripe)
|
||||
val declineCode = StripeDeclineCode.getFromCode(chargeFailure.code)
|
||||
if (declineCode.isKnown()) {
|
||||
DonationError.PaymentSetupError.StripeDeclinedError(source, this, declineCode, method)
|
||||
} else if (chargeFailure.code != null) {
|
||||
DonationError.PaymentSetupError.StripeCodedError(source, this, chargeFailure.code)
|
||||
} else {
|
||||
DonationError.PaymentSetupError.GenericError(source, this)
|
||||
}
|
||||
}
|
||||
ActiveSubscription.Processor.BRAINTREE -> {
|
||||
check(method is PaymentSourceType.PayPal)
|
||||
val code = chargeFailure.code
|
||||
if (code == null) {
|
||||
DonationError.PaymentSetupError.GenericError(source, this)
|
||||
} else {
|
||||
val declineCode = PayPalDeclineCode.KnownCode.fromCode(code.toInt())
|
||||
if (declineCode != null) {
|
||||
DonationError.PaymentSetupError.PayPalDeclinedError(source, this, declineCode)
|
||||
} else {
|
||||
DonationError.PaymentSetupError.PayPalCodedError(source, this, code.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,10 @@ public final class ActiveSubscription {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
static Processor fromCode(String code) {
|
||||
for (Processor value : Processor.values()) {
|
||||
if (value.code.equals(code)) {
|
||||
|
||||
@@ -112,6 +112,7 @@ import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequ
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
import org.whispersystems.signalservice.internal.crypto.AttachmentDigest;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.DonationProcessorError;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.ForbiddenException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.GroupExistsException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.GroupMismatchedDevicesException;
|
||||
@@ -1157,7 +1158,7 @@ public class PushServiceSocket {
|
||||
public PayPalConfirmPaymentIntentResponse confirmPayPalOneTimePaymentIntent(String currency, String amount, long level, String payerId, String paymentId, String paymentToken) throws IOException {
|
||||
String payload = JsonUtil.toJson(new PayPalConfirmOneTimePaymentIntentPayload(amount, currency, level, payerId, paymentId, paymentToken));
|
||||
Log.d(TAG, payload);
|
||||
String result = makeServiceRequestWithoutAuthentication(CONFIRM_PAYPAL_ONE_TIME_PAYMENT_INTENT, "POST", payload);
|
||||
String result = makeServiceRequestWithoutAuthentication(CONFIRM_PAYPAL_ONE_TIME_PAYMENT_INTENT, "POST", payload, NO_HEADERS, new DonationResponseHandler());
|
||||
return JsonUtil.fromJsonResponse(result, PayPalConfirmPaymentIntentResponse.class);
|
||||
}
|
||||
|
||||
@@ -1209,7 +1210,7 @@ public class PushServiceSocket {
|
||||
}
|
||||
|
||||
public void updateSubscriptionLevel(String subscriberId, String level, String currencyCode, String idempotencyKey) throws IOException {
|
||||
makeServiceRequestWithoutAuthentication(String.format(UPDATE_SUBSCRIPTION_LEVEL, subscriberId, level, currencyCode, idempotencyKey), "PUT", "");
|
||||
makeServiceRequestWithoutAuthentication(String.format(UPDATE_SUBSCRIPTION_LEVEL, subscriberId, level, currencyCode, idempotencyKey), "PUT", "", NO_HEADERS, new DonationResponseHandler());
|
||||
}
|
||||
|
||||
public ActiveSubscription getSubscription(String subscriberId) throws IOException {
|
||||
@@ -2760,6 +2761,24 @@ public class PushServiceSocket {
|
||||
makeServiceRequest(String.format(REPORT_SPAM, serviceId.toString(), serverGuid), "POST", JsonUtil.toJson(new SpamTokenMessage(reportingToken)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for a couple donation endpoints.
|
||||
*/
|
||||
private static class DonationResponseHandler implements ResponseCodeHandler {
|
||||
@Override
|
||||
public void handle(int responseCode, ResponseBody body) throws NonSuccessfulResponseCodeException, PushNetworkException {
|
||||
if (responseCode == 440) {
|
||||
try {
|
||||
throw JsonUtil.fromJson(body.string(), DonationProcessorError.class);
|
||||
} catch (IOException e) {
|
||||
throw new NonSuccessfulResponseCodeException(440);
|
||||
}
|
||||
} else {
|
||||
throw new NonSuccessfulResponseCodeException(responseCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class RegistrationSessionResponseHandler implements ResponseCodeHandler {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.internal.push.exceptions
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription.ChargeFailure
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription.Processor
|
||||
|
||||
/**
|
||||
* HTTP 440 Exception when something bad happens while updating a user's subscription level or
|
||||
* confirming a PayPal intent.
|
||||
*/
|
||||
class DonationProcessorError @JsonCreator constructor(
|
||||
val processor: Processor,
|
||||
val chargeFailure: ChargeFailure
|
||||
) : NonSuccessfulResponseCodeException(440) {
|
||||
override fun toString(): String {
|
||||
return """
|
||||
DonationProcessorError (440)
|
||||
Processor: $processor
|
||||
Charge Failure: $chargeFailure
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.internal.push.exceptions
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil
|
||||
|
||||
class DonationProcessorErrorTest {
|
||||
|
||||
companion object {
|
||||
private val TEST_PROCESSOR = ActiveSubscription.Processor.STRIPE
|
||||
private const val TEST_CODE = "account_closed"
|
||||
private const val TEST_MESSAGE = "test_message"
|
||||
private const val TEST_OUTCOME_NETWORK_STATUS = "test_outcomeNetworkStatus"
|
||||
private const val TEST_OUTCOME_REASON = "test_outcomeReason"
|
||||
private const val TEST_OUTCOME_TYPE = "test_outcomeType"
|
||||
private val TEST_JSON = """
|
||||
{
|
||||
"processor": "${TEST_PROCESSOR.code}",
|
||||
"chargeFailure": {
|
||||
"code": "$TEST_CODE",
|
||||
"message": "$TEST_MESSAGE",
|
||||
"outcomeNetworkStatus": "$TEST_OUTCOME_NETWORK_STATUS",
|
||||
"outcomeReason": "$TEST_OUTCOME_REASON",
|
||||
"outcomeType": "$TEST_OUTCOME_TYPE"
|
||||
}
|
||||
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTestJson_whenIFromJson_thenIExpectProperlyParsedError() {
|
||||
val result = JsonUtil.fromJson(TEST_JSON, DonationProcessorError::class.java)
|
||||
|
||||
assertEquals(TEST_PROCESSOR, result.processor)
|
||||
assertEquals(TEST_CODE, result.chargeFailure.code)
|
||||
assertEquals(TEST_OUTCOME_TYPE, result.chargeFailure.outcomeType)
|
||||
assertEquals(TEST_OUTCOME_NETWORK_STATUS, result.chargeFailure.outcomeNetworkStatus)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user