diff --git a/CHANGELOG.md b/CHANGELOG.md index a369268f..79c1bd27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Braintree Android Drop-In Release Notes +## unreleased + +* Bump braintree_android module dependency versions to `4.45.0` +* Fixes Google Play Store Rejection + * Add `hasUserLocationConsent` property to `DropInRequest` + * Deprecate existing constructor that does not pass in `hasUserLocationConsent` + ## 6.15.0 * Refresh vaulted payment methods list after 3DS is canceled (fixes #455) diff --git a/Drop-In/src/main/java/com/braintreepayments/api/DropInActivity.java b/Drop-In/src/main/java/com/braintreepayments/api/DropInActivity.java index c42e7115..ff534610 100644 --- a/Drop-In/src/main/java/com/braintreepayments/api/DropInActivity.java +++ b/Drop-In/src/main/java/com/braintreepayments/api/DropInActivity.java @@ -487,7 +487,7 @@ private void onVaultedPaymentMethodSelected(DropInEvent event) { } else { final DropInResult dropInResult = new DropInResult(); dropInResult.setPaymentMethodNonce(paymentMethodNonce); - dropInInternalClient.collectDeviceData(DropInActivity.this, (deviceData, error) -> { + dropInInternalClient.collectDeviceData(DropInActivity.this, dropInRequest.hasUserLocationConsent(), (deviceData, error) -> { if (deviceData != null) { dropInResult.setDeviceData(deviceData); animateBottomSheetClosedAndFinishDropInWithResult(dropInResult); @@ -531,7 +531,7 @@ private void onPaymentMethodNonceCreated(final PaymentMethodNonce paymentMethod) } else { DropInResult dropInResult = new DropInResult(); dropInResult.setPaymentMethodNonce(paymentMethod); - dropInInternalClient.collectDeviceData(this, (deviceData, deviceDataError) -> { + dropInInternalClient.collectDeviceData(this, dropInRequest.hasUserLocationConsent(), (deviceData, deviceDataError) -> { if (deviceData != null) { dropInResult.setDeviceData(deviceData); animateBottomSheetClosedAndFinishDropInWithResult(dropInResult); diff --git a/Drop-In/src/main/java/com/braintreepayments/api/DropInInternalClient.java b/Drop-In/src/main/java/com/braintreepayments/api/DropInInternalClient.java index 0560ea8c..e6a79e83 100644 --- a/Drop-In/src/main/java/com/braintreepayments/api/DropInInternalClient.java +++ b/Drop-In/src/main/java/com/braintreepayments/api/DropInInternalClient.java @@ -90,8 +90,13 @@ void sendAnalyticsEvent(String eventName) { braintreeClient.sendAnalyticsEvent(eventName); } - void collectDeviceData(FragmentActivity activity, DataCollectorCallback callback) { - dataCollector.collectDeviceData(activity, callback); + void collectDeviceData( + FragmentActivity activity, + boolean hasUserLocationConsent, + DataCollectorCallback callback + ) { + DataCollectorRequest request = new DataCollectorRequest(hasUserLocationConsent); + dataCollector.collectDeviceData(activity, request, callback); } void performThreeDSecureVerification(final FragmentActivity activity, PaymentMethodNonce paymentMethodNonce, final DropInResultCallback callback) { @@ -106,7 +111,7 @@ void performThreeDSecureVerification(final FragmentActivity activity, PaymentMet } else if (threeDSecureResult != null) { final DropInResult dropInResult = new DropInResult(); dropInResult.setPaymentMethodNonce(threeDSecureResult.getTokenizedCard()); - dataCollector.collectDeviceData(activity, (deviceData, dataCollectionError) -> { + collectDeviceData(activity, dropInRequest.hasUserLocationConsent(), (deviceData, dataCollectionError) -> { if (deviceData != null) { dropInResult.setDeviceData(deviceData); callback.onResult(dropInResult, null); @@ -143,7 +148,7 @@ void shouldRequestThreeDSecureVerification(PaymentMethodNonce paymentMethodNonce void tokenizePayPalRequest(FragmentActivity activity, PayPalFlowStartedCallback callback) { PayPalRequest paypalRequest = dropInRequest.getPayPalRequest(); if (paypalRequest == null) { - paypalRequest = new PayPalVaultRequest(); + paypalRequest = new PayPalVaultRequest(dropInRequest.hasUserLocationConsent()); } payPalClient.tokenizePayPalAccount(activity, paypalRequest, callback); } @@ -248,7 +253,7 @@ private void notifyDropInResult(FragmentActivity activity, PaymentMethodNonce pa final DropInResult dropInResult = new DropInResult(); dropInResult.setPaymentMethodNonce(paymentMethodNonce); - dataCollector.collectDeviceData(activity, (deviceData, dataCollectionError) -> { + collectDeviceData(activity, dropInRequest.hasUserLocationConsent(), (deviceData, dataCollectionError) -> { if (dataCollectionError != null) { callback.onResult(null, dataCollectionError); return; diff --git a/Drop-In/src/main/java/com/braintreepayments/api/DropInRequest.java b/Drop-In/src/main/java/com/braintreepayments/api/DropInRequest.java index 93f7e0a6..69bd6467 100644 --- a/Drop-In/src/main/java/com/braintreepayments/api/DropInRequest.java +++ b/Drop-In/src/main/java/com/braintreepayments/api/DropInRequest.java @@ -29,10 +29,30 @@ public class DropInRequest implements Parcelable { private boolean allowVaultCardOverride = false; private String customUrlScheme = null; + private final boolean hasUserLocationConsent; private int cardholderNameStatus = CardForm.FIELD_DISABLED; - public DropInRequest() {} + /** + * Deprecated. Use {@link DropInRequest#DropInRequest(boolean)} instead. + **/ + @Deprecated + public DropInRequest() { + hasUserLocationConsent = false; + } + + /** + * @param hasUserLocationConsent informs the SDK if your application has obtained consent from + * the user to collect location data in compliance with + * Google Play Developer Program policies + * This flag enables PayPal to collect necessary information required for Fraud Detection and Risk Management. + * + * @see User Data policies for the Google Play Developer Program + * @see Examples of prominent in-app disclosures + */ + public DropInRequest(boolean hasUserLocationConsent) { + this.hasUserLocationConsent = hasUserLocationConsent; + } /** * This method is optional. @@ -311,6 +331,13 @@ public String getCustomUrlScheme() { return customUrlScheme; } + /** + * @return If the user has consented to sharing location data. + */ + boolean hasUserLocationConsent() { + return hasUserLocationConsent; + } + @Override public int describeContents() { return 0; @@ -334,6 +361,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeByte(vaultCardDefaultValue ? (byte) 1 : (byte) 0); dest.writeByte(allowVaultCardOverride ? (byte) 1 : (byte) 0); dest.writeString(customUrlScheme); + dest.writeByte(hasUserLocationConsent ? (byte) 1 : (byte) 0); } protected DropInRequest(Parcel in) { @@ -353,6 +381,7 @@ protected DropInRequest(Parcel in) { vaultCardDefaultValue = in.readByte() != 0; allowVaultCardOverride = in.readByte() != 0; customUrlScheme = in.readString(); + hasUserLocationConsent = in.readByte() != 0; } public static final Creator CREATOR = new Creator() { diff --git a/Drop-In/src/sharedTest/java/com/braintreepayments/api/MockDataCollectorBuilder.java b/Drop-In/src/sharedTest/java/com/braintreepayments/api/MockDataCollectorBuilder.java index b18fb89f..88eef077 100644 --- a/Drop-In/src/sharedTest/java/com/braintreepayments/api/MockDataCollectorBuilder.java +++ b/Drop-In/src/sharedTest/java/com/braintreepayments/api/MockDataCollectorBuilder.java @@ -27,14 +27,18 @@ public DataCollector build() { DataCollector dataCollector = mock(DataCollector.class); doAnswer((Answer) invocation -> { - DataCollectorCallback callback = (DataCollectorCallback) invocation.getArguments()[1]; + DataCollectorCallback callback = (DataCollectorCallback) invocation.getArguments()[2]; if (collectDeviceDataSuccess != null) { callback.onResult(collectDeviceDataSuccess, null); } else if (collectDeviceDataError != null) { callback.onResult(null, collectDeviceDataError); } return null; - }).when(dataCollector).collectDeviceData(any(Context.class), any(DataCollectorCallback.class)); + }).when(dataCollector).collectDeviceData( + any(Context.class), + any(DataCollectorRequest.class), + any(DataCollectorCallback.class) + ); return dataCollector; } diff --git a/Drop-In/src/sharedTest/java/com/braintreepayments/api/MockDropInInternalClientBuilder.java b/Drop-In/src/sharedTest/java/com/braintreepayments/api/MockDropInInternalClientBuilder.java index 3042faf6..e9d02f7a 100644 --- a/Drop-In/src/sharedTest/java/com/braintreepayments/api/MockDropInInternalClientBuilder.java +++ b/Drop-In/src/sharedTest/java/com/braintreepayments/api/MockDropInInternalClientBuilder.java @@ -1,6 +1,7 @@ package com.braintreepayments.api; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; @@ -253,14 +254,14 @@ DropInInternalClient build() { }).when(dropInClient).getSupportedCardTypes(any(GetSupportedCardTypesCallback.class)); doAnswer((Answer) invocation -> { - DataCollectorCallback callback = (DataCollectorCallback) invocation.getArguments()[1]; + DataCollectorCallback callback = (DataCollectorCallback) invocation.getArguments()[2]; if (deviceDataSuccess != null) { callback.onResult(deviceDataSuccess, null); } else if (deviceDataError != null) { callback.onResult(null, deviceDataError); } return null; - }).when(dropInClient).collectDeviceData(any(FragmentActivity.class), any(DataCollectorCallback.class)); + }).when(dropInClient).collectDeviceData(any(FragmentActivity.class), anyBoolean(), any(DataCollectorCallback.class)); doAnswer((Answer) invocation -> { DeletePaymentMethodNonceCallback callback = (DeletePaymentMethodNonceCallback) invocation.getArguments()[2]; diff --git a/Drop-In/src/test/java/com/braintreepayments/api/DropInActivityUnitTest.kt b/Drop-In/src/test/java/com/braintreepayments/api/DropInActivityUnitTest.kt index 9d22afb1..f5ac588a 100644 --- a/Drop-In/src/test/java/com/braintreepayments/api/DropInActivityUnitTest.kt +++ b/Drop-In/src/test/java/com/braintreepayments/api/DropInActivityUnitTest.kt @@ -35,7 +35,7 @@ class DropInActivityUnitTest { @Before fun beforeEach() { authorization = Authorization.fromString(Fixtures.TOKENIZATION_KEY) - dropInRequest = DropInRequest() + dropInRequest = DropInRequest(true) } @After @@ -425,6 +425,39 @@ class DropInActivityUnitTest { ) } + @Test + fun threeDS_collectDeviceData_passes_in_hasUserLocationConsent() { + val dropInClient = MockDropInInternalClientBuilder() + .authorizationSuccess(authorization) + .shouldPerformThreeDSecureVerification(false) + .build() + setupDropInActivity(dropInClient, dropInRequest) + + val cardNonce = CardNonce.fromJSON(JSONObject(Fixtures.VISA_CREDIT_CARD_RESPONSE)) + activity.supportFragmentManager.setFragmentResult( + DropInEvent.REQUEST_KEY, + DropInEvent.createVaultedPaymentMethodSelectedEvent(cardNonce).toBundle() + ) + + verify(dropInClient).collectDeviceData(same(activity), eq(true), any()) + } + + @Test + fun card_collectDeviceData_passes_in_hasUserLocationConsent() { + val cardNonce = CardNonce.fromJSON(JSONObject(Fixtures.VISA_CREDIT_CARD_RESPONSE)) + val dropInClient = MockDropInInternalClientBuilder() + .authorizationSuccess(authorization) + .shouldPerformThreeDSecureVerification(false) + .cardTokenizeSuccess(cardNonce) + .build() + setupDropInActivity(dropInClient, dropInRequest) + + val event = DropInEvent.createCardDetailsSubmitEvent(Card()) + activity.supportFragmentManager.setFragmentResult(DropInEvent.REQUEST_KEY, event.toBundle()) + + verify(dropInClient).collectDeviceData(same(activity), eq(true), any()) + } + @Test fun onVaultedPaymentMethodSelectedEvent_returnsDeviceData() { val dropInClient = MockDropInInternalClientBuilder() diff --git a/Drop-In/src/test/java/com/braintreepayments/api/DropInInternalClientUnitTest.java b/Drop-In/src/test/java/com/braintreepayments/api/DropInInternalClientUnitTest.java index c9d6233e..7cec8598 100644 --- a/Drop-In/src/test/java/com/braintreepayments/api/DropInInternalClientUnitTest.java +++ b/Drop-In/src/test/java/com/braintreepayments/api/DropInInternalClientUnitTest.java @@ -127,9 +127,10 @@ public void collectDeviceData_forwardsInvocationToDataCollector() { DataCollectorCallback callback = mock(DataCollectorCallback.class); DropInInternalClient sut = new DropInInternalClient(params); - sut.collectDeviceData(activity, callback); + sut.collectDeviceData(activity, true, callback); - verify(dataCollector).collectDeviceData(activity, callback); + DataCollectorRequest request = new DataCollectorRequest(true); + verify(dataCollector).collectDeviceData(activity, request, callback); } @Test @@ -858,6 +859,26 @@ public void tokenizePayPalAccount_withPayPalVaultRequest_tokenizesPayPalWithVaul verify(payPalClient).tokenizePayPalAccount(same(activity), same(payPalRequest), same(callback)); } + @Test + public void tokenizePayPalRequest_when_dropInRequest_is_null_hasUserLocationConsent_is_set_from_dropInRequest() { + BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); + DropInRequest dropInRequest = new DropInRequest(true); + PayPalClient payPalClient = mock(PayPalClient.class); + DropInInternalClientParams params = new DropInInternalClientParams() + .dropInRequest(dropInRequest) + .payPalClient(payPalClient) + .braintreeClient(braintreeClient); + + PayPalFlowStartedCallback callback = mock(PayPalFlowStartedCallback.class); + DropInInternalClient sut = new DropInInternalClient(params); + + sut.tokenizePayPalRequest(activity, callback); + + ArgumentCaptor captor = ArgumentCaptor.forClass(PayPalRequest.class); + verify(payPalClient).tokenizePayPalAccount(same(activity), captor.capture(), same(callback)); + assertTrue(captor.getValue().hasUserLocationConsent()); + } + @Test public void tokenizeVenmoAccount_tokenizesVenmo() { Configuration configuration = mockConfiguration(false, true, false, false, false); diff --git a/Drop-In/src/test/java/com/braintreepayments/api/DropInRequestUnitTest.java b/Drop-In/src/test/java/com/braintreepayments/api/DropInRequestUnitTest.java index 3d31baf5..56ae3869 100644 --- a/Drop-In/src/test/java/com/braintreepayments/api/DropInRequestUnitTest.java +++ b/Drop-In/src/test/java/com/braintreepayments/api/DropInRequestUnitTest.java @@ -12,6 +12,7 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; +import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertNotNull; @RunWith(RobolectricTestRunner.class) @@ -58,7 +59,7 @@ public void includesAllOptions() { additionalInformation.setShippingMethodIndicator("GEN"); threeDSecureRequest.setAdditionalInformation(additionalInformation); - DropInRequest dropInRequest = new DropInRequest(); + DropInRequest dropInRequest = new DropInRequest(true); dropInRequest.setGooglePayRequest(googlePayRequest); dropInRequest.setGooglePayDisabled(true); dropInRequest.setPayPalRequest(paypalRequest); @@ -120,6 +121,7 @@ public void includesAllOptions() { assertTrue(dropInRequest.getVaultCardDefaultValue()); assertTrue(dropInRequest.getAllowVaultCardOverride()); assertEquals(CardForm.FIELD_OPTIONAL, dropInRequest.getCardholderNameStatus()); + assertTrue(dropInRequest.hasUserLocationConsent()); } @Test @@ -164,7 +166,7 @@ public void isParcelable() { additionalInformation.setShippingMethodIndicator("GEN"); threeDSecureRequest.setAdditionalInformation(additionalInformation); - DropInRequest dropInRequest = new DropInRequest(); + DropInRequest dropInRequest = new DropInRequest(true); dropInRequest.setGooglePayRequest(googlePayRequest); dropInRequest.setGooglePayDisabled(true); dropInRequest.setPayPalRequest(paypalRequest); @@ -229,13 +231,20 @@ public void isParcelable() { assertTrue(parceledDropInRequest.getVaultCardDefaultValue()); assertTrue(parceledDropInRequest.getAllowVaultCardOverride()); assertEquals(CardForm.FIELD_OPTIONAL, parceledDropInRequest.getCardholderNameStatus()); + assertTrue(parceledDropInRequest.hasUserLocationConsent()); } @Test public void getCardholderNameStatus_includesCardHolderNameStatus() { - DropInRequest dropInRequest = new DropInRequest(); + DropInRequest dropInRequest = new DropInRequest(true); dropInRequest.setCardholderNameStatus(CardForm.FIELD_REQUIRED); assertEquals(CardForm.FIELD_REQUIRED, dropInRequest.getCardholderNameStatus()); } + + @Test + public void no_argument_constructor_defaults_hasUserLocationConsent_to_false() { + DropInRequest request = new DropInRequest(); + assertFalse(request.hasUserLocationConsent()); + } } diff --git a/build.gradle b/build.gradle index c06dab0d..5c3897df 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { } } - ext.brainTreeVersion = "4.41.0" + ext.brainTreeVersion = "4.45.0" ext.deps = [ "braintreeCore" : "com.braintreepayments.api:braintree-core:$brainTreeVersion",