Add initial PayPal implementation behind a feature flag.

This commit is contained in:
Alex Hart
2022-11-30 12:43:46 -04:00
committed by Cody Henthorne
parent b70b4fac91
commit 979f87db78
47 changed files with 1382 additions and 144 deletions
@@ -9,6 +9,9 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription;
import org.whispersystems.signalservice.api.subscriptions.PayPalConfirmPaymentIntentResponse;
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentIntentResponse;
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentMethodResponse;
import org.whispersystems.signalservice.api.subscriptions.StripeClientSecret;
import org.whispersystems.signalservice.api.subscriptions.SubscriberId;
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels;
@@ -16,6 +19,7 @@ import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.internal.EmptyResponse;
import org.whispersystems.signalservice.internal.ServiceResponse;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.push.DonationProcessor;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import java.io.IOException;
@@ -87,8 +91,8 @@ public class DonationsService {
* @param paymentIntentId PaymentIntent ID from a boost donation intent response.
* @param receiptCredentialRequest Client-generated request token
*/
public ServiceResponse<ReceiptCredentialResponse> submitBoostReceiptCredentialRequestSync(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest) {
return wrapInServiceResponse(() -> new Pair<>(pushServiceSocket.submitBoostReceiptCredentials(paymentIntentId, receiptCredentialRequest), 200));
public ServiceResponse<ReceiptCredentialResponse> submitBoostReceiptCredentialRequestSync(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest, DonationProcessor processor) {
return wrapInServiceResponse(() -> new Pair<>(pushServiceSocket.submitBoostReceiptCredentials(paymentIntentId, receiptCredentialRequest, processor), 200));
}
/**
@@ -217,24 +221,129 @@ public class DonationsService {
public ServiceResponse<EmptyResponse> setDefaultStripePaymentMethod(SubscriberId subscriberId, String paymentMethodId) {
return wrapInServiceResponse(() -> {
pushServiceSocket.setDefaultSubscriptionPaymentMethod(subscriberId.serialize(), paymentMethodId);
pushServiceSocket.setDefaultStripeSubscriptionPaymentMethod(subscriberId.serialize(), paymentMethodId);
return new Pair<>(EmptyResponse.INSTANCE, 200);
});
}
/**
*
* @param subscriberId The subscriber ID to create a payment method for.
* @return Client secret for a SetupIntent. It should not be used with the PaymentIntent stripe APIs
* but instead with the SetupIntent stripe APIs.
* @param subscriberId The subscriber ID to create a payment method for.
* @return Client secret for a SetupIntent. It should not be used with the PaymentIntent stripe APIs
* but instead with the SetupIntent stripe APIs.
*/
public ServiceResponse<StripeClientSecret> createStripeSubscriptionPaymentMethod(SubscriberId subscriberId) {
return wrapInServiceResponse(() -> {
StripeClientSecret clientSecret = pushServiceSocket.createSubscriptionPaymentMethod(subscriberId.serialize());
StripeClientSecret clientSecret = pushServiceSocket.createStripeSubscriptionPaymentMethod(subscriberId.serialize());
return new Pair<>(clientSecret, 200);
});
}
/**
* Creates a PayPal one-time payment and returns the approval URL
* Response Codes
* 200 — success
* 400 — request error
* 409 — level requires a valid currency/amount combination that does not match
*
* @param locale User locale for proper language presentation
* @param currencyCode 3 letter currency code of the desired currency
* @param amount Stringified minimum precision amount
* @param level The badge level to purchase
* @param returnUrl The 'return' url after a successful login and confirmation
* @param cancelUrl The 'cancel' url for a cancelled confirmation
* @return Wrapped response with either an error code or a payment id and approval URL
*/
public ServiceResponse<PayPalCreatePaymentIntentResponse> createPayPalOneTimePaymentIntent(Locale locale,
String currencyCode,
String amount,
long level,
String returnUrl,
String cancelUrl)
{
return wrapInServiceResponse(() -> {
PayPalCreatePaymentIntentResponse response = pushServiceSocket.createPayPalOneTimePaymentIntent(
locale,
currencyCode.toUpperCase(Locale.US), // Chris Eager to make this case insensitive in the next build
Long.parseLong(amount),
level,
returnUrl,
cancelUrl
);
return new Pair<>(response, 200);
});
}
/**
* Confirms a PayPal one-time payment and returns the paymentId for receipt credentials
* Response Codes
* 200 — success
* 400 — request error
* 409 — level requires a valid currency/amount combination that does not match
*
* @param currency 3 letter currency code of the desired currency
* @param amount Stringified minimum precision amount
* @param level The badge level to purchase
* @param payerId Passed as a URL parameter back to returnUrl
* @param paymentId Passed as a URL parameter back to returnUrl
* @param paymentToken Passed as a URL parameter back to returnUrl
* @return Wrapped response with either an error code or a payment id
*/
public ServiceResponse<PayPalConfirmPaymentIntentResponse> confirmPayPalOneTimePaymentIntent(String currency,
String amount,
long level,
String payerId,
String paymentId,
String paymentToken)
{
return wrapInServiceResponse(() -> {
PayPalConfirmPaymentIntentResponse response = pushServiceSocket.confirmPayPalOneTimePaymentIntent(currency, amount, level, payerId, paymentId, paymentToken);
return new Pair<>(response, 200);
});
}
/**
* Sets up a payment method via PayPal for recurring charges.
*
* Response Codes
* 200 — success
* 403 — subscriberId password mismatches OR account authentication is present
* 404 — subscriberId is not found or malformed
*
* @param locale User locale
* @param subscriberId User subscriber id
* @param returnUrl A success URL
* @param cancelUrl A cancel URL
* @return A response with an approval url and token
*/
public ServiceResponse<PayPalCreatePaymentMethodResponse> createPayPalPaymentMethod(Locale locale,
SubscriberId subscriberId,
String returnUrl,
String cancelUrl) {
return wrapInServiceResponse(() -> {
PayPalCreatePaymentMethodResponse response = pushServiceSocket.createPayPalPaymentMethod(locale, subscriberId.serialize(), returnUrl, cancelUrl);
return new Pair<>(response, 200);
});
}
/**
* Sets the given payment method as the default in PayPal
*
* Response Codes
* 200 — success
* 403 — subscriberId password mismatches OR account authentication is present
* 404 — subscriberId is not found or malformed
* 409 — subscriber record is missing customer ID - must call POST /v1/subscription/{subscriberId}/create_payment_method first
*
* @param subscriberId User subscriber id
* @param paymentMethodId Payment method id to make default
*/
public ServiceResponse<EmptyResponse> setDefaultPayPalPaymentMethod(SubscriberId subscriberId, String paymentMethodId) {
return wrapInServiceResponse(() -> {
pushServiceSocket.setDefaultPaypalSubscriptionPaymentMethod(subscriberId.serialize(), paymentMethodId);
return new Pair<>(EmptyResponse.INSTANCE, 200);
});
}
public ServiceResponse<ReceiptCredentialResponse> submitReceiptCredentialRequestSync(SubscriberId subscriberId, ReceiptCredentialRequest receiptCredentialRequest) {
return wrapInServiceResponse(() -> {
ReceiptCredentialResponse response = pushServiceSocket.submitReceiptCredentials(subscriberId.serialize(), receiptCredentialRequest);
@@ -0,0 +1,21 @@
package org.whispersystems.signalservice.api.subscriptions;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Response object from creating a payment intent via PayPal
*/
public class PayPalConfirmPaymentIntentResponse {
private final String paymentId;
@JsonCreator
public PayPalConfirmPaymentIntentResponse(@JsonProperty("paymentId") String paymentId) {
this.paymentId = paymentId;
}
public String getPaymentId() {
return paymentId;
}
}
@@ -0,0 +1,27 @@
package org.whispersystems.signalservice.api.subscriptions;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Response object from creating a payment intent via PayPal
*/
public class PayPalCreatePaymentIntentResponse {
private final String approvalUrl;
private final String paymentId;
@JsonCreator
public PayPalCreatePaymentIntentResponse(@JsonProperty("approvalUrl") String approvalUrl, @JsonProperty("paymentId") String paymentId) {
this.approvalUrl = approvalUrl;
this.paymentId = paymentId;
}
public String getApprovalUrl() {
return approvalUrl;
}
public String getPaymentId() {
return paymentId;
}
}
@@ -0,0 +1,23 @@
package org.whispersystems.signalservice.api.subscriptions;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class PayPalCreatePaymentMethodResponse {
private final String approvalUrl;
private final String token;
@JsonCreator
public PayPalCreatePaymentMethodResponse(@JsonProperty("approvalUrl") String approvalUrl, @JsonProperty("token") String token) {
this.approvalUrl = approvalUrl;
this.token = token;
}
public String getApprovalUrl() {
return approvalUrl;
}
public String getToken() {
return token;
}
}
@@ -12,8 +12,12 @@ class BoostReceiptCredentialRequestJson {
@JsonProperty("receiptCredentialRequest")
private final String receiptCredentialRequest;
BoostReceiptCredentialRequestJson(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest) {
@JsonProperty("processor")
private final String processor;
BoostReceiptCredentialRequestJson(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest, DonationProcessor processor) {
this.paymentIntentId = paymentIntentId;
this.receiptCredentialRequest = Base64.encodeBytes(receiptCredentialRequest.serialize());
this.processor = processor.getCode();
}
}
@@ -0,0 +1,32 @@
package org.whispersystems.signalservice.internal.push;
import java.util.Objects;
/**
* Represents the processor being used for a given payment, required when accessing
* receipt credentials.
*/
public enum DonationProcessor {
STRIPE("STRIPE"),
PAYPAL("BRAINTREE");
private final String code;
DonationProcessor(String code) {
this.code = code;
}
public String getCode() {
return code;
}
public static DonationProcessor fromCode(String code) {
for (final DonationProcessor value : values()) {
if (Objects.equals(code, value.code)) {
return value;
}
}
throw new IllegalArgumentException(code);
}
}
@@ -0,0 +1,35 @@
package org.whispersystems.signalservice.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Request JSON for confirming a PayPal one-time payment intent
*/
class PayPalConfirmOneTimePaymentIntentPayload {
@JsonProperty
private String amount;
@JsonProperty
private String currency;
@JsonProperty
private long level;
@JsonProperty
private String payerId;
@JsonProperty
private String paymentId;
@JsonProperty
private String paymentToken;
public PayPalConfirmOneTimePaymentIntentPayload(String amount, String currency, long level, String payerId, String paymentId, String paymentToken) {
this.amount = amount;
this.currency = currency;
this.level = level;
this.payerId = payerId;
this.paymentId = paymentId;
this.paymentToken = paymentToken;
}
}
@@ -0,0 +1,31 @@
package org.whispersystems.signalservice.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Request JSON for creating a PayPal one-time payment intent
*/
class PayPalCreateOneTimePaymentIntentPayload {
@JsonProperty
private long amount;
@JsonProperty
private String currency;
@JsonProperty
private long level;
@JsonProperty
private String returnUrl;
@JsonProperty
private String cancelUrl;
public PayPalCreateOneTimePaymentIntentPayload(long amount, String currency, long level, String returnUrl, String cancelUrl) {
this.amount = amount;
this.currency = currency;
this.level = level;
this.returnUrl = returnUrl;
this.cancelUrl = cancelUrl;
}
}
@@ -0,0 +1,16 @@
package org.whispersystems.signalservice.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
class PayPalCreatePaymentMethodPayload {
@JsonProperty
private String returnUrl;
@JsonProperty
private String cancelUrl;
PayPalCreatePaymentMethodPayload(String returnUrl, String cancelUrl) {
this.returnUrl = returnUrl;
this.cancelUrl = cancelUrl;
}
}
@@ -86,6 +86,9 @@ import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedExc
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
import org.whispersystems.signalservice.api.storage.StorageAuthResponse;
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription;
import org.whispersystems.signalservice.api.subscriptions.PayPalConfirmPaymentIntentResponse;
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentIntentResponse;
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentMethodResponse;
import org.whispersystems.signalservice.api.subscriptions.StripeClientSecret;
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
@@ -261,17 +264,21 @@ public class PushServiceSocket {
private static final String DONATION_REDEEM_RECEIPT = "/v1/donation/redeem-receipt";
private static final String SUBSCRIPTION_LEVELS = "/v1/subscription/levels";
private static final String UPDATE_SUBSCRIPTION_LEVEL = "/v1/subscription/%s/level/%s/%s/%s";
private static final String SUBSCRIPTION = "/v1/subscription/%s";
private static final String CREATE_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/create_payment_method";
private static final String DEFAULT_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method/%s";
private static final String SUBSCRIPTION_RECEIPT_CREDENTIALS = "/v1/subscription/%s/receipt_credentials";
private static final String BOOST_AMOUNTS = "/v1/subscription/boost/amounts";
private static final String GIFT_AMOUNT = "/v1/subscription/boost/amounts/gift";
private static final String CREATE_BOOST_PAYMENT_INTENT = "/v1/subscription/boost/create";
private static final String BOOST_RECEIPT_CREDENTIALS = "/v1/subscription/boost/receipt_credentials";
private static final String BOOST_BADGES = "/v1/subscription/boost/badges";
private static final String SUBSCRIPTION_LEVELS = "/v1/subscription/levels";
private static final String UPDATE_SUBSCRIPTION_LEVEL = "/v1/subscription/%s/level/%s/%s/%s";
private static final String SUBSCRIPTION = "/v1/subscription/%s";
private static final String CREATE_STRIPE_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/create_payment_method";
private static final String CREATE_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/create_payment_method/paypal";
private static final String DEFAULT_STRIPE_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method/%s";
private static final String DEFAULT_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method/paypal/%s";
private static final String SUBSCRIPTION_RECEIPT_CREDENTIALS = "/v1/subscription/%s/receipt_credentials";
private static final String BOOST_AMOUNTS = "/v1/subscription/boost/amounts";
private static final String GIFT_AMOUNT = "/v1/subscription/boost/amounts/gift";
private static final String CREATE_STRIPE_ONE_TIME_PAYMENT_INTENT = "/v1/subscription/boost/create";
private static final String CREATE_PAYPAL_ONE_TIME_PAYMENT_INTENT = "/v1/subscription/boost/paypal/create";
private static final String CONFIRM_PAYPAL_ONE_TIME_PAYMENT_INTENT = "/v1/subscription/boost/paypal/confirm";
private static final String BOOST_RECEIPT_CREDENTIALS = "/v1/subscription/boost/receipt_credentials";
private static final String BOOST_BADGES = "/v1/subscription/boost/badges";
private static final String CDSI_AUTH = "/v2/directory/auth";
@@ -1019,10 +1026,33 @@ public class PushServiceSocket {
public StripeClientSecret createStripeOneTimePaymentIntent(String currencyCode, long amount, long level) throws IOException {
String payload = JsonUtil.toJson(new StripeOneTimePaymentIntentPayload(amount, currencyCode, level));
String result = makeServiceRequestWithoutAuthentication(CREATE_BOOST_PAYMENT_INTENT, "POST", payload);
String result = makeServiceRequestWithoutAuthentication(CREATE_STRIPE_ONE_TIME_PAYMENT_INTENT, "POST", payload);
return JsonUtil.fromJsonResponse(result, StripeClientSecret.class);
}
public PayPalCreatePaymentIntentResponse createPayPalOneTimePaymentIntent(Locale locale, String currencyCode, long amount, long level, String returnUrl, String cancelUrl) throws IOException {
Map<String, String> headers = Collections.singletonMap("Accept-Language", locale.getLanguage() + "-" + locale.getCountry());
String payload = JsonUtil.toJson(new PayPalCreateOneTimePaymentIntentPayload(amount, currencyCode, level, returnUrl, cancelUrl));
String result = makeServiceRequestWithoutAuthentication(CREATE_PAYPAL_ONE_TIME_PAYMENT_INTENT, "POST", payload, headers, NO_HANDLER);
return JsonUtil.fromJsonResponse(result, PayPalCreatePaymentIntentResponse.class);
}
public PayPalConfirmPaymentIntentResponse confirmPayPalOneTimePaymentIntent(String currency, String amount, long level, String payerId, String paymentId, String paymentToken) throws IOException {
String payload = JsonUtil.toJson(new PayPalConfirmOneTimePaymentIntentPayload(amount, currency, level, payerId, paymentId, paymentToken));
Log.d(TAG, payload);
String result = makeServiceRequestWithoutAuthentication(CONFIRM_PAYPAL_ONE_TIME_PAYMENT_INTENT, "POST", payload);
return JsonUtil.fromJsonResponse(result, PayPalConfirmPaymentIntentResponse.class);
}
public PayPalCreatePaymentMethodResponse createPayPalPaymentMethod(Locale locale, String subscriberId, String returnUrl, String cancelUrl) throws IOException {
Map<String, String> headers = Collections.singletonMap("Accept-Language", locale.getLanguage() + "-" + locale.getCountry());
String payload = JsonUtil.toJson(new PayPalCreatePaymentMethodPayload(returnUrl, cancelUrl));
String result = makeServiceRequestWithoutAuthentication(String.format(CREATE_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD, subscriberId), "POST", payload);
return JsonUtil.fromJsonResponse(result, PayPalCreatePaymentMethodResponse.class);
}
public Map<String, List<BigDecimal>> getBoostAmounts() throws IOException {
String result = makeServiceRequestWithoutAuthentication(BOOST_AMOUNTS, "GET", null);
TypeReference<HashMap<String, List<BigDecimal>>> typeRef = new TypeReference<HashMap<String, List<BigDecimal>>>() {};
@@ -1040,8 +1070,8 @@ public class PushServiceSocket {
return JsonUtil.fromJsonResponse(result, SubscriptionLevels.class);
}
public ReceiptCredentialResponse submitBoostReceiptCredentials(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest) throws IOException {
String payload = JsonUtil.toJson(new BoostReceiptCredentialRequestJson(paymentIntentId, receiptCredentialRequest));
public ReceiptCredentialResponse submitBoostReceiptCredentials(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest, DonationProcessor processor) throws IOException {
String payload = JsonUtil.toJson(new BoostReceiptCredentialRequestJson(paymentIntentId, receiptCredentialRequest, processor));
String response = makeServiceRequestWithoutAuthentication(
BOOST_RECEIPT_CREDENTIALS,
"POST",
@@ -1082,13 +1112,17 @@ public class PushServiceSocket {
makeServiceRequestWithoutAuthentication(String.format(SUBSCRIPTION, subscriberId), "DELETE", null);
}
public StripeClientSecret createSubscriptionPaymentMethod(String subscriberId) throws IOException {
String response = makeServiceRequestWithoutAuthentication(String.format(CREATE_SUBSCRIPTION_PAYMENT_METHOD, subscriberId), "POST", "");
public StripeClientSecret createStripeSubscriptionPaymentMethod(String subscriberId) throws IOException {
String response = makeServiceRequestWithoutAuthentication(String.format(CREATE_STRIPE_SUBSCRIPTION_PAYMENT_METHOD, subscriberId), "POST", "");
return JsonUtil.fromJson(response, StripeClientSecret.class);
}
public void setDefaultSubscriptionPaymentMethod(String subscriberId, String paymentMethodId) throws IOException {
makeServiceRequestWithoutAuthentication(String.format(DEFAULT_SUBSCRIPTION_PAYMENT_METHOD, subscriberId, paymentMethodId), "POST", "");
public void setDefaultStripeSubscriptionPaymentMethod(String subscriberId, String paymentMethodId) throws IOException {
makeServiceRequestWithoutAuthentication(String.format(DEFAULT_STRIPE_SUBSCRIPTION_PAYMENT_METHOD, subscriberId, paymentMethodId), "POST", "");
}
public void setDefaultPaypalSubscriptionPaymentMethod(String subscriberId, String paymentMethodId) throws IOException {
makeServiceRequestWithoutAuthentication(String.format(DEFAULT_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD, subscriberId, paymentMethodId), "POST", "");
}
public ReceiptCredentialResponse submitReceiptCredentials(String subscriptionId, ReceiptCredentialRequest receiptCredentialRequest) throws IOException {