Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User Location Consent Feature Branch into Main #473

Merged
merged 3 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading