Skip to content

Commit

Permalink
Merge pull request #473 from braintree/user-location-consent-feature
Browse files Browse the repository at this point in the history
User Location Consent Feature Branch into Main
  • Loading branch information
tdchow committed Apr 18, 2024
2 parents 247cede + 8fc5ee9 commit 2fe23e3
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 19 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <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 DropInRequest(boolean hasUserLocationConsent) {
this.hasUserLocationConsent = hasUserLocationConsent;
}

/**
* This method is optional.
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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<DropInRequest> CREATOR = new Creator<DropInRequest>() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,18 @@ public DataCollector build() {
DataCollector dataCollector = mock(DataCollector.class);

doAnswer((Answer<Void>) 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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -253,14 +254,14 @@ DropInInternalClient build() {
}).when(dropInClient).getSupportedCardTypes(any(GetSupportedCardTypesCallback.class));

doAnswer((Answer<Void>) 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<Void>) invocation -> {
DeletePaymentMethodNonceCallback callback = (DeletePaymentMethodNonceCallback) invocation.getArguments()[2];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class DropInActivityUnitTest {
@Before
fun beforeEach() {
authorization = Authorization.fromString(Fixtures.TOKENIZATION_KEY)
dropInRequest = DropInRequest()
dropInRequest = DropInRequest(true)
}

@After
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<PayPalRequest> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -120,6 +121,7 @@ public void includesAllOptions() {
assertTrue(dropInRequest.getVaultCardDefaultValue());
assertTrue(dropInRequest.getAllowVaultCardOverride());
assertEquals(CardForm.FIELD_OPTIONAL, dropInRequest.getCardholderNameStatus());
assertTrue(dropInRequest.hasUserLocationConsent());
}

@Test
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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());
}
}
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ buildscript {
}
}

ext.brainTreeVersion = "4.41.0"
ext.brainTreeVersion = "4.45.0"

ext.deps = [
"braintreeCore" : "com.braintreepayments.api:braintree-core:$brainTreeVersion",
Expand Down

0 comments on commit 2fe23e3

Please sign in to comment.