mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-20 07:08:05 +01:00
Add appstore subscriptions endpoint
This commit is contained in:
committed by
ravi-signal
parent
02ff3f2ff4
commit
42e920cd5c
@@ -1142,7 +1142,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
List.of(stripeManager, braintreeManager, googlePlayBillingManager, appleAppStoreManager),
|
||||
zkReceiptOperations, issuedReceiptsManager);
|
||||
commonControllers.add(new SubscriptionController(clock, config.getSubscription(), config.getOneTimeDonations(),
|
||||
subscriptionManager, stripeManager, braintreeManager, googlePlayBillingManager,
|
||||
subscriptionManager, stripeManager, braintreeManager, googlePlayBillingManager, appleAppStoreManager,
|
||||
profileBadgeConverter, resourceBundleLevelTranslator, bankMandateTranslator));
|
||||
commonControllers.add(new OneTimeDonationController(clock, config.getOneTimeDonations(), stripeManager, braintreeManager,
|
||||
zkReceiptOperations, issuedReceiptsManager, oneTimeDonationsManager));
|
||||
|
||||
@@ -76,6 +76,7 @@ import org.whispersystems.textsecuregcm.storage.PaymentTime;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriberCredentials;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionException;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.AppleAppStoreManager;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.BankMandateTranslator;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.BankTransferType;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager;
|
||||
@@ -106,6 +107,7 @@ public class SubscriptionController {
|
||||
private final StripeManager stripeManager;
|
||||
private final BraintreeManager braintreeManager;
|
||||
private final GooglePlayBillingManager googlePlayBillingManager;
|
||||
private final AppleAppStoreManager appleAppStoreManager;
|
||||
private final BadgeTranslator badgeTranslator;
|
||||
private final LevelTranslator levelTranslator;
|
||||
private final BankMandateTranslator bankMandateTranslator;
|
||||
@@ -122,6 +124,7 @@ public class SubscriptionController {
|
||||
@Nonnull StripeManager stripeManager,
|
||||
@Nonnull BraintreeManager braintreeManager,
|
||||
@Nonnull GooglePlayBillingManager googlePlayBillingManager,
|
||||
@Nonnull AppleAppStoreManager appleAppStoreManager,
|
||||
@Nonnull BadgeTranslator badgeTranslator,
|
||||
@Nonnull LevelTranslator levelTranslator,
|
||||
@Nonnull BankMandateTranslator bankMandateTranslator) {
|
||||
@@ -132,6 +135,7 @@ public class SubscriptionController {
|
||||
this.stripeManager = Objects.requireNonNull(stripeManager);
|
||||
this.braintreeManager = Objects.requireNonNull(braintreeManager);
|
||||
this.googlePlayBillingManager = Objects.requireNonNull(googlePlayBillingManager);
|
||||
this.appleAppStoreManager = appleAppStoreManager;
|
||||
this.badgeTranslator = Objects.requireNonNull(badgeTranslator);
|
||||
this.levelTranslator = Objects.requireNonNull(levelTranslator);
|
||||
this.bankMandateTranslator = Objects.requireNonNull(bankMandateTranslator);
|
||||
@@ -401,6 +405,39 @@ public class SubscriptionController {
|
||||
== subscriptionConfiguration.getSubscriptionLevel(level2).type();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{subscriberId}/appstore/{originalTransactionId}")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Set app store subscription", description = """
|
||||
Set an originalTransactionId that represents an IAP subscription made with the app store.
|
||||
|
||||
To set up an app store subscription:
|
||||
1. Create a subscriber with `PUT subscriptions/{subscriberId}` (you must regularly refresh this subscriber)
|
||||
2. [Create a subscription](https://developer.apple.com/documentation/storekit/in-app_purchase/) with the App Store
|
||||
directly via StoreKit and obtain a originalTransactionId.
|
||||
3. `POST` the purchaseToken here
|
||||
4. Obtain a receipt at `POST /v1/subscription/{subscriberId}/receipt_credentials` which can then be used to obtain the
|
||||
entitlement
|
||||
""")
|
||||
@ApiResponse(responseCode = "200", description = "The originalTransactionId was successfully validated")
|
||||
@ApiResponse(responseCode = "402", description = "The subscription transaction is incomplete or invalid")
|
||||
@ApiResponse(responseCode = "403", description = "subscriberId authentication failure OR account authentication is present")
|
||||
@ApiResponse(responseCode = "404", description = "No such subscriberId exists or subscriberId is malformed or the specified transaction does not exist")
|
||||
@ApiResponse(responseCode = "409", description = "subscriberId is already linked to a processor that does not support appstore payments. Delete this subscriberId and use a new one.")
|
||||
@ApiResponse(responseCode = "429", description = "Rate limit exceeded.")
|
||||
public CompletableFuture<SetSubscriptionLevelSuccessResponse> setAppStoreSubscription(
|
||||
@ReadOnly @Auth Optional<AuthenticatedDevice> authenticatedAccount,
|
||||
@PathParam("subscriberId") String subscriberId,
|
||||
@PathParam("originalTransactionId") String originalTransactionId) throws SubscriptionException {
|
||||
final SubscriberCredentials subscriberCredentials =
|
||||
SubscriberCredentials.process(authenticatedAccount, subscriberId, clock);
|
||||
|
||||
return subscriptionManager
|
||||
.updateAppStoreTransactionId(subscriberCredentials, appleAppStoreManager, originalTransactionId)
|
||||
.thenApply(SetSubscriptionLevelSuccessResponse::new);
|
||||
}
|
||||
|
||||
|
||||
@POST
|
||||
@Path("/{subscriberId}/playbilling/{purchaseToken}")
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequest;
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse;
|
||||
import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations;
|
||||
import org.whispersystems.textsecuregcm.controllers.SubscriptionController;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.AppleAppStoreManager;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.CustomerAwareSubscriptionPaymentProcessor;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.GooglePlayBillingManager;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.PaymentProvider;
|
||||
@@ -116,7 +117,8 @@ public class SubscriptionManager {
|
||||
.thenRun(Util.NOOP);
|
||||
}
|
||||
|
||||
public CompletableFuture<Optional<SubscriptionInformation>> getSubscriptionInformation(final SubscriberCredentials subscriberCredentials) {
|
||||
public CompletableFuture<Optional<SubscriptionInformation>> getSubscriptionInformation(
|
||||
final SubscriberCredentials subscriberCredentials) {
|
||||
return getSubscriber(subscriberCredentials).thenCompose(record -> {
|
||||
if (record.subscriptionId == null) {
|
||||
return CompletableFuture.completedFuture(Optional.empty());
|
||||
@@ -155,8 +157,8 @@ public class SubscriptionManager {
|
||||
*
|
||||
* @param subscriberCredentials Subscriber credentials derived from the subscriberId
|
||||
* @param request The ZK Receipt credential request
|
||||
* @param expiration A function that takes a {@link CustomerAwareSubscriptionPaymentProcessor.ReceiptItem} and returns
|
||||
* the expiration time of the receipt
|
||||
* @param expiration A function that takes a {@link CustomerAwareSubscriptionPaymentProcessor.ReceiptItem}
|
||||
* and returns the expiration time of the receipt
|
||||
* @return If the subscription had a valid payment, the requested ZK receipt credential
|
||||
*/
|
||||
public CompletableFuture<ReceiptResult> createReceiptCredentials(
|
||||
@@ -300,8 +302,9 @@ public class SubscriptionManager {
|
||||
.getSubscription(subId)
|
||||
.thenCompose(subscription -> processor.getLevelAndCurrencyForSubscription(subscription)
|
||||
.thenCompose(existingLevelAndCurrency -> {
|
||||
if (existingLevelAndCurrency.equals(new CustomerAwareSubscriptionPaymentProcessor.LevelAndCurrency(level,
|
||||
currency.toLowerCase(Locale.ROOT)))) {
|
||||
if (existingLevelAndCurrency.equals(
|
||||
new CustomerAwareSubscriptionPaymentProcessor.LevelAndCurrency(level,
|
||||
currency.toLowerCase(Locale.ROOT)))) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
if (!transitionValidator.isTransitionValid(existingLevelAndCurrency.level(), level)) {
|
||||
@@ -387,6 +390,41 @@ public class SubscriptionManager {
|
||||
.thenApply(ignore -> validatedToken.getLevel())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the provided app store transactionId and write it the subscriptions table if is valid.
|
||||
*
|
||||
* @param subscriberCredentials Subscriber credentials derived from the subscriberId
|
||||
* @param appleAppStoreManager Performs app store API operations
|
||||
* @param originalTransactionId The client provided originalTransactionId that represents a purchased subscription in
|
||||
* the app store
|
||||
* @return A stage that completes with the subscription level for the accepted subscription
|
||||
*/
|
||||
public CompletableFuture<Long> updateAppStoreTransactionId(
|
||||
final SubscriberCredentials subscriberCredentials,
|
||||
final AppleAppStoreManager appleAppStoreManager,
|
||||
final String originalTransactionId) {
|
||||
|
||||
return getSubscriber(subscriberCredentials).thenCompose(record -> {
|
||||
if (record.processorCustomer != null
|
||||
&& record.processorCustomer.processor() != PaymentProvider.APPLE_APP_STORE) {
|
||||
return CompletableFuture.failedFuture(
|
||||
new SubscriptionException.ProcessorConflict("existing processor does not match"));
|
||||
}
|
||||
|
||||
// For IAP providers, the subscriptionId and the customerId are both just the identifier for the subscription in
|
||||
// the provider (in this case, the originalTransactionId). Changes to the subscription always just result in a new
|
||||
// originalTransactionId
|
||||
final ProcessorCustomer pc = new ProcessorCustomer(originalTransactionId, PaymentProvider.APPLE_APP_STORE);
|
||||
|
||||
return appleAppStoreManager
|
||||
.validateTransaction(originalTransactionId)
|
||||
.thenCompose(level -> subscriptions
|
||||
.setIapPurchase(record, pc, originalTransactionId, level, subscriberCredentials.now())
|
||||
.thenApply(ignore -> level));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private SubscriptionPaymentProcessor getProcessor(PaymentProvider provider) {
|
||||
return processors.get(provider);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user