Skip to content

Commit

Permalink
[FraudProtection] User Location Data Consent (#248)
Browse files Browse the repository at this point in the history
* Bump Magnes version to 5.5.0.

* Add docstrings to PayPalDataCollectorRequest and associated methods.

* Update CHANGELOG.

* Implement PayPalDataCollector hasUserLocationConsent flag forwarding.

* Update CHANGELOG.

* Update FraudProtection/src/main/java/com/paypal/android/fraudprotection/PayPalDataCollectorRequest.kt

Co-authored-by: Sarah Koop <[email protected]>

* Fix lint errors.

---------

Co-authored-by: Sarah Koop <[email protected]>
  • Loading branch information
sshropshire and sarahkoop authored Apr 10, 2024
1 parent cf95ac7 commit edcb069
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 23 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
* Add `authChallenge` property to `CardVaultResult`
* Add `CardAuthChallenge` type
* FraudDetection
* Bump Magnes dependency to version 5.4.0
* Fixes Google Play Store Rejection
* Bump Magnes version to 5.5.0
* Create `PayPalDataCollectorRequest`
* Add `PayPalDataCollector#collectDeviceData(context, request)`
* Deprecate `PayPalDataCollector#collectDeviceData(context, clientMetadataId, additionalData)`
* PaymentButtons
* Update font typeface to "PayPalOpen" to meet brand guidelines

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.paypal.android.cardpayments.threedsecure.SCA
import com.paypal.android.corepayments.CoreConfig
import com.paypal.android.corepayments.PayPalSDKError
import com.paypal.android.fraudprotection.PayPalDataCollector
import com.paypal.android.fraudprotection.PayPalDataCollectorRequest
import com.paypal.android.models.OrderRequest
import com.paypal.android.models.TestCard
import com.paypal.android.uishared.enums.StoreInVaultOption
Expand Down Expand Up @@ -119,7 +120,8 @@ class ApproveOrderViewModel @Inject constructor(
} else {
viewModelScope.launch {
completeOrderState = ActionState.Loading
val cmid = payPalDataCollector.collectDeviceData(context)
val dataCollectorRequest = PayPalDataCollectorRequest(hasUserLocationConsent = false)
val cmid = payPalDataCollector.collectDeviceData(context, dataCollectorRequest)
completeOrderState =
completeOrderUseCase(orderId, intentOption, cmid).mapToActionState()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.paypal.android.api.services.SDKSampleServerResult
import com.paypal.android.corepayments.CoreConfig
import com.paypal.android.corepayments.PayPalSDKError
import com.paypal.android.fraudprotection.PayPalDataCollector
import com.paypal.android.fraudprotection.PayPalDataCollectorRequest
import com.paypal.android.paypalnativepayments.PayPalNativeCheckoutClient
import com.paypal.android.paypalnativepayments.PayPalNativeCheckoutListener
import com.paypal.android.paypalnativepayments.PayPalNativeCheckoutRequest
Expand Down Expand Up @@ -167,7 +168,8 @@ class PayPalNativeViewModel @Inject constructor(
completeOrderState = ActionState.Failure(Exception("Create an order to continue."))
} else {
completeOrderState = ActionState.Loading
val cmid = payPalDataCollector.collectDeviceData(getApplication())
val dataCollectorRequest = PayPalDataCollectorRequest(hasUserLocationConsent = false)
val cmid = payPalDataCollector.collectDeviceData(getApplication(), dataCollectorRequest)
completeOrderState = completeOrderUseCase(orderId, intentOption, cmid).mapToActionState()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.paypal.android.usecase.CompleteOrderUseCase
import com.paypal.android.usecase.CreateOrderUseCase
import com.paypal.android.usecase.GetClientIdUseCase
import com.paypal.android.api.services.SDKSampleServerResult
import com.paypal.android.fraudprotection.PayPalDataCollectorRequest
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand Down Expand Up @@ -147,7 +148,8 @@ class PayPalWebViewModel @Inject constructor(
} else {
viewModelScope.launch {
completeOrderState = ActionState.Loading
val cmid = payPalDataCollector.collectDeviceData(context)
val dataCollectorRequest = PayPalDataCollectorRequest(hasUserLocationConsent = false)
val cmid = payPalDataCollector.collectDeviceData(context, dataCollectorRequest)
completeOrderState = completeOrderUseCase(orderId, intentOption, cmid).mapToActionState()
}
}
Expand Down
4 changes: 1 addition & 3 deletions FraudProtection/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,7 @@ android {
}

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

api project(':CorePayments')
implementation deps.kotlinStdLib
Expand Down
Binary file removed FraudProtection/libs/android-magnessdk-5.4.0.jar
Binary file not shown.
Binary file added FraudProtection/libs/android-magnessdk-5.5.0.jar
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,46 @@ class PayPalDataCollector internal constructor(
*
* @return clientMetadataId Your server will send this to PayPal
*/
@Deprecated("This method is no longer supported.")
@JvmOverloads
fun collectDeviceData(
context: Context,
clientMetadataId: String? = null,
additionalData: HashMap<String, String>? = null
): String {
val request = PayPalDataCollectorRequest(
hasUserLocationConsent = false,
clientMetadataId = clientMetadataId,
additionalData = additionalData
)
return collectDeviceData(context, request)
}

/**
* Use to collects device data at the time of payment. Once a user initiates a payment
* from their device, PayPal uses the Client Metadata ID to verify that the payment is
* originating from a valid, user-consented device and application. This helps reduce fraud and
* decrease declines. This method MUST be called prior to initiating a pre-consented payment (a
* "future payment") from a mobile device. Pass the result to your server, to include in the
* payment request sent to PayPal. Do not otherwise cache or store this value.
*
* @param context Android Context
* @param request Request object containing parameters to configure data collection
*/
fun collectDeviceData(context: Context, request: PayPalDataCollectorRequest): String {
val appContext = context.applicationContext
return try {
val magnesSettingsBuilder = MagnesSettings.Builder(appContext)
.setMagnesSource(MagnesSource.PAYPAL)
.disableBeacon(false)
.setMagnesEnvironment(environment)
.setAppGuid(uuidHelper.getInstallationGUID(context))
.setHasUserLocationConsent(request.hasUserLocationConsent)
magnesSDK.setUp(magnesSettingsBuilder.build())
val result = magnesSDK.collectAndSubmit(
appContext,
clientMetadataId,
additionalData
request.clientMetadataId,
HashMap(request.additionalData ?: emptyMap())
)
result.paypalClientMetaDataId
} catch (e: InvalidInputException) {
Expand All @@ -66,6 +88,7 @@ class PayPalDataCollector internal constructor(
}
}

// NEXT MAJOR VERSION: consider removing this method; it has no merchant facing purpose
fun setLogging(shouldLog: Boolean) {
System.setProperty("magnes.debug.mode", shouldLog.toString())
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.paypal.android.fraudprotection

/**
* Request object containing parameters to configure fraud protection data collection.
*
* @property [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.
* @property [clientMetadataId] forward this data to your server when completing a transaction
* @property [additionalData] additional metadata to link with data collection
*/
data class PayPalDataCollectorRequest @JvmOverloads constructor(
val hasUserLocationConsent: Boolean,
val clientMetadataId: String? = null,
val additionalData: Map<String, String>? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import lib.android.paypal.com.magnessdk.MagnesSettings
import lib.android.paypal.com.magnessdk.MagnesSource
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.util.UUID

Expand All @@ -24,6 +26,13 @@ class PayPalDataCollectorUnitTest {
private val sandboxConfig = CoreConfig("fake-client-id", Environment.SANDBOX)
private val liveConfig = CoreConfig("fake-client-id", Environment.LIVE)

private lateinit var context: Context

@Before
fun beforeEach() {
context = mockk(relaxed = true)
}

@Test
fun `when environment is SANDBOX, magnes settings environment is STAGE`() {
val appGUID = UUID.randomUUID().toString()
Expand All @@ -33,11 +42,14 @@ class PayPalDataCollectorUnitTest {

every { mockUUIDHelper.getInstallationGUID(any()) } returns appGUID
val sut = PayPalDataCollector(sandboxConfig, mockMagnesSDK, mockUUIDHelper)
sut.collectDeviceData(mockk(relaxed = true))
sut.collectDeviceData(context, PayPalDataCollectorRequest(hasUserLocationConsent = false))
verify { mockMagnesSDK.setUp(capture(magnesSettingsSlot)) }

val magnesSettings = magnesSettingsSlot.captured
assertEquals(magnesSettings.environment, lib.android.paypal.com.magnessdk.Environment.SANDBOX)
assertEquals(
magnesSettings.environment,
lib.android.paypal.com.magnessdk.Environment.SANDBOX
)
assertFalse(magnesSettings.isDisableBeacon)
assertEquals(magnesSettings.appGuid, appGUID)
assertEquals(magnesSettings.magnesSource, MagnesSource.PAYPAL.version)
Expand All @@ -53,7 +65,7 @@ class PayPalDataCollectorUnitTest {
every { mockUUIDHelper.getInstallationGUID(any()) } returns appGUID

val sut = PayPalDataCollector(liveConfig, mockMagnesSDK, mockUUIDHelper)
sut.collectDeviceData(mockk(relaxed = true))
sut.collectDeviceData(context, PayPalDataCollectorRequest(hasUserLocationConsent = false))

verify { mockMagnesSDK.setUp(capture(magnesSettingsSlot)) }

Expand All @@ -64,14 +76,15 @@ class PayPalDataCollectorUnitTest {
@Test
fun `when appGUID is invalid, InvalidInputException is thrown`() {
mockkStatic(Log::class)
val errorMessage = "Application’s Globally Unique Identifier (AppGUID) does not match the criteria," +
" This is a string that identifies the merchant application that sets up Magnes on the mobile" +
" device. If the merchant app does not pass an AppGuid, Magnes creates one to identify" +
" the app. An AppGuid is an application identifier per-installation; that is," +
" if a new instance of the app is installed on the mobile device, or the app" +
" is reinstalled, it will have a new AppGuid.\n ***AppGuid Criteria*** \n " +
"Max length: 36 characters \n Min Length: 30 characters \n " +
"Regex: Letters, numbers and dashes only \n"
val errorMessage =
"Application’s Globally Unique Identifier (AppGUID) does not match the criteria," +
" This is a string that identifies the merchant application that sets up Magnes on the mobile" +
" device. If the merchant app does not pass an AppGuid, Magnes creates one to identify" +
" the app. An AppGuid is an application identifier per-installation; that is," +
" if a new instance of the app is installed on the mobile device, or the app" +
" is reinstalled, it will have a new AppGuid.\n ***AppGuid Criteria*** \n " +
"Max length: 36 characters \n Min Length: 30 characters \n " +
"Regex: Letters, numbers and dashes only \n"
val appGUID = "invalid_uuid"
val mockMagnesSDK = mockk<MagnesSDK>(relaxed = true)
val mockUUIDHelper = mockk<UUIDHelper>(relaxed = true)
Expand All @@ -80,7 +93,10 @@ class PayPalDataCollectorUnitTest {
every { mockUUIDHelper.getInstallationGUID(any()) } returns appGUID

val sut = PayPalDataCollector(sandboxConfig, mockMagnesSDK, mockUUIDHelper)
val result = sut.collectDeviceData(mockk(relaxed = true))
val result = sut.collectDeviceData(
context,
PayPalDataCollectorRequest(hasUserLocationConsent = false)
)

verify { Log.e(any(), any(), capture(exceptionSlot)) }

Expand All @@ -103,14 +119,38 @@ class PayPalDataCollectorUnitTest {
every { mockMagnesSDK.collectAndSubmit(any(), any(), any()) } returns magnesResult

val sut = PayPalDataCollector(sandboxConfig, mockMagnesSDK, mockUUIDHelper)
val result = sut.collectDeviceData(mockContext, clientMetadataId, HashMap())
val request = PayPalDataCollectorRequest(
hasUserLocationConsent = false,
clientMetadataId = clientMetadataId
)
val result = sut.collectDeviceData(mockContext, request)
assertEquals(result, clientMetadataId)
}

@Test
fun `when setLogging is called, System is called with correct value`() {
val sut = PayPalDataCollector(mockk(relaxed = true), mockk(relaxed = true), mockk(relaxed = true))
val sut =
PayPalDataCollector(mockk(relaxed = true), mockk(relaxed = true), mockk(relaxed = true))
sut.setLogging(true)
assertEquals(System.getProperty("magnes.debug.mode"), true.toString())
}

@Test
fun `collectDeviceData forwards hasUserLocationConsent value`() {
val appGUID = UUID.randomUUID().toString()
val mockMagnesSDK = mockk<MagnesSDK>(relaxed = true)
val mockUUIDHelper = mockk<UUIDHelper>(relaxed = true)
val magnesSettingsSlot = slot<MagnesSettings>()

every { mockUUIDHelper.getInstallationGUID(any()) } returns appGUID

val sut = PayPalDataCollector(liveConfig, mockMagnesSDK, mockUUIDHelper)
val request = PayPalDataCollectorRequest(hasUserLocationConsent = true)
sut.collectDeviceData(context, request)

verify { mockMagnesSDK.setUp(capture(magnesSettingsSlot)) }

val magnesSettings = magnesSettingsSlot.captured
assertTrue(magnesSettings.hasUserLocationConsent())
}
}

0 comments on commit edcb069

Please sign in to comment.