From b8dc541fc504ccc6a54150a382c1ecd859b277a3 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 5 Nov 2021 12:28:07 -0300 Subject: [PATCH] Add better application error handling for badges and token redemption. --- .../securesms/badges/models/Badge.kt | 4 ++ .../app/subscription/DonationExceptions.kt | 1 + .../subscription/DonationPaymentRepository.kt | 41 ++++++++++++++++-- .../app/subscription/boost/BoostFragment.kt | 17 +++++++- .../subscribe/SubscribeFragment.kt | 18 ++++++-- .../jobs/BoostReceiptRequestResponseJob.java | 34 +++++++++------ .../jobs/DonationReceiptRedemptionJob.java | 20 ++++----- .../securesms/jobs/RefreshOwnProfileJob.java | 26 ++++++++++-- ...SubscriptionReceiptRequestResponseJob.java | 42 +++++++++++++------ app/src/main/res/values/strings.xml | 2 + 10 files changed, 160 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/models/Badge.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/models/Badge.kt index e0b8d26d9..bfd6b6e2c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/models/Badge.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/models/Badge.kt @@ -36,6 +36,10 @@ data class Badge( val visible: Boolean, ) : Parcelable, Key { + fun setVisible(): Badge { + return copy(visible = true) + } + fun isExpired(): Boolean = expirationTimestamp < System.currentTimeMillis() && expirationTimestamp > 0 fun isBoost(): Boolean = id == BOOST_BADGE_ID diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationExceptions.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationExceptions.kt index 5186309ac..4ff4882b9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationExceptions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationExceptions.kt @@ -2,4 +2,5 @@ package org.thoughtcrime.securesms.components.settings.app.subscription class DonationExceptions { object TimedOutWaitingForTokenRedemption : Exception() + object RedemptionFailed : Exception() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt index 768032c76..58249505a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt @@ -12,6 +12,7 @@ import org.signal.donations.GooglePayApi import org.signal.donations.GooglePayPaymentSource import org.signal.donations.StripeApi import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.jobmanager.JobTracker import org.thoughtcrime.securesms.jobs.BoostReceiptRequestResponseJob import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob import org.thoughtcrime.securesms.keyvalue.SignalStore @@ -114,15 +115,30 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet val jobId = BoostReceiptRequestResponseJob.enqueueChain(paymentIntent) val countDownLatch = CountDownLatch(1) + var finalJobState: JobTracker.JobState? = null ApplicationDependencies.getJobManager().addListener(jobId) { _, jobState -> if (jobState.isComplete) { + finalJobState = jobState countDownLatch.countDown() } } try { if (countDownLatch.await(10, TimeUnit.SECONDS)) { - it.onComplete() + when (finalJobState) { + JobTracker.JobState.SUCCESS -> { + Log.d(TAG, "Request response job chain succeeded.", true) + it.onComplete() + } + JobTracker.JobState.FAILURE -> { + Log.d(TAG, "Request response job chain failed permanently.", true) + it.onError(DonationExceptions.RedemptionFailed) + } + else -> { + Log.d(TAG, "Request response job chain ignored due to in-progress jobs.", true) + it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption) + } + } } else { it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption) } @@ -137,7 +153,7 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet .flatMapCompletable { levelUpdateOperation -> val subscriber = SignalStore.donationsValues().requireSubscriber() - Log.d(TAG, "Attempting to set user subscription level to $subscriptionLevel") + Log.d(TAG, "Attempting to set user subscription level to $subscriptionLevel", true) ApplicationDependencies.getDonationsService().updateSubscriptionLevel( subscriber.subscriberId, @@ -145,27 +161,46 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet subscriber.currencyCode, levelUpdateOperation.idempotencyKey.serialize() ).flatMap(ServiceResponse::flattenResult).ignoreElement().andThen { + Log.d(TAG, "Successfully set user subscription to level $subscriptionLevel", true) SignalStore.donationsValues().clearUserManuallyCancelled() SignalStore.donationsValues().clearLevelOperation() LevelUpdate.updateProcessingState(false) it.onComplete() }.andThen { + Log.d(TAG, "Enqueuing request response job chain.", true) val jobId = SubscriptionReceiptRequestResponseJob.enqueueSubscriptionContinuation() val countDownLatch = CountDownLatch(1) + var finalJobState: JobTracker.JobState? = null ApplicationDependencies.getJobManager().addListener(jobId) { _, jobState -> if (jobState.isComplete) { + finalJobState = jobState countDownLatch.countDown() } } try { if (countDownLatch.await(10, TimeUnit.SECONDS)) { - it.onComplete() + when (finalJobState) { + JobTracker.JobState.SUCCESS -> { + Log.d(TAG, "Request response job chain succeeded.", true) + it.onComplete() + } + JobTracker.JobState.FAILURE -> { + Log.d(TAG, "Request response job chain failed permanently.", true) + it.onError(DonationExceptions.RedemptionFailed) + } + else -> { + Log.d(TAG, "Request response job chain ignored due to in-progress jobs.", true) + it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption) + } + } } else { + Log.d(TAG, "Request response job timed out.", true) it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption) } } catch (e: InterruptedException) { + Log.w(TAG, "Request response interrupted.", e, true) it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostFragment.kt index c4cb58fc9..a89aa47d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostFragment.kt @@ -21,11 +21,13 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon import org.thoughtcrime.securesms.components.settings.DSLSettingsText +import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent import org.thoughtcrime.securesms.components.settings.app.subscription.DonationExceptions import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton import org.thoughtcrime.securesms.components.settings.configure +import org.thoughtcrime.securesms.help.HelpFragment import org.thoughtcrime.securesms.util.BottomSheetUtil.requireCoordinatorLayout import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.LifecycleDisposable @@ -201,7 +203,7 @@ class BoostFragment : DSLSettingsBottomSheetFragment( private fun onPaymentError(throwable: Throwable?) { if (throwable is DonationExceptions.TimedOutWaitingForTokenRedemption) { - Log.w(TAG, "Error occurred while redeeming token", throwable) + Log.w(TAG, "Timed out while redeeming token", throwable, true) MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.DonationsErrors__redemption_still_pending) .setMessage(R.string.DonationsErrors__you_might_not_see_your_badge_right_away) @@ -210,8 +212,19 @@ class BoostFragment : DSLSettingsBottomSheetFragment( findNavController().popBackStack() } .show() + } else if (throwable is DonationExceptions.RedemptionFailed) { + Log.w(TAG, "Error occurred while trying to redeem token", throwable, true) + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.DonationsErrors__redemption_failed) + .setMessage(R.string.DonationsErrors__please_contact_support) + .setPositiveButton(R.string.Subscription__contact_support) { dialog, _ -> + dialog.dismiss() + requireActivity().finish() + requireActivity().startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.DONATION_INDEX)) + } + .show() } else { - Log.w(TAG, "Error occurred while processing payment", throwable) + Log.w(TAG, "Error occurred while processing payment", throwable, true) MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.DonationsErrors__payment_failed) .setMessage(R.string.DonationsErrors__your_payment) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeFragment.kt index 50be135ba..3cb3f564d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeFragment.kt @@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.DonationE import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton import org.thoughtcrime.securesms.components.settings.configure +import org.thoughtcrime.securesms.help.HelpFragment import org.thoughtcrime.securesms.payments.FiatMoneyUtil import org.thoughtcrime.securesms.subscription.Subscription import org.thoughtcrime.securesms.util.CommunicationActions @@ -235,7 +236,7 @@ class SubscribeFragment : DSLSettingsFragment( private fun onPaymentError(throwable: Throwable?) { if (throwable is DonationExceptions.TimedOutWaitingForTokenRedemption) { - Log.w(TAG, "Error occurred while redeeming token", throwable) + Log.w(TAG, "Timeout occurred while redeeming token", throwable, true) MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.DonationsErrors__redemption_still_pending) .setMessage(R.string.DonationsErrors__you_might_not_see_your_badge_right_away) @@ -245,8 +246,19 @@ class SubscribeFragment : DSLSettingsFragment( requireActivity().startActivity(AppSettingsActivity.subscriptions(requireContext())) } .show() + } else if (throwable is DonationExceptions.RedemptionFailed) { + Log.w(TAG, "Error occurred while trying to redeem token", throwable, true) + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.DonationsErrors__redemption_failed) + .setMessage(R.string.DonationsErrors__please_contact_support) + .setPositiveButton(R.string.Subscription__contact_support) { dialog, _ -> + dialog.dismiss() + requireActivity().finish() + requireActivity().startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.DONATION_INDEX)) + } + .show() } else { - Log.w(TAG, "Error occurred while processing payment", throwable) + Log.w(TAG, "Error occurred while processing payment", throwable, true) MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.DonationsErrors__payment_failed) .setMessage(R.string.DonationsErrors__your_payment) @@ -267,7 +279,7 @@ class SubscribeFragment : DSLSettingsFragment( } private fun onSubscriptionFailedToCancel(throwable: Throwable) { - Log.w(TAG, "Failed to cancel subscription", throwable) + Log.w(TAG, "Failed to cancel subscription", throwable, true) MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.DonationsErrors__failed_to_cancel_subscription) .setMessage(R.string.DonationsErrors__subscription_cancellation_requires_an_internet_connection) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java index 1c2f336c8..936a2d0a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java @@ -19,9 +19,6 @@ import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.subscription.SubscriptionNotification; -import org.whispersystems.libsignal.util.Pair; -import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription; -import org.whispersystems.signalservice.api.subscriptions.SubscriberId; import org.whispersystems.signalservice.internal.ServiceResponse; import java.io.IOException; @@ -122,12 +119,7 @@ public class BoostReceiptRequestResponseJob extends BaseJob { .blockingGet(); if (response.getApplicationError().isPresent()) { - if (response.getStatus() == 204) { - Log.w(TAG, "User does not have receipts available to exchange. Exiting.", response.getApplicationError().get()); - } else { - Log.w(TAG, "Encountered a server failure: " + response.getStatus(), response.getApplicationError().get()); - throw new RetryableException(); - } + handleApplicationError(response); } else if (response.getResult().isPresent()) { ReceiptCredential receiptCredential = getReceiptCredential(response.getResult().get()); @@ -140,18 +132,36 @@ public class BoostReceiptRequestResponseJob extends BaseJob { receiptCredentialPresentation.serialize()) .build()); } else { - Log.w(TAG, "Encountered a retryable exception: " + response.getStatus(), response.getExecutionError().orNull()); + Log.w(TAG, "Encountered a retryable exception: " + response.getStatus(), response.getExecutionError().orNull(), true); throw new RetryableException(); } } + private static void handleApplicationError(ServiceResponse response) throws Exception { + Throwable applicationException = response.getApplicationError().get(); + switch (response.getStatus()) { + case 204: + Log.w(TAG, "User does not have receipts available to exchange. Exiting.", applicationException, true); + break; + case 400: + Log.w(TAG, "Receipt credential request failed to validate.", applicationException, true); + throw new Exception(applicationException); + case 409: + Log.w(TAG, "Receipt already redeemed with a different request credential.", response.getApplicationError().get(), true); + throw new Exception(applicationException); + default: + Log.w(TAG, "Encountered a server failure: " + response.getStatus(), applicationException, true); + throw new RetryableException(); + } + } + private ReceiptCredentialPresentation getReceiptCredentialPresentation(@NonNull ReceiptCredential receiptCredential) throws RetryableException { ClientZkReceiptOperations operations = ApplicationDependencies.getClientZkReceiptOperations(); try { return operations.createReceiptCredentialPresentation(receiptCredential); } catch (VerificationFailedException e) { - Log.w(TAG, "getReceiptCredentialPresentation: encountered a verification failure in zk", e); + Log.w(TAG, "getReceiptCredentialPresentation: encountered a verification failure in zk", e, true); requestContext = null; throw new RetryableException(); } @@ -163,7 +173,7 @@ public class BoostReceiptRequestResponseJob extends BaseJob { try { return operations.receiveReceiptCredential(requestContext, response); } catch (VerificationFailedException e) { - Log.w(TAG, "getReceiptCredential: encountered a verification failure in zk", e); + Log.w(TAG, "getReceiptCredential: encountered a verification failure in zk", e, true); requestContext = null; throw new RetryableException(); } 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 6626f2ac8..aee2c3ad3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java @@ -4,19 +4,14 @@ import androidx.annotation.NonNull; import org.signal.core.util.logging.Log; import org.signal.zkgroup.receipts.ReceiptCredentialPresentation; -import org.thoughtcrime.securesms.badges.models.Badge; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.whispersystems.signalservice.api.subscriptions.IdempotencyKey; import org.whispersystems.signalservice.internal.EmptyResponse; import org.whispersystems.signalservice.internal.ServiceResponse; import java.io.IOException; -import java.util.Objects; import java.util.concurrent.TimeUnit; /** @@ -75,13 +70,13 @@ public class DonationReceiptRedemptionJob extends BaseJob { Data inputData = getInputData(); if (inputData == null) { - Log.w(TAG, "No input data. Failing."); + Log.w(TAG, "No input data. Failing.", null, true); throw new IllegalStateException("Expected a presentation object in input data."); } byte[] presentationBytes = inputData.getStringAsBlob(INPUT_RECEIPT_CREDENTIAL_PRESENTATION); if (presentationBytes == null) { - Log.d(TAG, "No response data. Exiting."); + Log.d(TAG, "No response data. Exiting.", null, true); return; } @@ -92,10 +87,15 @@ public class DonationReceiptRedemptionJob extends BaseJob { .blockingGet(); if (response.getApplicationError().isPresent()) { - Log.w(TAG, "Encountered a non-recoverable exception", response.getApplicationError().get()); - throw new IOException(response.getApplicationError().get()); + if (response.getStatus() >= 500) { + Log.w(TAG, "Encountered a server exception " + response.getStatus(), response.getApplicationError().get(), true); + throw new RetryableException(); + } else { + Log.w(TAG, "Encountered a non-recoverable exception " + response.getStatus(), response.getApplicationError().get(), true); + throw new IOException(response.getApplicationError().get()); + } } else if (response.getExecutionError().isPresent()) { - Log.w(TAG, "Encountered a retryable exception", response.getExecutionError().get()); + Log.w(TAG, "Encountered a retryable exception", response.getExecutionError().get(), true); throw new RetryableException(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java index 9e9b5f13b..d5266bf07 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java @@ -8,6 +8,7 @@ import androidx.annotation.Nullable; import org.signal.core.util.logging.Log; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; +import org.thoughtcrime.securesms.badges.BadgeRepository; import org.thoughtcrime.securesms.badges.Badges; import org.thoughtcrime.securesms.badges.models.Badge; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; @@ -30,6 +31,7 @@ import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import java.io.IOException; +import java.sql.Ref; import java.util.Comparator; import java.util.List; import java.util.Objects; @@ -213,9 +215,27 @@ public class RefreshOwnProfileJob extends BaseJob { SignalStore.donationsValues().setExpiredBadge(mostRecentExpiration); } - DatabaseFactory.getRecipientDatabase(context) - .setBadges(Recipient.self().getId(), - badges.stream().map(Badges::fromServiceBadge).collect(Collectors.toList())); + boolean userHasVisibleBadges = badges.stream().anyMatch(SignalServiceProfile.Badge::isVisible); + boolean userHasInvisibleBadges = badges.stream().anyMatch(b -> !b.isVisible()); + + List appBadges = badges.stream().map(Badges::fromServiceBadge).collect(Collectors.toList()); + + if (userHasVisibleBadges && userHasInvisibleBadges) { + Log.d(TAG, "Detected mixed visibility of badges. Telling the server to mark them all visible.", true); + + BadgeRepository badgeRepository = new BadgeRepository(context); + badgeRepository.setVisibilityForAllBadges(true); + + DatabaseFactory.getRecipientDatabase(context) + .setBadges(Recipient.self().getId(), + appBadges.stream() + .map(Badge::setVisible) + .collect(Collectors.toList())); + + } else { + DatabaseFactory.getRecipientDatabase(context) + .setBadges(Recipient.self().getId(), appBadges); + } } private static boolean isSubscription(String badgeId) { 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 1b48e681b..17ad230cb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java @@ -109,10 +109,10 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { protected void onRun() throws Exception { ActiveSubscription.Subscription subscription = getLatestSubscriptionInformation(); if (subscription == null || !subscription.isActive()) { - Log.d(TAG, "User does not have an active subscription. Exiting."); + Log.d(TAG, "User does not have an active subscription. Exiting.", true); return; } else { - Log.i(TAG, "Recording end of period from active subscription."); + Log.i(TAG, "Recording end of period from active subscription.", true); SignalStore.donationsValues().setLastEndOfPeriod(subscription.getEndOfCurrentPeriod()); } @@ -133,12 +133,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { .blockingGet(); if (response.getApplicationError().isPresent()) { - if (response.getStatus() == 204) { - Log.w(TAG, "User does not have receipts available to exchange. Exiting.", response.getApplicationError().get()); - } else { - Log.w(TAG, "Encountered a server failure response: " + response.getStatus(), response.getApplicationError().get()); - throw new RetryableException(); - } + handleApplicationError(response); } else if (response.getResult().isPresent()) { ReceiptCredential receiptCredential = getReceiptCredential(response.getResult().get()); @@ -151,7 +146,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { receiptCredentialPresentation.serialize()) .build()); } else { - Log.w(TAG, "Encountered a retryable exception: " + response.getStatus(), response.getExecutionError().orNull()); + Log.w(TAG, "Encountered a retryable exception: " + response.getStatus(), response.getExecutionError().orNull(), true); throw new RetryableException(); } } @@ -164,7 +159,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { if (activeSubscription.getResult().isPresent()) { return activeSubscription.getResult().get().getActiveSubscription(); } else if (activeSubscription.getApplicationError().isPresent()) { - Log.w(TAG, "Unrecoverable error getting the user's current subscription. Failing."); + Log.w(TAG, "Unrecoverable error getting the user's current subscription. Failing.", activeSubscription.getApplicationError().get(), true); throw new IOException(activeSubscription.getApplicationError().get()); } else { throw new RetryableException(); @@ -177,7 +172,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { try { return operations.createReceiptCredentialPresentation(receiptCredential); } catch (VerificationFailedException e) { - Log.w(TAG, "getReceiptCredentialPresentation: encountered a verification failure in zk", e); + Log.w(TAG, "getReceiptCredentialPresentation: encountered a verification failure in zk", e, true); requestContext = null; throw new RetryableException(); } @@ -189,12 +184,35 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { try { return operations.receiveReceiptCredential(requestContext, response); } catch (VerificationFailedException e) { - Log.w(TAG, "getReceiptCredential: encountered a verification failure in zk", e); + Log.w(TAG, "getReceiptCredential: encountered a verification failure in zk", e, true); requestContext = null; throw new RetryableException(); } } + private static void handleApplicationError(ServiceResponse response) throws Exception { + switch (response.getStatus()) { + case 204: + Log.w(TAG, "User does not have receipts available to exchange. Exiting.", response.getApplicationError().get(), true); + break; + case 400: + Log.w(TAG, "Receipt credential request failed to validate.", response.getApplicationError().get(), true); + throw new Exception(response.getApplicationError().get()); + case 403: + Log.w(TAG, "SubscriberId password mismatch or account auth was present.", response.getApplicationError().get(), true); + throw new Exception(response.getApplicationError().get()); + case 404: + Log.w(TAG, "SubscriberId not found or misformed.", response.getApplicationError().get(), true); + throw new Exception(response.getApplicationError().get()); + case 409: + Log.w(TAG, "Latest paid receipt on subscription already redeemed with a different request credential.", response.getApplicationError().get(), true); + throw new Exception(response.getApplicationError().get()); + default: + Log.w(TAG, "Encountered a server failure response: " + response.getStatus(), response.getApplicationError().get(), true); + throw new RetryableException(); + } + } + /** * Checks that the generated Receipt Credential has the following characteristics * - level should match the current subscription level and be the same level you signed up for at the time the subscription was last updated diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1df1f9d00..ae57518cd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4007,6 +4007,8 @@ Payment failed Your payment couldn\'t be processed and you have not been charged. Please try again. Redemption still pending + Redemption failed + Please contact support You may not see your badge right away, but we\'re working on it! Google Pay Unavailable You have to set up Google Pay to donate in-app.