Attach client platforms when creating donations

This commit is contained in:
Jon Chambers 2024-04-22 09:31:57 -04:00 committed by GitHub
parent b8f64fe3d4
commit ed72d7f9ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 61 additions and 26 deletions

View File

@ -46,8 +46,6 @@ public class DonationController {
ReceiptCredentialPresentation build(byte[] bytes) throws InvalidInputException;
}
private static final Logger logger = LoggerFactory.getLogger(DonationController.class);
private final Clock clock;
private final ServerZkReceiptOperations serverZkReceiptOperations;
private final RedeemedReceiptsManager redeemedReceiptsManager;

View File

@ -10,7 +10,6 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.net.HttpHeaders;
import com.stripe.exception.StripeException;
import com.vdurmont.semver4j.Semver;
import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
@ -36,6 +35,7 @@ import java.util.concurrent.CompletionException;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.validation.Valid;
@ -99,6 +99,9 @@ import org.whispersystems.textsecuregcm.subscriptions.SubscriptionCurrencyUtil;
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessor;
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorManager;
import org.whispersystems.textsecuregcm.util.ExactlySize;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
import org.whispersystems.websocket.auth.ReadOnly;
@Path("/v1/subscription")
@ -126,7 +129,6 @@ public class SubscriptionController {
private static final String TYPE_TAG_NAME = "type";
private static final String SUBSCRIPTION_TYPE_TAG_NAME = "subscriptionType";
private static final String EURO_CURRENCY_CODE = "EUR";
private static final Semver LAST_PROBLEMATIC_IOS_VERSION = new Semver("6.44.0");
public SubscriptionController(
@Nonnull Clock clock,
@ -290,7 +292,8 @@ public class SubscriptionController {
public CompletableFuture<Response> createPaymentMethod(
@ReadOnly @Auth Optional<AuthenticatedAccount> authenticatedAccount,
@PathParam("subscriberId") String subscriberId,
@QueryParam("type") @DefaultValue("CARD") PaymentMethod paymentMethodType) {
@QueryParam("type") @DefaultValue("CARD") PaymentMethod paymentMethodType,
@HeaderParam(HttpHeaders.USER_AGENT) @Nullable final String userAgentString) {
RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock);
@ -309,7 +312,7 @@ public class SubscriptionController {
return CompletableFuture.completedFuture(record);
})
.orElseGet(() -> subscriptionProcessorManager.createCustomer(requestData.subscriberUser)
.orElseGet(() -> subscriptionProcessorManager.createCustomer(requestData.subscriberUser, getClientPlatform(userAgentString))
.thenApply(ProcessorCustomer::customerId)
.thenCompose(customerId -> subscriptionManager.setProcessorAndCustomerId(record,
new ProcessorCustomer(customerId, subscriptionProcessorManager.getProcessor()),
@ -345,7 +348,8 @@ public class SubscriptionController {
@ReadOnly @Auth Optional<AuthenticatedAccount> authenticatedAccount,
@PathParam("subscriberId") String subscriberId,
@NotNull @Valid CreatePayPalBillingAgreementRequest request,
@Context ContainerRequestContext containerRequestContext) {
@Context ContainerRequestContext containerRequestContext,
@HeaderParam(HttpHeaders.USER_AGENT) @Nullable final String userAgentString) {
RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock);
@ -362,7 +366,7 @@ public class SubscriptionController {
}
return CompletableFuture.completedFuture(record);
})
.orElseGet(() -> braintreeManager.createCustomer(requestData.subscriberUser)
.orElseGet(() -> braintreeManager.createCustomer(requestData.subscriberUser, getClientPlatform(userAgentString))
.thenApply(ProcessorCustomer::customerId)
.thenCompose(customerId -> subscriptionManager.setProcessorAndCustomerId(record,
new ProcessorCustomer(customerId, braintreeManager.getProcessor()),
@ -665,7 +669,9 @@ public class SubscriptionController {
@Path("/boost/create")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public CompletableFuture<Response> createBoostPaymentIntent(@NotNull @Valid CreateBoostRequest request) {
public CompletableFuture<Response> createBoostPaymentIntent(@NotNull @Valid CreateBoostRequest request,
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent) {
return CompletableFuture.runAsync(() -> {
if (request.level == null) {
request.level = oneTimeDonationConfiguration.boost().level();
@ -683,7 +689,7 @@ public class SubscriptionController {
}
validateRequestCurrencyAmount(request, amount, stripeManager);
})
.thenCompose(unused -> stripeManager.createPaymentIntent(request.currency, request.amount, request.level))
.thenCompose(unused -> stripeManager.createPaymentIntent(request.currency, request.amount, request.level, getClientPlatform(userAgent)))
.thenApply(paymentIntent -> Response.ok(new CreateBoostResponse(paymentIntent.getClientSecret())).build());
}
@ -769,7 +775,8 @@ public class SubscriptionController {
@Path("/boost/paypal/confirm")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public CompletableFuture<Response> confirmPayPalBoost(@NotNull @Valid ConfirmPayPalBoostRequest request) {
public CompletableFuture<Response> confirmPayPalBoost(@NotNull @Valid ConfirmPayPalBoostRequest request,
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent) {
return CompletableFuture.runAsync(() -> {
if (request.level == null) {
@ -1072,6 +1079,15 @@ public class SubscriptionController {
}
}
@Nullable
private static ClientPlatform getClientPlatform(@Nullable final String userAgentString) {
try {
return UserAgentUtil.parseUserAgentString(userAgentString).getPlatform();
} catch (final UnrecognizedUserAgentException e) {
return null;
}
}
private record RequestData(@Nonnull byte[] subscriberBytes,
@Nonnull byte[] subscriberUser,
@Nonnull byte[] subscriberKey,

View File

@ -44,6 +44,7 @@ import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
public class BraintreeManager implements SubscriptionProcessorManager {
@ -252,10 +253,15 @@ public class BraintreeManager implements SubscriptionProcessorManager {
}
@Override
public CompletableFuture<ProcessorCustomer> createCustomer(final byte[] subscriberUser) {
public CompletableFuture<ProcessorCustomer> createCustomer(final byte[] subscriberUser, @Nullable final ClientPlatform clientPlatform) {
return CompletableFuture.supplyAsync(() -> {
final CustomerRequest request = new CustomerRequest()
CustomerRequest request = new CustomerRequest()
.customField("subscriber_user", HexFormat.of().formatHex(subscriberUser));
if (clientPlatform != null) {
request.customField("client_platform", clientPlatform.name().toLowerCase());
}
try {
return braintreeGateway.customer().create(request);
} catch (BraintreeException e) {

View File

@ -74,10 +74,12 @@ import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.Conversions;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
public class StripeManager implements SubscriptionProcessorManager {
private static final Logger logger = LoggerFactory.getLogger(StripeManager.class);
private static final String METADATA_KEY_LEVEL = "level";
private static final String METADATA_KEY_CLIENT_PLATFORM = "clientPlatform";
private final StripeClient stripeClient;
private final Executor executor;
@ -127,14 +129,18 @@ public class StripeManager implements SubscriptionProcessorManager {
}
@Override
public CompletableFuture<ProcessorCustomer> createCustomer(byte[] subscriberUser) {
public CompletableFuture<ProcessorCustomer> createCustomer(final byte[] subscriberUser, @Nullable final ClientPlatform clientPlatform) {
return CompletableFuture.supplyAsync(() -> {
CustomerCreateParams params = CustomerCreateParams.builder()
.putMetadata("subscriberUser", HexFormat.of().formatHex(subscriberUser))
.build();
final CustomerCreateParams.Builder builder = CustomerCreateParams.builder()
.putMetadata("subscriberUser", HexFormat.of().formatHex(subscriberUser));
if (clientPlatform != null) {
builder.putMetadata(METADATA_KEY_CLIENT_PLATFORM, clientPlatform.name().toLowerCase());
}
try {
return stripeClient.customers()
.create(params, commonOptions(generateIdempotencyKeyForSubscriberUser(subscriberUser)));
.create(builder.build(), commonOptions(generateIdempotencyKeyForSubscriberUser(subscriberUser)));
} catch (StripeException e) {
throw new CompletionException(e);
}
@ -194,16 +200,24 @@ public class StripeManager implements SubscriptionProcessorManager {
/**
* Creates a payment intent. May throw a 400 WebApplicationException if the amount is too small.
*/
public CompletableFuture<PaymentIntent> createPaymentIntent(String currency, long amount, long level) {
public CompletableFuture<PaymentIntent> createPaymentIntent(final String currency,
final long amount,
final long level,
@Nullable final ClientPlatform clientPlatform) {
return CompletableFuture.supplyAsync(() -> {
PaymentIntentCreateParams params = PaymentIntentCreateParams.builder()
final PaymentIntentCreateParams.Builder builder = PaymentIntentCreateParams.builder()
.setAmount(amount)
.setCurrency(currency.toLowerCase(Locale.ROOT))
.setDescription(boostDescription)
.putMetadata("level", Long.toString(level))
.build();
.putMetadata("level", Long.toString(level));
if (clientPlatform != null) {
builder.putMetadata(METADATA_KEY_CLIENT_PLATFORM, clientPlatform.name().toLowerCase());
}
try {
return stripeClient.paymentIntents().create(params, commonOptions());
return stripeClient.paymentIntents().create(builder.build(), commonOptions());
} catch (StripeException e) {
if ("amount_too_small".equalsIgnoreCase(e.getCode())) {
throw new WebApplicationException(Response

View File

@ -13,6 +13,7 @@ import java.util.concurrent.CompletableFuture;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
public interface SubscriptionProcessorManager {
SubscriptionProcessor getProcessor();
@ -23,7 +24,7 @@ public interface SubscriptionProcessorManager {
CompletableFuture<PaymentDetails> getPaymentDetails(String paymentId);
CompletableFuture<ProcessorCustomer> createCustomer(byte[] subscriberUser);
CompletableFuture<ProcessorCustomer> createCustomer(byte[] subscriberUser, @Nullable ClientPlatform clientPlatform);
CompletableFuture<String> createPaymentMethodSetupToken(String customerId);

View File

@ -231,7 +231,7 @@ class SubscriptionControllerTest {
void testCreateBoostPaymentIntent() {
when(STRIPE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.CARD))
.thenReturn(Set.of("usd", "jpy", "bif", "eur"));
when(STRIPE_MANAGER.createPaymentIntent(anyString(), anyLong(), anyLong()))
when(STRIPE_MANAGER.createPaymentIntent(anyString(), anyLong(), anyLong(), any()))
.thenReturn(CompletableFuture.completedFuture(PAYMENT_INTENT));
String clientSecret = "some_client_secret";
@ -584,7 +584,7 @@ class SubscriptionControllerTest {
final String customerId = "some-customer-id";
final ProcessorCustomer customer = new ProcessorCustomer(
customerId, SubscriptionProcessor.STRIPE);
when(STRIPE_MANAGER.createCustomer(any()))
when(STRIPE_MANAGER.createCustomer(any(), any()))
.thenReturn(CompletableFuture.completedFuture(customer));
final Map<String, AttributeValue> dynamoItemWithProcessorCustomer = new HashMap<>(dynamoItem);