Skip to content

Commit

Permalink
PayPal Native - Play Store Compliance Fix (#977)
Browse files Browse the repository at this point in the history
* Bump MXO SDK to 1.3.2 and bump Magnes SDK to 5.5.1

* Add hasUserLocationConsent boolean to PayPal Native request classes and pass flag to the MXO SDK and Magnes

* Add sections to CHANGELOG.md

Co-authored-by: Sarah Koop <skoop@paypal.com>

* Update hasUserLocationConsent doc to remove optional

Co-authored-by: Sarah Koop <skoop@paypal.com>

* Make PayPalNativeRequest constructor package private

Co-authored-by: Sarah Koop <skoop@paypal.com>

---------

Co-authored-by: Sarah Koop <skoop@paypal.com>
  • Loading branch information
tdchow and sarahkoop authored Apr 16, 2024
1 parent 3e071c3 commit 14e072f
Show file tree
Hide file tree
Showing 14 changed files with 162 additions and 25 deletions.
13 changes: 11 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,17 @@

## unreleased

* Updated expiring pinned vendor SSL certificates
* Add `GooglePayClient#tokenize(PaymentData, GooglePayOnActivityResultCallback)` to be invoked after direct Google Play Services integration
* BraintreeCore
* Updated expiring pinned vendor SSL certificates
* GooglePay
* Add `GooglePayClient#tokenize(PaymentData, GooglePayOnActivityResultCallback)` to be invoked after direct Google Play Services integration
* PayPalNativeCheckout
* Bump native-checkout version to `1.3.2`
* Fixes Google Play Store Rejection
* Add `hasUserLocationConsent` property to `PayPalNativeCheckoutRequest`, `PayPalNativeCheckoutVaultRequest` and `PayPalNativeRequest`
* Deprecate existing constructors that do not pass in `hasUserLocationConsent`
* PayPalDataCollector
* Bump Magnes version to `5.5.1`

## 4.44.0 (2024-04-05)

Expand Down
2 changes: 1 addition & 1 deletion PayPalDataCollector/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ android {
}

dependencies {
implementation files('libs/android-magnessdk-5.5.0.jar')
implementation files('libs/android-magnessdk-5.5.1.jar')

implementation deps.annotation
api project(':BraintreeCore')
Expand Down
Binary file not shown.
Binary file not shown.
4 changes: 1 addition & 3 deletions PayPalNativeCheckout/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ dependencies {
implementation deps.appCompat
implementation project(':PayPalDataCollector')

implementation('com.paypal.checkout:android-sdk:1.2.1') {
exclude group: 'com.paypal.android.sdk', module: 'data-collector'
}
implementation('com.paypal.checkout:android-sdk:1.3.2')

testImplementation deps.robolectric
testImplementation deps.jsonAssert
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ private void sendPayPalRequest(
createOrderActions.setBillingAgreementId(payPalResponse.getPairingId());
braintreeClient.sendAnalyticsEvent("paypal-native.billing-agreement.succeeded", payPalContextId);
}
});
}, payPalRequest.hasUserLocationConsent());
} else {
listener.onPayPalFailure(new Exception(error));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ void sendRequest(final Context context, final PayPalNativeRequest payPalRequest,
String pairingIdKey = isBillingAgreement ? "ba_token" : "token";
String pairingId = parsedRedirectUri.getQueryParameter(pairingIdKey);
String clientMetadataId = payPalRequest.getRiskCorrelationId() != null
? payPalRequest.getRiskCorrelationId() : payPalDataCollector.getClientMetadataId(context, configuration, false);
? payPalRequest.getRiskCorrelationId() : payPalDataCollector.getClientMetadataId(context, configuration, payPalRequest.hasUserLocationConsent());

if (pairingId != null) {
payPalResponse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public class PayPalNativeCheckoutRequest extends PayPalNativeRequest implements
private boolean shouldOfferPayLater;

/**
* Deprecated. Use {@link PayPalNativeCheckoutRequest#PayPalNativeCheckoutRequest(String, boolean)} instead.
*
* @param amount The transaction amount in currency units (as * determined by setCurrencyCode).
* For example, "1.20" corresponds to one dollar and twenty cents. Amount must be a non-negative
* number, may optionally contain exactly 2 decimal places separated by '.' and is
Expand All @@ -58,7 +60,30 @@ public class PayPalNativeCheckoutRequest extends PayPalNativeRequest implements
* for mismatches between this client-side amount and the final amount in the Transaction
* are determined by the gateway.
**/
@Deprecated
public PayPalNativeCheckoutRequest(@NonNull String amount) {
this(amount, false);
}

/**
* @param amount The transaction amount in currency units (as * determined by setCurrencyCode).
* For example, "1.20" corresponds to one dollar and twenty cents. Amount must be a non-negative
* number, may optionally contain exactly 2 decimal places separated by '.' and is
* limited to 7 digits before the decimal point.
* <p>
* This amount may differ slightly from the transaction amount. The exact decline rules
* for mismatches between this client-side amount and the final amount in the Transaction
* are determined by the gateway.
* @param hasUserLocationConsent is an optional parameter that informs the SDK
* if your application has obtained consent from the user to collect location data in compliance with
* <a href="https://support.google.com/googleplay/android-developer/answer/10144311#personal-sensitive">Google Play Developer Program policies</a>
* This flag enables PayPal to collect necessary information required for Fraud Detection and Risk Management.
*
* @see <a href="https://support.google.com/googleplay/android-developer/answer/10144311#personal-sensitive">User Data policies for the Google Play Developer Program </a>
* @see <a href="https://support.google.com/googleplay/android-developer/answer/9799150?hl=en#Prominent%20in-app%20disclosure">Examples of prominent in-app disclosures</a>
**/
public PayPalNativeCheckoutRequest(@NonNull String amount, boolean hasUserLocationConsent) {
super(hasUserLocationConsent);
this.amount = amount;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,25 @@ public class PayPalNativeCheckoutVaultRequest extends PayPalNativeRequest implem

private boolean shouldOfferCredit;

/**
* Deprecated. Use {@link PayPalNativeCheckoutVaultRequest#PayPalNativeCheckoutVaultRequest(boolean)} instead.
*/
@Deprecated
public PayPalNativeCheckoutVaultRequest() {
this(false);
}

/**
* @param hasUserLocationConsent informs the SDK
* if your application has obtained consent from the user to collect location data in compliance with
* <a href="https://support.google.com/googleplay/android-developer/answer/10144311#personal-sensitive">Google Play Developer Program policies</a>
* This flag enables PayPal to collect necessary information required for Fraud Detection and Risk Management.
*
* @see <a href="https://support.google.com/googleplay/android-developer/answer/10144311#personal-sensitive">User Data policies for the Google Play Developer Program </a>
* @see <a href="https://support.google.com/googleplay/android-developer/answer/9799150?hl=en#Prominent%20in-app%20disclosure">Examples of prominent in-app disclosures</a>
*/
public PayPalNativeCheckoutVaultRequest(boolean hasUserLocationConsent) {
super(hasUserLocationConsent);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,22 @@ public abstract class PayPalNativeRequest implements Parcelable {
private final ArrayList<PayPalNativeCheckoutLineItem> lineItems;
private String returnUrl;
private String userAuthenticationEmail;
private final boolean hasUserLocationConsent;

/**
* Deprecated. Use {@link PayPalNativeRequest#PayPalNativeRequest(boolean)} instead.
*
* Constructs a request for PayPal Checkout and Vault flows.
*/
@Deprecated
public PayPalNativeRequest() {
this(false);
}

PayPalNativeRequest(boolean hasUserLocationConsent) {
shippingAddressRequired = false;
lineItems = new ArrayList<>();
this.hasUserLocationConsent = hasUserLocationConsent;
}

/**
Expand Down Expand Up @@ -256,6 +265,9 @@ public ArrayList<PayPalNativeCheckoutLineItem> getLineItems() {
return lineItems;
}

public boolean hasUserLocationConsent() {
return hasUserLocationConsent;
}

abstract String createRequestBody(Configuration configuration, Authorization authorization, String successUrl, String cancelUrl) throws JSONException;

Expand All @@ -272,6 +284,7 @@ protected PayPalNativeRequest(Parcel in) {
lineItems = in.createTypedArrayList(PayPalNativeCheckoutLineItem.CREATOR);
returnUrl = in.readString();
userAuthenticationEmail = in.readString();
hasUserLocationConsent = in.readByte() != 0;
}

@Override
Expand All @@ -293,5 +306,6 @@ public void writeToParcel(Parcel parcel, int i) {
parcel.writeTypedList(lineItems);
parcel.writeString(returnUrl);
parcel.writeString(userAuthenticationEmail);
parcel.writeByte((byte) (hasUserLocationConsent ? 1 : 0));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class PayPalCheckoutRequestUnitTest {

@Test
public void newPayPalCheckoutRequest_setsDefaultValues() {
PayPalNativeCheckoutRequest request = new PayPalNativeCheckoutRequest("1.00");
PayPalNativeCheckoutRequest request = new PayPalNativeCheckoutRequest("1.00", false);

assertNotNull(request.getAmount());
assertNull(request.getCurrencyCode());
Expand All @@ -29,11 +29,18 @@ public void newPayPalCheckoutRequest_setsDefaultValues() {
assertEquals(PayPalNativeCheckoutPaymentIntent.AUTHORIZE, request.getIntent());
assertNull(request.getBillingAgreementDescription());
assertFalse(request.getShouldOfferPayLater());
assertFalse(request.hasUserLocationConsent());
}

@Test
public void setsValuesCorrectly() {
public void newPayPalCheckoutRequest_without_hasUserLocationConsent_defaults_to_false() {
PayPalNativeCheckoutRequest request = new PayPalNativeCheckoutRequest("1.00");
assertFalse(request.hasUserLocationConsent());
}

@Test
public void setsValuesCorrectly() {
PayPalNativeCheckoutRequest request = new PayPalNativeCheckoutRequest("1.00", true);
request.setCurrencyCode("USD");
request.setShouldOfferPayLater(true);
request.setIntent(PayPalNativeCheckoutPaymentIntent.SALE);
Expand Down Expand Up @@ -74,11 +81,12 @@ public void setsValuesCorrectly() {
assertEquals("CA", request.getShippingAddressOverride().getRegion());
assertEquals("US", request.getShippingAddressOverride().getCountryCodeAlpha2());
assertTrue(request.getShouldOfferPayLater());
assertTrue(request.hasUserLocationConsent());
}

@Test
public void parcelsCorrectly() {
PayPalNativeCheckoutRequest request = new PayPalNativeCheckoutRequest("12.34");
PayPalNativeCheckoutRequest request = new PayPalNativeCheckoutRequest("12.34", true);
request.setCurrencyCode("USD");
request.setLocaleCode("en-US");
request.setBillingAgreementDescription("Billing Agreement Description");
Expand Down Expand Up @@ -117,5 +125,6 @@ public void parcelsCorrectly() {
assertEquals("merchant_account_id", result.getMerchantAccountId());
assertEquals(1, result.getLineItems().size());
assertEquals("An Item", result.getLineItems().get(0).getName());
assertTrue(result.hasUserLocationConsent());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,17 @@ import com.paypal.checkout.error.ErrorInfo
import com.paypal.checkout.error.OnError
import com.paypal.checkout.shipping.OnShippingChange
import com.paypal.pyplcheckout.instrumentation.constants.PEnums
import com.paypal.pyplcheckout.instrumentation.constants.PEnums.EventCode
import com.paypal.pyplcheckout.instrumentation.constants.PEnums.Outcome
import com.paypal.pyplcheckout.instrumentation.constants.PEnums.StateName
import com.paypal.pyplcheckout.instrumentation.constants.PEnums.TransitionName
import com.paypal.pyplcheckout.instrumentation.di.PLog
import com.paypal.pyplcheckout.instrumentation.di.PLog.transition
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.slot
import io.mockk.verify
import junit.framework.TestCase.assertEquals
Expand All @@ -45,6 +52,13 @@ class PayPalNativeCheckoutClientUnitTest {
fun beforeEach() {
mockkStatic(PayPalCheckout::class)
mockkStatic(PLog::class)
mockkStatic(PayPalCheckout::class)

every { PayPalCheckout.setConfig(any()) } just runs
every { PayPalCheckout.startCheckout(any(), any()) } just runs
every {
PLog.transition(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())
} just runs

activity = mockk(relaxed = true)
listener = mockk(relaxed = true)
Expand All @@ -58,7 +72,7 @@ class PayPalNativeCheckoutClientUnitTest {

@Test
fun tokenizePayPalAccount_throwsWhenPayPalRequestIsBaseClass() {
val baseRequest: PayPalNativeRequest = object : PayPalNativeRequest() {
val baseRequest: PayPalNativeRequest = object : PayPalNativeRequest(true) {
@Throws(JSONException::class)
public override fun createRequestBody(
configuration: Configuration,
Expand Down Expand Up @@ -86,7 +100,7 @@ class PayPalNativeCheckoutClientUnitTest {
@Test
@Ignore("Refactor test to work with mockk")
fun requestBillingAgreement_launchNativeCheckout_sendsAnalyticsEvents() {
val payPalVaultRequest = PayPalNativeCheckoutVaultRequest()
val payPalVaultRequest = PayPalNativeCheckoutVaultRequest(true)
payPalVaultRequest.merchantAccountId = "sample-merchant-account-id"
payPalVaultRequest.returnUrl = "returnUrl://paypalpay"
val payPalResponse = PayPalNativeCheckoutResponse(payPalVaultRequest)
Expand Down Expand Up @@ -125,7 +139,7 @@ class PayPalNativeCheckoutClientUnitTest {

@Test
fun requestNativeCheckout_returnsErrorFromFailedResponse() {
val payPalVaultRequest = PayPalNativeCheckoutVaultRequest()
val payPalVaultRequest = PayPalNativeCheckoutVaultRequest(true)
payPalVaultRequest.merchantAccountId = "sample-merchant-account-id"
payPalVaultRequest.returnUrl = "returnUrl://paypalpay"
val payPalInternalClient = MockkPayPalInternalClientBuilder()
Expand All @@ -144,7 +158,7 @@ class PayPalNativeCheckoutClientUnitTest {
@Ignore("Refactor test to work with mockk")
@Suppress("LongMethod")
fun requestOneTimePayment_launchNativeCheckout_sendsAnalyticsEvents() {
val payPalCheckoutRequest = PayPalNativeCheckoutRequest("1.00")
val payPalCheckoutRequest = PayPalNativeCheckoutRequest("1.00", true)
payPalCheckoutRequest.intent = "authorize"
payPalCheckoutRequest.merchantAccountId = "sample-merchant-account-id"
payPalCheckoutRequest.returnUrl = "returnUrl://paypalpay"
Expand Down Expand Up @@ -235,7 +249,7 @@ class PayPalNativeCheckoutClientUnitTest {
fun paypalAccount_isSetupCorrectly() {
val riskCorrelationId = "riskId"
val sampleMerchantId = "sample-merchant-account-id"
val payPalCheckoutRequest = PayPalNativeCheckoutRequest("1.00")
val payPalCheckoutRequest = PayPalNativeCheckoutRequest("1.00", true)
payPalCheckoutRequest.intent = "authorize"
payPalCheckoutRequest.merchantAccountId = sampleMerchantId
payPalCheckoutRequest.returnUrl = "returnUrl://paypalpay"
Expand All @@ -259,7 +273,7 @@ class PayPalNativeCheckoutClientUnitTest {
@Throws(Exception::class)
@Ignore("Refactor test to work with mockk")
fun requestOneTimePayment_sendsBrowserSwitchStartAnalyticsEvent() {
val payPalCheckoutRequest = PayPalNativeCheckoutRequest("1.00")
val payPalCheckoutRequest = PayPalNativeCheckoutRequest("1.00", true)
payPalCheckoutRequest.intent = "authorize"
payPalCheckoutRequest.merchantAccountId = "sample-merchant-account-id"
payPalCheckoutRequest.returnUrl = "returnUrl://paypalpay"
Expand Down Expand Up @@ -300,7 +314,7 @@ class PayPalNativeCheckoutClientUnitTest {
val braintreeClient = MockkBraintreeClientBuilder().build()
val payPalInternalClient = MockkPayPalInternalClientBuilder().build()
val sut = PayPalNativeCheckoutClient(braintreeClient, payPalInternalClient)
val request = PayPalNativeCheckoutRequest("1.00")
val request = PayPalNativeCheckoutRequest("1.00", true)
request.shouldOfferPayLater = true
sut.tokenizePayPalAccount(activity, request)
verify { braintreeClient.sendAnalyticsEvent("paypal-native.single-payment.paylater.offered") }
Expand All @@ -313,7 +327,7 @@ class PayPalNativeCheckoutClientUnitTest {
val braintreeClient = MockkBraintreeClientBuilder()
.configurationSuccess(payPalEnabledConfig)
.build()
val payPalRequest = PayPalNativeCheckoutVaultRequest()
val payPalRequest = PayPalNativeCheckoutVaultRequest(true)
val sut = PayPalNativeCheckoutClient(braintreeClient, payPalInternalClient)
sut.tokenizePayPalAccount(activity, payPalRequest)
verify { payPalInternalClient.sendRequest(activity, payPalRequest, any()) }
Expand All @@ -326,7 +340,7 @@ class PayPalNativeCheckoutClientUnitTest {
val braintreeClient = MockkBraintreeClientBuilder()
.configurationSuccess(payPalEnabledConfig)
.build()
val payPalRequest = PayPalNativeCheckoutRequest("1.00")
val payPalRequest = PayPalNativeCheckoutRequest("1.00", true)
val sut = PayPalNativeCheckoutClient(braintreeClient, payPalInternalClient)
sut.tokenizePayPalAccount(activity, payPalRequest)
verify { payPalInternalClient.sendRequest(activity, payPalRequest, any()) }
Expand All @@ -337,7 +351,7 @@ class PayPalNativeCheckoutClientUnitTest {
fun tokenizePayPalAccount_sendsPayPalCreditOfferedAnalyticsEvent() {
val payPalInternalClient = MockkPayPalInternalClientBuilder().build()
val braintreeClient = MockkBraintreeClientBuilder().build()
val payPalRequest = PayPalNativeCheckoutVaultRequest()
val payPalRequest = PayPalNativeCheckoutVaultRequest(true)
payPalRequest.shouldOfferCredit = true
val sut = PayPalNativeCheckoutClient(braintreeClient, payPalInternalClient)
sut.tokenizePayPalAccount(activity, payPalRequest)
Expand All @@ -346,7 +360,7 @@ class PayPalNativeCheckoutClientUnitTest {

@Test
fun launchNativeCheckout_notifiesErrorWhenPayPalRequestIsBaseClass_sendsAnalyticsEvents() {
val baseRequest: PayPalNativeRequest = object : PayPalNativeRequest() {
val baseRequest: PayPalNativeRequest = object : PayPalNativeRequest(true) {
@Throws(JSONException::class)
public override fun createRequestBody(
configuration: Configuration,
Expand All @@ -369,4 +383,24 @@ class PayPalNativeCheckoutClientUnitTest {
verify { braintreeClient.sendAnalyticsEvent("paypal-native.tokenize.started") }
verify { braintreeClient.sendAnalyticsEvent("paypal-native.tokenize.invalid-request.failed") }
}

@Test
fun `when launchNativeCheckout is called, hasUserLocationConsent is sent to PayPalCheckout startCheckout`() {
val request = PayPalNativeCheckoutVaultRequest(true)
request.returnUrl = "returnUrl://paypalpay"

val braintreeClient = MockkBraintreeClientBuilder()
.configurationSuccess(payPalEnabledConfig)
.build()
val payPalResponse = PayPalNativeCheckoutResponse(request)
.clientMetadataId("sample-client-metadata-id")
val payPalInternalClient = MockkPayPalInternalClientBuilder()
.sendRequestSuccess(payPalResponse)
.build()
val sut = PayPalNativeCheckoutClient(braintreeClient, payPalInternalClient)

sut.launchNativeCheckout(activity, request)

verify { PayPalCheckout.startCheckout(any(), true) }
}
}
Loading

0 comments on commit 14e072f

Please sign in to comment.