Improve recognition of failed payment states.

fork-5.53.8
Greyson Parrelli 2021-11-22 16:57:43 -05:00
rodzic 1508b1d401
commit cd1f0632fa
4 zmienionych plików z 73 dodań i 47 usunięć

Wyświetl plik

@ -60,7 +60,7 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
public static JobManager.Chain createJobChain(StripeApi.PaymentIntent paymentIntent) {
BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntent);
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForBoost();
RefreshOwnProfileJob refreshOwnProfileJob = new RefreshOwnProfileJob();
RefreshOwnProfileJob refreshOwnProfileJob = RefreshOwnProfileJob.forBoost();
return ApplicationDependencies.getJobManager()
.startChain(requestReceiptJob)
@ -195,19 +195,19 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
* - expiration_time is between now and 60 days from now
*/
private boolean isCredentialValid(@NonNull ReceiptCredential receiptCredential) {
long now = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
long monthFromNow = now + TimeUnit.DAYS.toSeconds(60);
boolean isCorrectLevel = receiptCredential.getReceiptLevel() == 1;
boolean isExpiration86400 = receiptCredential.getReceiptExpirationTime() % 86400 == 0;
boolean isExpirationInTheFuture = receiptCredential.getReceiptExpirationTime() > now;
boolean isExpirationWithinAMonth = receiptCredential.getReceiptExpirationTime() <= monthFromNow;
long now = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
long maxExpirationTime = now + TimeUnit.DAYS.toSeconds(60);
boolean isCorrectLevel = receiptCredential.getReceiptLevel() == 1;
boolean isExpiration86400 = receiptCredential.getReceiptExpirationTime() % 86400 == 0;
boolean isExpirationInTheFuture = receiptCredential.getReceiptExpirationTime() > now;
boolean isExpirationWithinMax = receiptCredential.getReceiptExpirationTime() <= maxExpirationTime;
Log.d(TAG, "Credential validation: isCorrectLevel(" + isCorrectLevel +
") isExpiration86400(" + isExpiration86400 +
") isExpirationInTheFuture(" + isExpirationInTheFuture +
") isExpirationWithinAMonth(" + isExpirationWithinAMonth + ")", true);
") isExpirationWithinMax(" + isExpirationWithinMax + ")", true);
return isCorrectLevel && isExpiration86400 && isExpirationInTheFuture && isExpirationWithinAMonth;
return isCorrectLevel && isExpiration86400 && isExpirationInTheFuture && isExpirationWithinMax;
}
@Override

Wyświetl plik

@ -48,13 +48,28 @@ public class RefreshOwnProfileJob extends BaseJob {
private static final String TAG = Log.tag(RefreshOwnProfileJob.class);
private static final String SUBSCRIPTION_QUEUE = ProfileUploadJob.QUEUE + "_Subscription";
private static final String BOOST_QUEUE = ProfileUploadJob.QUEUE + "_Boost";
public RefreshOwnProfileJob() {
this(ProfileUploadJob.QUEUE);
}
private RefreshOwnProfileJob(@NonNull String queue) {
this(new Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setQueue(ProfileUploadJob.QUEUE)
.setMaxInstancesForFactory(1)
.setMaxAttempts(10)
.build());
.addConstraint(NetworkConstraint.KEY)
.setQueue(queue)
.setMaxInstancesForFactory(1)
.setMaxAttempts(10)
.build());
}
public static @NonNull RefreshOwnProfileJob forSubscription() {
return new RefreshOwnProfileJob(SUBSCRIPTION_QUEUE);
}
public static @NonNull RefreshOwnProfileJob forBoost() {
return new RefreshOwnProfileJob(BOOST_QUEUE);
}
@ -198,11 +213,11 @@ public class RefreshOwnProfileJob extends BaseJob {
.max(Comparator.comparingLong(Badge::getExpirationTimestamp))
.get();
Log.d(TAG, "Marking subscription badge as expired, should notifiy next time the conversation list is open.");
Log.d(TAG, "Marking subscription badge as expired, should notify next time the conversation list is open.", true);
SignalStore.donationsValues().setExpiredBadge(mostRecentExpiration);
if (!SignalStore.donationsValues().isUserManuallyCancelled()) {
Log.d(TAG, "Detected an unexpected subscription expiry.");
Log.d(TAG, "Detected an unexpected subscription expiry.", true);
SignalStore.donationsValues().setShouldCancelSubscriptionBeforeNextSubscribeAttempt(true);
}
} else if (!remoteHasBoostBadges && localHasBoostBadges) {
@ -214,7 +229,7 @@ public class RefreshOwnProfileJob extends BaseJob {
.max(Comparator.comparingLong(Badge::getExpirationTimestamp))
.get();
Log.d(TAG, "Marking boost badge as expired, should notifiy next time the conversation list is open.");
Log.d(TAG, "Marking boost badge as expired, should notify next time the conversation list is open.", true);
SignalStore.donationsValues().setExpiredBadge(mostRecentExpiration);
}

Wyświetl plik

@ -67,7 +67,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
Subscriber subscriber = SignalStore.donationsValues().requireSubscriber();
SubscriptionReceiptRequestResponseJob requestReceiptJob = createJob(subscriber.getSubscriberId());
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForSubscription();
RefreshOwnProfileJob refreshOwnProfileJob = new RefreshOwnProfileJob();
RefreshOwnProfileJob refreshOwnProfileJob = RefreshOwnProfileJob.forSubscription();
return ApplicationDependencies.getJobManager()
.startChain(requestReceiptJob)
@ -115,14 +115,16 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
private void doRun() throws Exception {
ActiveSubscription.Subscription subscription = getLatestSubscriptionInformation();
if (subscription == null || !subscription.isActive()) {
Log.w(TAG, "User does not have an active subscription yet.", true);
if (subscription == null) {
Log.w(TAG, "Subscription is null.", true);
throw new RetryableException();
} else if (subscription.isFailedPayment()) {
Log.w(TAG, "Subscription payment failure. Passing through to redemption job.", true);
SignalStore.donationsValues().setShouldCancelSubscriptionBeforeNextSubscribeAttempt(true);
setOutputData(new Data.Builder().putBoolean(DonationReceiptRedemptionJob.INPUT_PAYMENT_FAILURE, true).build());
Log.w(TAG, "Subscription payment failure in active subscription response (status = " + subscription.getStatus() + "). Passing through to redemption job.", true);
onPaymentFailure();
return;
} else if (!subscription.isActive()) {
Log.w(TAG, "Subscription is not yet active. Status: " + subscription.getStatus(), true);
throw new RetryableException();
} else {
Log.i(TAG, "Recording end of period from active subscription.", true);
SignalStore.donationsValues().setLastEndOfPeriod(subscription.getEndOfCurrentPeriod());
@ -150,10 +152,6 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
if (response.getApplicationError().isPresent()) {
handleApplicationError(response);
if (response.getStatus() == 204) {
SignalStore.donationsValues().clearSubscriptionRedemptionFailed();
}
} else if (response.getResult().isPresent()) {
ReceiptCredential receiptCredential = getReceiptCredential(response.getResult().get());
@ -214,15 +212,15 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
private void handleApplicationError(ServiceResponse<ReceiptCredentialResponse> 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;
Log.w(TAG, "Payment is still processing. Trying again.", response.getApplicationError().get(), true);
SignalStore.donationsValues().clearSubscriptionRedemptionFailed();
throw new RetryableException();
case 400:
Log.w(TAG, "Receipt credential request failed to validate.", response.getApplicationError().get(), true);
throw new Exception(response.getApplicationError().get());
case 402:
Log.w(TAG, "Subscription payment failure. Passing through to redemption job.", true);
SignalStore.donationsValues().setShouldCancelSubscriptionBeforeNextSubscribeAttempt(true);
setOutputData(new Data.Builder().putBoolean(DonationReceiptRedemptionJob.INPUT_PAYMENT_FAILURE, true).build());
Log.w(TAG, "Subscription payment failure in credential response. Passing through to redemption job.", true);
onPaymentFailure();
break;
case 403:
Log.w(TAG, "SubscriberId password mismatch or account auth was present.", response.getApplicationError().get(), true);
@ -239,6 +237,11 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
}
}
private void onPaymentFailure() {
SignalStore.donationsValues().setShouldCancelSubscriptionBeforeNextSubscribeAttempt(true);
setOutputData(new Data.Builder().putBoolean(DonationReceiptRedemptionJob.INPUT_PAYMENT_FAILURE, true).build());
}
/**
* 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
@ -247,21 +250,21 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
* - expiration_time is between now and 60 days from now
*/
private boolean isCredentialValid(@NonNull ActiveSubscription.Subscription subscription, @NonNull ReceiptCredential receiptCredential) {
long now = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
long monthFromNow = now + TimeUnit.DAYS.toSeconds(60);
boolean isSameLevel = subscription.getLevel() == receiptCredential.getReceiptLevel();
boolean isExpirationAfterSub = subscription.getEndOfCurrentPeriod() < receiptCredential.getReceiptExpirationTime();
boolean isExpiration86400 = receiptCredential.getReceiptExpirationTime() % 86400 == 0;
boolean isExpirationInTheFuture = receiptCredential.getReceiptExpirationTime() > now;
boolean isExpirationWithinAMonth = receiptCredential.getReceiptExpirationTime() <= monthFromNow;
long now = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
long maxExpirationTime = now + TimeUnit.DAYS.toSeconds(60);
boolean isSameLevel = subscription.getLevel() == receiptCredential.getReceiptLevel();
boolean isExpirationAfterSub = subscription.getEndOfCurrentPeriod() < receiptCredential.getReceiptExpirationTime();
boolean isExpiration86400 = receiptCredential.getReceiptExpirationTime() % 86400 == 0;
boolean isExpirationInTheFuture = receiptCredential.getReceiptExpirationTime() > now;
boolean isExpirationWithinMax = receiptCredential.getReceiptExpirationTime() <= maxExpirationTime;
Log.d(TAG, "Credential validation: isSameLevel(" + isSameLevel +
") isExpirationAfterSub(" + isExpirationAfterSub +
") isExpiration86400(" + isExpiration86400 +
") isExpirationInTheFuture(" + isExpirationInTheFuture +
") isExpirationWithinAMonth(" + isExpirationWithinAMonth + ")", true);
") isExpirationWithinMax(" + isExpirationWithinMax + ")", true);
return isSameLevel && isExpirationAfterSub && isExpiration86400 && isExpirationInTheFuture && isExpirationWithinAMonth;
return isSameLevel && isExpirationAfterSub && isExpiration86400 && isExpirationInTheFuture && isExpirationWithinMax;
}
@Override

Wyświetl plik

@ -4,7 +4,10 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public final class ActiveSubscription {
@ -49,6 +52,13 @@ public final class ActiveSubscription {
private final String status;
private static final Set<Status> FAILURE_STATUSES = new HashSet<>(Arrays.asList(
INCOMPLETE_EXPIRED,
PAST_DUE,
CANCELED,
UNPAID
));
Status(String status) {
this.status = status;
}
@ -64,8 +74,7 @@ public final class ActiveSubscription {
}
static boolean isPaymentFailed(String status) {
Status s = getStatus(status);
return s == INCOMPLETE || s == INCOMPLETE_EXPIRED;
return FAILURE_STATUSES.contains(getStatus(status));
}
}
@ -85,11 +94,11 @@ public final class ActiveSubscription {
}
public boolean isInProgress() {
return activeSubscription != null && !isActive() && !isFailedPayment();
return activeSubscription != null && !isActive() && !activeSubscription.isFailedPayment();
}
public boolean isFailedPayment() {
return activeSubscription != null && !isActive() && isFailedPayment();
return activeSubscription != null && !isActive() && activeSubscription.isFailedPayment();
}
public static final class Subscription {
@ -171,8 +180,7 @@ public final class ActiveSubscription {
}
public boolean isInProgress() {
return !isActive() &&
!Status.isPaymentFailed(getStatus());
return !isActive() && !Status.isPaymentFailed(getStatus());
}
public boolean isFailedPayment() {