Add PayPalLineItemInput with localized description

This commit is contained in:
Chris Eager
2026-02-06 12:09:25 -06:00
committed by Chris Eager
parent 118b1d31cf
commit 39beb59b58
9 changed files with 129 additions and 15 deletions

View File

@@ -257,6 +257,7 @@ import org.whispersystems.textsecuregcm.subscriptions.AppleAppStoreClient;
import org.whispersystems.textsecuregcm.subscriptions.AppleAppStoreManager;
import org.whispersystems.textsecuregcm.subscriptions.BankMandateTranslator;
import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager;
import org.whispersystems.textsecuregcm.subscriptions.PayPalDonationsTranslator;
import org.whispersystems.textsecuregcm.subscriptions.GooglePlayBillingManager;
import org.whispersystems.textsecuregcm.subscriptions.StripeManager;
import org.whispersystems.textsecuregcm.telephony.CarrierDataProvider;
@@ -419,6 +420,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
ConfiguredProfileBadgeConverter profileBadgeConverter = new ConfiguredProfileBadgeConverter(
clock, config.getBadges(), headerControlledResourceBundleLookup);
BankMandateTranslator bankMandateTranslator = new BankMandateTranslator(headerControlledResourceBundleLookup);
PayPalDonationsTranslator payPalDonationsTranslator =
new PayPalDonationsTranslator(headerControlledResourceBundleLookup);
environment.lifecycle().manage(new ManagedAwsCrt());
@@ -1135,7 +1138,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
subscriptionManager, stripeManager, braintreeManager, googlePlayBillingManager, appleAppStoreManager,
profileBadgeConverter, bankMandateTranslator, dynamicConfigurationManager));
commonControllers.add(new OneTimeDonationController(clock, config.getOneTimeDonations(), stripeManager, braintreeManager,
zkReceiptOperations, issuedReceiptsManager, oneTimeDonationsManager));
payPalDonationsTranslator, zkReceiptOperations, issuedReceiptsManager, oneTimeDonationsManager));
}
for (Object controller : commonControllers) {

View File

@@ -36,6 +36,7 @@ import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
@@ -57,6 +58,7 @@ import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
import org.whispersystems.textsecuregcm.storage.OneTimeDonationsManager;
import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager;
import org.whispersystems.textsecuregcm.subscriptions.ChargeFailure;
import org.whispersystems.textsecuregcm.subscriptions.PayPalDonationsTranslator;
import org.whispersystems.textsecuregcm.subscriptions.CustomerAwareSubscriptionPaymentProcessor;
import org.whispersystems.textsecuregcm.subscriptions.PaymentDetails;
import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod;
@@ -90,6 +92,7 @@ public class OneTimeDonationController {
private final OneTimeDonationConfiguration oneTimeDonationConfiguration;
private final StripeManager stripeManager;
private final BraintreeManager braintreeManager;
private final PayPalDonationsTranslator payPalDonationsTranslator;
private final ServerZkReceiptOperations zkReceiptOperations;
private final IssuedReceiptsManager issuedReceiptsManager;
private final OneTimeDonationsManager oneTimeDonationsManager;
@@ -99,6 +102,7 @@ public class OneTimeDonationController {
@Nonnull OneTimeDonationConfiguration oneTimeDonationConfiguration,
@Nonnull StripeManager stripeManager,
@Nonnull BraintreeManager braintreeManager,
@Nonnull PayPalDonationsTranslator payPalDonationsTranslator,
@Nonnull ServerZkReceiptOperations zkReceiptOperations,
@Nonnull IssuedReceiptsManager issuedReceiptsManager,
@Nonnull OneTimeDonationsManager oneTimeDonationsManager) {
@@ -106,6 +110,7 @@ public class OneTimeDonationController {
this.oneTimeDonationConfiguration = Objects.requireNonNull(oneTimeDonationConfiguration);
this.stripeManager = Objects.requireNonNull(stripeManager);
this.braintreeManager = Objects.requireNonNull(braintreeManager);
this.payPalDonationsTranslator = Objects.requireNonNull(payPalDonationsTranslator);
this.zkReceiptOperations = Objects.requireNonNull(zkReceiptOperations);
this.issuedReceiptsManager = Objects.requireNonNull(issuedReceiptsManager);
this.oneTimeDonationsManager = Objects.requireNonNull(oneTimeDonationsManager);
@@ -265,14 +270,23 @@ public class OneTimeDonationController {
validateRequestCurrencyAmount(request, BigDecimal.valueOf(request.amount), braintreeManager);
})
.thenCompose(unused -> {
final Locale locale = HeaderUtils.getAcceptableLanguagesForRequest(containerRequestContext).stream()
final List<Locale> acceptableLanguages =
HeaderUtils.getAcceptableLanguagesForRequest(containerRequestContext);
// These two localizations are a best-effort, and it's possible that the first `locale` and the localized line
// item name will not match. We could try to align with the locales PayPal documents <https://developer.paypal.com/reference/locale-codes/#supported-locale-codes>
// but that's a moving target, and we can hopefully have one of them be better for the user by selecting
// independently.
final Locale locale = acceptableLanguages.stream()
.filter(l -> !"*".equals(l.getLanguage()))
.findFirst()
.orElse(Locale.US);
final String localizedLineItemName = payPalDonationsTranslator.translate(acceptableLanguages,
PayPalDonationsTranslator.ONE_TIME_DONATION_LINE_ITEM_KEY);
return braintreeManager.createOneTimePayment(request.currency.toUpperCase(Locale.ROOT), request.amount,
locale.toLanguageTag(),
request.returnUrl, request.cancelUrl);
request.returnUrl, request.cancelUrl, localizedLineItemName);
})
.thenApply(approvalDetails -> Response.ok(
new CreatePayPalBoostResponse(approvalDetails.approvalUrl(), approvalDetails.paymentId())).build());

View File

@@ -21,12 +21,14 @@ import com.braintree.graphql.client.type.PayPalBillingAgreementInput;
import com.braintree.graphql.client.type.PayPalExperienceProfileInput;
import com.braintree.graphql.client.type.PayPalIntent;
import com.braintree.graphql.client.type.PayPalLandingPageType;
import com.braintree.graphql.client.type.PayPalLineItemInput;
import com.braintree.graphql.client.type.PayPalOneTimePaymentInput;
import com.braintree.graphql.client.type.PayPalProductAttributesInput;
import com.braintree.graphql.client.type.PayPalUserAction;
import com.braintree.graphql.client.type.TokenizePayPalBillingAgreementInput;
import com.braintree.graphql.client.type.TokenizePayPalOneTimePaymentInput;
import com.braintree.graphql.client.type.TransactionInput;
import com.braintree.graphql.client.type.TransactionLineItemType;
import com.braintree.graphql.client.type.VaultPaymentMethodInput;
import com.braintree.graphql.clientoperation.ChargePayPalOneTimePaymentMutation;
import com.braintree.graphql.clientoperation.CreatePayPalBillingAgreementMutation;
@@ -77,10 +79,10 @@ class BraintreeGraphqlClient {
CompletableFuture<CreatePayPalOneTimePaymentMutation.CreatePayPalOneTimePayment> createPayPalOneTimePayment(
final BigDecimal amount, final String currency, final String returnUrl,
final String cancelUrl, final String locale) {
final String cancelUrl, final String locale, final String localizedLineItemName) {
final CreatePayPalOneTimePaymentInput input = buildCreatePayPalOneTimePaymentInput(amount, currency, returnUrl,
cancelUrl, locale);
cancelUrl, locale, localizedLineItemName);
final CreatePayPalOneTimePaymentMutation mutation = new CreatePayPalOneTimePaymentMutation(input);
final HttpRequest request = buildRequest(mutation);
@@ -104,7 +106,12 @@ class BraintreeGraphqlClient {
}
private static CreatePayPalOneTimePaymentInput buildCreatePayPalOneTimePaymentInput(BigDecimal amount,
String currency, String returnUrl, String cancelUrl, String locale) {
String currency, String returnUrl, String cancelUrl, String locale, String localizedLineItemName) {
// Note locale and localizedLineItemName are a best-effort, and it's possible that they will not match.
// We could try to align with the locales PayPal documents <https://developer.paypal.com/reference/locale-codes/#supported-locale-codes>
// but that's a moving target, and we can hopefully have one of them be better for the user by selecting
// independently.
return new CreatePayPalOneTimePaymentInput(
Optional.absent(),
@@ -113,7 +120,17 @@ class BraintreeGraphqlClient {
cancelUrl,
Optional.absent(),
PayPalIntent.SALE,
Optional.absent(),
Optional.present(List.of(
new PayPalLineItemInput(
localizedLineItemName,
1, // quantity, always 1
amount.toString(),
TransactionLineItemType.DEBIT,
Optional.absent(),
Optional.absent(),
0, // unitTaxAmount, always zero
Optional.absent()
))),
Optional.present(false), // offerPayLater,
Optional.absent(),
Optional.present(

View File

@@ -149,10 +149,10 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess
}
public CompletableFuture<PayPalOneTimePaymentApprovalDetails> createOneTimePayment(String currency, long amount,
String locale, String returnUrl, String cancelUrl) {
String locale, String returnUrl, String cancelUrl, String localizedLineItemname) {
return braintreeGraphqlClient.createPayPalOneTimePayment(convertApiAmountToBraintreeAmount(currency, amount),
currency.toUpperCase(Locale.ROOT), returnUrl,
cancelUrl, locale)
cancelUrl, locale, localizedLineItemname)
.thenApply(result -> new PayPalOneTimePaymentApprovalDetails((String) result.approvalUrl, result.paymentId));
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.subscriptions;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.ResourceBundle;
import javax.annotation.Nonnull;
import org.signal.i18n.HeaderControlledResourceBundleLookup;
public class PayPalDonationsTranslator {
public static final String ONE_TIME_DONATION_LINE_ITEM_KEY = "oneTime.donationLineItemName";
private static final String BASE_NAME = "org.signal.donations.PayPal";
private final HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup;
public PayPalDonationsTranslator(
@Nonnull final HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup) {
this.headerControlledResourceBundleLookup = Objects.requireNonNull(headerControlledResourceBundleLookup);
}
public String translate(final List<Locale> acceptableLanguages, final String key) {
final ResourceBundle resourceBundle = headerControlledResourceBundleLookup.getResourceBundle(BASE_NAME,
acceptableLanguages);
return resourceBundle.getString(key);
}
}