Skip to content

Commit

Permalink
Merge pull request #962 from braintree/user-location-consent-feature
Browse files Browse the repository at this point in the history
Merge User Location Consent Branch into Main
  • Loading branch information
tdchow authored Apr 4, 2024
2 parents 7883ad3 + bc47a7d commit f55ad9c
Show file tree
Hide file tree
Showing 31 changed files with 536 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,38 +31,44 @@ public DataCollector(@NonNull BraintreeClient braintreeClient) {
}

/**
* @deprecated Passing in {@link DataCollectorRequest} is required. Use
* {@link DataCollector#collectDeviceData(Context, DataCollectorRequest, DataCollectorCallback)} instead.
*
* Collect device information for fraud identification purposes.
*
* @param context Android Context
* @param callback {@link DataCollectorCallback}
*/
public void collectDeviceData(@NonNull Context context, @NonNull DataCollectorCallback callback) {
collectDeviceData(context, null, callback);
@Deprecated
public void collectDeviceData(
@NonNull Context context,
@NonNull DataCollectorCallback callback
) {
DataCollectorRequest request = new DataCollectorRequest(false);
collectDeviceData(context, request, callback);
}

/**
* Collects device data based on your merchant configuration.
* <p>
* We recommend that you call this method as early as possible, e.g. at app launch. If that's too early,
* call it at the beginning of customer checkout.
* <p>
* Use the return value on your server, e.g. with `Transaction.sale`.
* Collect device information for fraud identification purposes.
*
* @param context Android Context
* @param merchantId Optional - Custom Kount merchant id. Leave blank to use the default.
* @param callback {@link DataCollectorCallback}
* @deprecated Kount is officially deprecated, use {@link PayPalDataCollector#collectDeviceData(Context, String, PayPalDataCollectorCallback)} instead.
* @param context Android Context
* @param dataCollectorRequest The {@link DataCollectorRequest} containing the configuration for
* the data collection request
* @param callback {@link DataCollectorCallback}
*/
@Deprecated
public void collectDeviceData(@NonNull final Context context, @Nullable final String merchantId, @NonNull final DataCollectorCallback callback) {
public void collectDeviceData(
@NonNull Context context,
@NonNull DataCollectorRequest dataCollectorRequest,
@NonNull DataCollectorCallback callback
) {
final Context appContext = context.getApplicationContext();
braintreeClient.getConfiguration(new ConfigurationCallback() {
@Override
public void onResult(@Nullable Configuration configuration, @Nullable Exception error) {
if (configuration != null) {
final JSONObject deviceData = new JSONObject();
try {
String clientMetadataId = getPayPalClientMetadataId(appContext, configuration);
String clientMetadataId = getPayPalClientMetadataId(appContext, configuration, dataCollectorRequest);
if (!TextUtils.isEmpty(clientMetadataId)) {
deviceData.put(CORRELATION_ID_KEY, clientMetadataId);
}
Expand All @@ -76,16 +82,44 @@ public void onResult(@Nullable Configuration configuration, @Nullable Exception
});
}

/**
* Collects device data based on your merchant configuration.
* <p>
* We recommend that you call this method as early as possible, e.g. at app launch. If that's too early,
* call it at the beginning of customer checkout.
* <p>
* Use the return value on your server, e.g. with `Transaction.sale`.
*
* @param context Android Context
* @param merchantId Optional - Custom Kount merchant id. Leave blank to use the default.
* @param callback {@link DataCollectorCallback}
* @deprecated Kount is officially deprecated, use {@link PayPalDataCollector#collectDeviceData(Context, String, PayPalDataCollectorCallback)} instead.
*/
@Deprecated
public void collectDeviceData(
@NonNull final Context context,
@Nullable final String merchantId,
@NonNull final DataCollectorCallback callback
) {
collectDeviceData(context, callback);
}

/**
* Collect device information for fraud identification purposes from PayPal only.
*
* @param context Android Context
* @param configuration the merchant configuration
* @param dataCollectorRequest The {@link DataCollectorRequest} containing the configuration for
* the data collection request
* @return The client metadata id associated with the collected data.
*/
private String getPayPalClientMetadataId(Context context, Configuration configuration) {
private String getPayPalClientMetadataId(
Context context,
Configuration configuration,
DataCollectorRequest dataCollectorRequest
) {
try {
return payPalDataCollector.getClientMetadataId(context, configuration);
return payPalDataCollector.getClientMetadataId(context, configuration, dataCollectorRequest.getHasUserLocationConsent());
} catch (NoClassDefFoundError ignored) {
}
return "";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.braintreepayments.api

/**
* Parameters needed when the SDK collects data for fraud identification purposes
*
* @property 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
* [Google Play Developer Program policies](https://support.google.com/googleplay/android-developer/answer/10144311#personal-sensitive)
* 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](https://support.google.com/googleplay/android-developer/answer/10144311#personal-sensitive)
* @see [Examples of prominent in-app disclosures](https://support.google.com/googleplay/android-developer/answer/9799150?hl=en#Prominent%20in-app%20disclosure)
*/
data class DataCollectorRequest(
val hasUserLocationConsent: Boolean
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.braintreepayments.api;

import static junit.framework.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
Expand Down Expand Up @@ -57,7 +58,7 @@ public void collectDeviceData_forwardsConfigurationFetchErrors() {

@Test
public void collectDeviceData_getsDeviceDataJSONWithCorrelationIdFromPayPal() throws Exception {
when(payPalDataCollector.getClientMetadataId(context, configuration)).thenReturn("sample_correlation_id");
when(payPalDataCollector.getClientMetadataId(context, configuration, false)).thenReturn("sample_correlation_id");

BraintreeClient braintreeClient = new MockBraintreeClientBuilder()
.configuration(configuration)
Expand All @@ -76,4 +77,40 @@ public void collectDeviceData_getsDeviceDataJSONWithCorrelationIdFromPayPal() th
JSONObject json = new JSONObject(deviceData);
assertEquals("sample_correlation_id", json.getString("correlation_id"));
}

@Test
public void collectDeviceData_without_DataCollectorRequest_sets_hasUserLocationConsent_to_false() {
BraintreeClient braintreeClient = new MockBraintreeClientBuilder()
.configuration(configuration)
.build();

DataCollectorCallback callback = mock(DataCollectorCallback.class);
DataCollector sut = new DataCollector(braintreeClient, payPalDataCollector);

sut.collectDeviceData(context, callback);

ArgumentCaptor<String> deviceDataCaptor = ArgumentCaptor.forClass(String.class);
verify(callback).onResult(deviceDataCaptor.capture(), (Exception) isNull());

verify(payPalDataCollector).getClientMetadataId(context, configuration, false);
}

@Test
public void collectDeviceData_with_DataCollectorRequest_sets_correct_values_for_getClientMetadataId() {
BraintreeClient braintreeClient = new MockBraintreeClientBuilder()
.configuration(configuration)
.build();

DataCollectorCallback callback = mock(DataCollectorCallback.class);
DataCollector sut = new DataCollector(braintreeClient, payPalDataCollector);

DataCollectorRequest dataCollectorRequest = new DataCollectorRequest(true);

sut.collectDeviceData(context, dataCollectorRequest, callback);

ArgumentCaptor<String> deviceDataCaptor = ArgumentCaptor.forClass(String.class);
verify(callback).onResult(deviceDataCaptor.capture(), (Exception) isNull());

verify(payPalDataCollector).getClientMetadataId(context, configuration, true);
}
}
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

## unreleased

* Local Payment
* Fixes Google Play Store Rejection
* Add `hasUserLocationConsent` property to `LocalPaymentRequest`
* Deprecate existing constructor that does not pass in `hasUserLocationConsent`
* PayPal
* Fixes Google Play Store Rejection
* Add `hasUserLocationConsent` property to `PayPalCheckoutRequest`, `PayPalVaultRequest` and `PayPalRequest`
* Deprecate existing constructors that do not pass in `hasUserLocationConsent`
* BraintreeDataCollector
* Bump Magnes SDK to version 5.5.0
* Fixes Google Play Store Rejection
* Add `DataCollectorRequest` to pass in `hasUserLocationConsent`
* Update `DataCollector.collectDeviceData()` to take in `DataCollectorRequest`
* Deprecate existing `DataCollector.collectDeviceData()`
* PayPalDataCollector
* Fixes Google Play Store Rejection
* Add `PayPalDataCollectorRequest` to pass in `hasUserLocationConsent`
* Update `PayPalDataCollector.collectDeviceData()` to take in `PayPalDataCollectorRequest`
* Deprecate existing `PayPalDataCollector.collectDeviceData()`
* GooglePay
* Add `GooglePayClient#isReadyToPay(Context, ReadyForGooglePayRequest, GooglePayIsReadyToPayCallback)` method
* Deprecate `GooglePayClient#isReadyToPay(FragmentActivity, ReadyForGooglePayRequest, GooglePayIsReadyToPayCallback)` method
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public class LocalPaymentClient {
*/
private String payPalContextId = null;

private boolean hasUserLocationConsent;

@VisibleForTesting
BrowserSwitchResult pendingBrowserSwitchResult;

Expand Down Expand Up @@ -136,6 +138,7 @@ public void onResult(@Nullable LocalPaymentResult localPaymentResult, @Nullable
if (pairingId != null && !pairingId.isEmpty()) {
payPalContextId = pairingId;
}
hasUserLocationConsent = request.hasUserLocationConsent();
sendAnalyticsEvent(request.getPaymentType(), "local-payment.create.succeeded");
} else if (error != null) {
sendAnalyticsEvent(request.getPaymentType(), "local-payment.webswitch.initiate.failed");
Expand Down Expand Up @@ -323,17 +326,18 @@ public void onBrowserSwitchResult(@NonNull final Context context, @NonNull Brows
@Override
public void onResult(@Nullable Configuration configuration, @Nullable Exception error) {
if (configuration != null) {
localPaymentApi.tokenize(merchantAccountId, responseString, payPalDataCollector.getClientMetadataId(context, configuration), new LocalPaymentBrowserSwitchResultCallback() {
@Override
public void onResult(@Nullable LocalPaymentNonce localPaymentNonce, @Nullable Exception error) {
if (localPaymentNonce != null) {
sendAnalyticsEvent(paymentType, "local-payment.tokenize.succeeded");
} else if (error != null) {
sendAnalyticsEvent(paymentType, "local-payment.tokenize.failed");
localPaymentApi.tokenize(merchantAccountId, responseString, payPalDataCollector.getClientMetadataId(context, configuration, hasUserLocationConsent),
new LocalPaymentBrowserSwitchResultCallback() {
@Override
public void onResult(@Nullable LocalPaymentNonce localPaymentNonce, @Nullable Exception error) {
if (localPaymentNonce != null) {
sendAnalyticsEvent(paymentType, "local-payment.tokenize.succeeded");
} else if (error != null) {
sendAnalyticsEvent(paymentType, "local-payment.tokenize.failed");
}
callback.onResult(localPaymentNonce, error);
}
callback.onResult(localPaymentNonce, error);
}
});
});
} else if (error != null) {
callback.onResult(null, error);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,28 @@ public class LocalPaymentRequest {
private String phone;
private boolean shippingAddressRequired;
private String surname;
private final boolean hasUserLocationConsent;

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

/**
* @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 LocalPaymentRequest(boolean hasUserLocationConsent) {
this.hasUserLocationConsent = hasUserLocationConsent;
}

/**
* @param address Optional - The address of the customer. An error will occur if this address is not valid.
Expand Down Expand Up @@ -206,6 +228,10 @@ public String getSurname() {
return surname;
}

public boolean hasUserLocationConsent() {
return hasUserLocationConsent;
}

public String build(String returnUrl, String cancelUrl) {
try {
JSONObject payload = new JSONObject()
Expand Down
Loading

0 comments on commit f55ad9c

Please sign in to comment.