diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManager.java index 26dd11dd0..e2a00d113 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManager.java @@ -169,11 +169,13 @@ public class GooglePlayBillingManager implements SubscriptionPaymentProcessor { Metrics.counter(VALIDATE_COUNTER_NAME, subscriptionTags(subscription)).increment(); - // We only ever acknowledge valid tokens. There are cases where a subscription was once valid and then was - // cancelled, so the user could still be entitled to their purchase. However, if we never acknowledge it, the - // user's charge will eventually be refunded anyway. See - // https://developer.android.com/google/play/billing/integrate#pending - if (state != SubscriptionState.ACTIVE) { + // We only accept tokens in a state where the user may be entitled to their purchase. This is true even in the + // CANCELLED state. For example, a user may subscribe for 1 month, then immediately cancel (disabling auto-renew) + // and then submit their token. In this case they should still be able to retrieve their entitlement. + // See https://developer.android.com/google/play/billing/integrate#life + if (state != SubscriptionState.ACTIVE + && state != SubscriptionState.IN_GRACE_PERIOD + && state != SubscriptionState.CANCELED) { throw ExceptionUtils.wrap(new SubscriptionException.PaymentRequired( "Cannot acknowledge purchase for subscription in state " + subscription.getSubscriptionState())); } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManagerTest.java index 6179f034f..dd36b2155 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManagerTest.java @@ -31,6 +31,7 @@ import java.time.Duration; import java.time.Instant; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -133,7 +134,7 @@ class GooglePlayBillingManagerTest { } @ParameterizedTest - @EnumSource(mode = EnumSource.Mode.EXCLUDE, names = {"ACTIVE"}) + @EnumSource public void rejectInactivePurchase(GooglePlayBillingManager.SubscriptionState subscriptionState) throws IOException { when(subscriptionsv2Get.execute()).thenReturn(new SubscriptionPurchaseV2() .setAcknowledgementState(GooglePlayBillingManager.AcknowledgementState.PENDING.apiString()) @@ -142,9 +143,12 @@ class GooglePlayBillingManagerTest { .setExpiryTime(Instant.now().plus(Duration.ofDays(1)).toString()) .setProductId(PRODUCT_ID)))); - CompletableFutureTestUtil.assertFailsWithCause( - SubscriptionException.PaymentRequired.class, - googlePlayBillingManager.validateToken(PURCHASE_TOKEN)); + final CompletableFuture future = googlePlayBillingManager + .validateToken(PURCHASE_TOKEN); + switch (subscriptionState) { + case ACTIVE, IN_GRACE_PERIOD, CANCELED -> assertThatNoException().isThrownBy(() -> future.join()); + default -> CompletableFutureTestUtil.assertFailsWithCause(SubscriptionException.PaymentRequired.class, future); + } } @Test