diff --git a/BraintreeCore/build.gradle b/BraintreeCore/build.gradle index ee878d912d..cbd9b56c3a 100644 --- a/BraintreeCore/build.gradle +++ b/BraintreeCore/build.gradle @@ -78,6 +78,7 @@ dependencies { testImplementation deps.mockitoCore testImplementation deps.jsonAssert testImplementation deps.mockk + testImplementation deps.kotlinTest testImplementation project(':PayPal') testImplementation project(':TestUtils') testImplementation project(':UnionPay') diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/BuyerPhone.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/BuyerPhone.kt new file mode 100644 index 0000000000..4f1d7e20f3 --- /dev/null +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/BuyerPhone.kt @@ -0,0 +1,14 @@ +package com.braintreepayments.api + +/** + * Representation of a user phone number. + * @property countryCode The international country code for the shopper's phone number + * (e.g., "1" for the United States). + * @property nationalNumber The national segment of the shopper's phone number + * (excluding the country code). + */ + +data class BuyerPhone( + var countryCode: String, + var nationalNumber: String +) diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/PaymentReadyApi.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/PaymentReadyApi.kt new file mode 100644 index 0000000000..9767f4a690 --- /dev/null +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/PaymentReadyApi.kt @@ -0,0 +1,30 @@ +package com.braintreepayments.api + +import org.json.JSONObject + +// TODO: Implementation, documentation and interface. +internal class PaymentReadyApi { + fun processRequest(request: ShopperInsightsRequest): String = request.toJson() + + private fun ShopperInsightsRequest.toJson(): String { + return JSONObject().apply { + put(KEY_CUSTOMER, JSONObject().apply { + putOpt(KEY_EMAIL, email) + phone?.let { + put(KEY_PHONE, JSONObject().apply { + put(KEY_COUNTRY_CODE, it.countryCode) + put(KEY_NATIONAL_NUMBER, it.nationalNumber) + }) + } + }) + }.toString() + } + + companion object { + internal const val KEY_COUNTRY_CODE = "countryCode" + internal const val KEY_NATIONAL_NUMBER = "nationalNumber" + internal const val KEY_CUSTOMER = "customer" + internal const val KEY_EMAIL = "email" + internal const val KEY_PHONE = "phone" + } +} diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/ShopperInsightsCallback.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/ShopperInsightsCallback.kt new file mode 100644 index 0000000000..ed9a57b1b8 --- /dev/null +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/ShopperInsightsCallback.kt @@ -0,0 +1,8 @@ +package com.braintreepayments.api + +/** + * A callback that returns information on whether someone is a PayPal or a Venmo shopper. + */ +fun interface ShopperInsightsCallback { + fun onResult(result: ShopperInsightsResult) +} diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/ShopperInsightsClient.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/ShopperInsightsClient.kt new file mode 100644 index 0000000000..e56b0df214 --- /dev/null +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/ShopperInsightsClient.kt @@ -0,0 +1,50 @@ +package com.braintreepayments.api + +import androidx.annotation.VisibleForTesting + +/** + * Use [ShopperInsightsClient] to optimize your checkout experience + * by prioritizing the customer’s preferred payment methods in your UI. + * By customizing each customer’s checkout experience, + * you can improve conversion, increase sales/repeat buys and boost user retention/loyalty. + * + * Note: **This feature is in beta. It's public API may change in future releases.** + */ +class ShopperInsightsClient @VisibleForTesting internal constructor( + private val paymentReadyAPI: PaymentReadyApi +) { + /** + * Retrieves recommended payment methods based on the provided shopper insights request. + * + * @param request The [ShopperInsightsRequest] containing information about the shopper. + * @return A [ShopperInsightsResult] object indicating the recommended payment methods. + */ + fun getRecommendedPaymentMethods( + request: ShopperInsightsRequest, + callback: ShopperInsightsCallback + ) { + if (request.email == null && request.phone == null) { + callback.onResult( + ShopperInsightsResult.Failure( + IllegalArgumentException( + "One of ShopperInsightsRequest.email or " + + "ShopperInsightsRequest.phone must be non-null." + ) + ) + ) + return + } + + // TODO: - Add isAppInstalled checks for PP & Venmo. DTBTSDK-3176 + paymentReadyAPI.processRequest(request) + // Hardcoded result + callback.onResult( + ShopperInsightsResult.Success( + ShopperInsightsInfo( + isPayPalRecommended = false, + isVenmoRecommended = false + ) + ) + ) + } +} diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/ShopperInsightsInfo.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/ShopperInsightsInfo.kt new file mode 100644 index 0000000000..e250de163a --- /dev/null +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/ShopperInsightsInfo.kt @@ -0,0 +1,20 @@ +package com.braintreepayments.api + +/** + * Data class encapsulating the result of a shopper insight api request. + * + * This class holds information about the recommended payment methods for a shopper + * The recommendations include flags for whether payment methods like PayPal or Venmo + * should be displayed with high priority in the user interface. + * + * @property isPayPalRecommended If true, indicates that the PayPal payment option + * should be given high priority in the checkout UI. + * @property isVenmoRecommended If true, indicates that the Venmo payment option + * should be given high priority in the checkout UI. + * + * Note: **This feature is in beta. It's public API may change in future releases.** + */ +data class ShopperInsightsInfo( + val isPayPalRecommended: Boolean, + val isVenmoRecommended: Boolean +) diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/ShopperInsightsRequest.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/ShopperInsightsRequest.kt new file mode 100644 index 0000000000..b2b4a8f91c --- /dev/null +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/ShopperInsightsRequest.kt @@ -0,0 +1,16 @@ +package com.braintreepayments.api + +/** + * Data class representing a request for shopper insights. + * + * @property email The shopper's email address + * @property phone The shopper's phone number + * + * One of [email] or [phone] must be provided to get shopper insights. + * + * Note: **This feature is in beta. It's public API may change in future releases.** + */ +data class ShopperInsightsRequest( + var email: String?, + var phone: BuyerPhone? +) diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/ShopperInsightsResult.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/ShopperInsightsResult.kt new file mode 100644 index 0000000000..c6ed736f3a --- /dev/null +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/ShopperInsightsResult.kt @@ -0,0 +1,17 @@ +package com.braintreepayments.api + +/** + * The result object returned when insights about a shopper is requested. + */ +sealed class ShopperInsightsResult { + + /** + * @property response The response object describing the shopper's insights. + */ + class Success(val response: ShopperInsightsInfo) : ShopperInsightsResult() + + /** + * @property error An object that describes the error that occurred. + */ + class Failure(val error: Exception) : ShopperInsightsResult() +} diff --git a/BraintreeCore/src/test/java/com/braintreepayments/api/PaymentReadyApiTest.kt b/BraintreeCore/src/test/java/com/braintreepayments/api/PaymentReadyApiTest.kt new file mode 100644 index 0000000000..b9e1a5bb43 --- /dev/null +++ b/BraintreeCore/src/test/java/com/braintreepayments/api/PaymentReadyApiTest.kt @@ -0,0 +1,82 @@ +package com.braintreepayments.api + +import org.junit.Test +import org.skyscreamer.jsonassert.JSONAssert + +class PaymentReadyApiTest { + + private val paymentReadyApi = PaymentReadyApi() + + @Test + fun `test phone to json string conversion`() { + val testCountryCode = "1" + val testNationalNumber = "123456789" + val request = ShopperInsightsRequest( + null, + BuyerPhone( + countryCode = testCountryCode, + nationalNumber = testNationalNumber + ) + ) + + val observedJsonString = paymentReadyApi.processRequest(request) + val expectedJsonString = """ + { + "customer": { + "phone": { + "countryCode": "$testCountryCode", + "nationalNumber": "$testNationalNumber" + } + } + } + """.trimIndent() + + JSONAssert.assertEquals(expectedJsonString, observedJsonString, true) + } + + @Test + fun `test email to json string conversion`() { + val email = "fake-email@email-provider.com" + val request = ShopperInsightsRequest(email, null) + + val observedJsonString = paymentReadyApi.processRequest(request) + val expectedJsonString = """ + { + "customer": { + "email": "$email" + } + } + """.trimIndent() + + JSONAssert.assertEquals(expectedJsonString, observedJsonString, true) + } + + @Test + fun `test email and phone to json string conversion`() { + val email = "fake-email@email-provider.com" + val testCountryCode = "1" + val testNationalNumber = "123456789" + val request = ShopperInsightsRequest( + email, + BuyerPhone( + countryCode = testCountryCode, + nationalNumber = testNationalNumber + ) + ) + + val observedJsonString = paymentReadyApi.processRequest(request) + val expectedJsonString = """ + { + "customer": { + "phone": { + "countryCode": "$testCountryCode", + "nationalNumber": "$testNationalNumber" + }, + "email": "$email" + } + } + """.trimIndent() + + JSONAssert.assertEquals(expectedJsonString, observedJsonString, true) + } +} diff --git a/BraintreeCore/src/test/java/com/braintreepayments/api/ShopperInsightsClientUnitTest.kt b/BraintreeCore/src/test/java/com/braintreepayments/api/ShopperInsightsClientUnitTest.kt new file mode 100644 index 0000000000..c1b317ae99 --- /dev/null +++ b/BraintreeCore/src/test/java/com/braintreepayments/api/ShopperInsightsClientUnitTest.kt @@ -0,0 +1,57 @@ +package com.braintreepayments.api + +import io.mockk.mockk +import kotlin.test.assertEquals +import kotlin.test.assertIs +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Test + +/** + * Unit tests for BraintreeShopperInsightsClient. + * + * This class contains tests for the shopper insights functionality within the Braintree SDK. + * It focuses on testing how the client handles different scenarios when fetching recommended + * payment methods. + */ +class ShopperInsightsClientUnitTest { + + private lateinit var sut: ShopperInsightsClient + private lateinit var paymentApi: PaymentReadyApi + + @Before + fun beforeEach() { + paymentApi = mockk(relaxed = true) + sut = ShopperInsightsClient(paymentApi) + } + + /** + * Tests if the getRecommendedPaymentMethods method returns paypal and venmo recommendations + * when providing a shopping insight request. + */ + @Test + fun testGetRecommendedPaymentMethods_returnsDefaultRecommendations() { + val request = ShopperInsightsRequest("fake-email", null) + sut.getRecommendedPaymentMethods(request) { result -> + assertNotNull(result) + val successResult = assertIs(result) + assertNotNull(successResult.response.isPayPalRecommended) + assertNotNull(successResult.response.isVenmoRecommended) + } + } + + @Test + fun `testGetRecommendedPaymentMethods - request object has null properties`() { + val request = ShopperInsightsRequest(null, null) + sut.getRecommendedPaymentMethods(request) { result -> + assertNotNull(result) + val error = assertIs(result) + val iae = assertIs(error.error) + assertEquals( + "One of ShopperInsightsRequest.email or " + + "ShopperInsightsRequest.phone must be non-null.", + iae.message + ) + } + } +}