Skip to content

Commit

Permalink
Merge branch 'main' into contact-information-feature
Browse files Browse the repository at this point in the history
  • Loading branch information
jaxdesmarais authored Jan 21, 2025
2 parents 028271a + f8c1635 commit 285c1e8
Show file tree
Hide file tree
Showing 1,195 changed files with 2,676 additions and 2,440 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
* Add `PayPalContactInformation` request object
* Add `PayPalCheckoutRequest.contactInformation` optional property

## 5.4.0 (2025-01-21)

* PayPal
* Fix bug to ensure that `PayPalVaultRequest.userAuthenticationEmail` is not sent as an empty string
* Add `shippingCallbackUrl` to `PayPalCheckoutRequest`
* ThreeDSecure
* Return error if no `dfReferenceId` is returned in the 3D Secure flow

## 5.3.0 (2024-12-11)

* PayPal
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.braintreepayments.api.paypal

import android.net.Uri
import android.text.TextUtils
import com.braintreepayments.api.core.Authorization
import com.braintreepayments.api.core.ClientToken
Expand Down Expand Up @@ -55,6 +56,10 @@ import org.json.JSONObject
* @property shouldOfferPayLater Offers PayPal Pay Later if the customer qualifies. Defaults to
* false.
*
* @property shippingCallbackUrl Server side shipping callback URL to be notified when a customer
* updates their shipping address or options. A callback request will be sent to the merchant server
* at this URL.
*
* @property contactInformation Contact information of the recipient for the order
*/
@Parcelize
Expand All @@ -66,6 +71,7 @@ class PayPalCheckoutRequest @JvmOverloads constructor(
var currencyCode: String? = null,
var shouldRequestBillingAgreement: Boolean = false,
var shouldOfferPayLater: Boolean = false,
var shippingCallbackUrl: Uri? = null,
var contactInformation: PayPalContactInformation? = null,
override var localeCode: String? = null,
override var billingAgreementDescription: String? = null,
Expand All @@ -78,7 +84,7 @@ class PayPalCheckoutRequest @JvmOverloads constructor(
override var riskCorrelationId: String? = null,
override var userAuthenticationEmail: String? = null,
override var userPhoneNumber: PayPalPhoneNumber? = null,
override var lineItems: List<PayPalLineItem> = emptyList(),
override var lineItems: List<PayPalLineItem> = emptyList()
) : PayPalRequest(
hasUserLocationConsent = hasUserLocationConsent,
localeCode = localeCode,
Expand Down Expand Up @@ -108,6 +114,10 @@ class PayPalCheckoutRequest @JvmOverloads constructor(
.put(CANCEL_URL_KEY, cancelUrl)
.put(OFFER_PAY_LATER_KEY, shouldOfferPayLater)

shippingCallbackUrl?.let {
if (it.toString().isNotEmpty()) parameters.put(SHIPPING_CALLBACK_URL_KEY, it)
}

if (authorization is ClientToken) {
parameters.put(AUTHORIZATION_FINGERPRINT_KEY, authorization.bearer)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ import com.braintreepayments.api.BrowserSwitchException
import com.braintreepayments.api.BrowserSwitchFinalResult
import com.braintreepayments.api.BrowserSwitchStartResult
import com.braintreepayments.api.core.AnalyticsClient
import com.braintreepayments.api.core.AnalyticsEventParams
import com.braintreepayments.api.core.BraintreeException
import com.braintreepayments.api.core.GetReturnLinkUseCase
import com.braintreepayments.api.core.MerchantRepository

/**
* Responsible for launching PayPal user authentication in a web browser
*/
class PayPalLauncher internal constructor(
private val browserSwitchClient: BrowserSwitchClient,
private val merchantRepository: MerchantRepository = MerchantRepository.instance,
private val getReturnLinkUseCase: GetReturnLinkUseCase = GetReturnLinkUseCase(merchantRepository),
lazyAnalyticsClient: Lazy<AnalyticsClient>
) {
/**
Expand Down Expand Up @@ -82,7 +87,20 @@ class PayPalLauncher internal constructor(
pendingRequest: PayPalPendingRequest.Started,
intent: Intent
): PayPalPaymentAuthResult {
analyticsClient.sendEvent(PayPalAnalytics.HANDLE_RETURN_STARTED)
analyticsClient.sendEvent(
PayPalAnalytics.HANDLE_RETURN_STARTED,
AnalyticsEventParams(
appSwitchUrl = when (val returnLinkResult = getReturnLinkUseCase()) {
is GetReturnLinkUseCase.ReturnLinkResult.AppLink -> {
returnLinkResult.appLinkReturnUri.toString()
}
is GetReturnLinkUseCase.ReturnLinkResult.DeepLink -> {
returnLinkResult.deepLinkFallbackUrlScheme
}
else -> null
}
)
)
return when (val browserSwitchResult =
browserSwitchClient.completeRequest(intent, pendingRequest.pendingRequestString)) {
is BrowserSwitchFinalResult.Success -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ abstract class PayPalRequest internal constructor(
internal const val PLAN_TYPE_KEY: String = "plan_type"
internal const val PLAN_METADATA_KEY: String = "plan_metadata"
internal const val PHONE_NUMBER_KEY: String = "phone_number"
internal const val SHIPPING_CALLBACK_URL_KEY: String = "shipping_callback_url"
internal const val RECIPIENT_EMAIL_KEY: String = "recipient_email"
internal const val RECIPIENT_PHONE_NUMBER_KEY: String = "international_phone"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class PayPalVaultRequest
override var riskCorrelationId: String? = null,
override var userAuthenticationEmail: String? = null,
override var userPhoneNumber: PayPalPhoneNumber? = null,
override var lineItems: List<PayPalLineItem> = emptyList(),
override var lineItems: List<PayPalLineItem> = emptyList()
) : PayPalRequest(
hasUserLocationConsent = hasUserLocationConsent,
localeCode = localeCode,
Expand All @@ -62,11 +62,11 @@ class PayPalVaultRequest
merchantAccountId = merchantAccountId,
riskCorrelationId = riskCorrelationId,
userAuthenticationEmail = userAuthenticationEmail,
lineItems = lineItems
lineItems = lineItems,
) {

@Throws(JSONException::class)
@Suppress("LongMethod")
@Suppress("LongMethod", "CyclomaticComplexMethod")
override fun createRequestBody(
configuration: Configuration?,
authorization: Authorization?,
Expand All @@ -90,7 +90,9 @@ class PayPalVaultRequest
parameters.put(DESCRIPTION_KEY, billingAgreementDescription)
}

parameters.putOpt(PAYER_EMAIL_KEY, userAuthenticationEmail)
if (!userAuthenticationEmail.isNullOrEmpty()) {
parameters.put(PAYER_EMAIL_KEY, userAuthenticationEmail)
}

userPhoneNumber?.let { parameters.put(PHONE_NUMBER_KEY, it.toJson()) }

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.braintreepayments.api.paypal;

import android.net.Uri;
import android.os.Parcel;

import com.braintreepayments.api.core.Authorization;
import com.braintreepayments.api.core.Configuration;
import com.braintreepayments.api.core.PostalAddress;

import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
Expand Down Expand Up @@ -157,6 +159,58 @@ public void createRequestBody_does_not_set_userAuthenticationEmail_when_email_is
}

@Test
public void createRequestBody_sets_shippingCallbackUri_when_not_null() throws JSONException {
String urlString = "https://www.example.com/path";
Uri uri = Uri.parse(urlString);

PayPalCheckoutRequest request = new PayPalCheckoutRequest("1.00", true);
request.setShippingCallbackUrl(uri);

String requestBody = request.createRequestBody(
mock(Configuration.class),
mock(Authorization.class),
"success_url",
"cancel_url",
null
);

JSONObject jsonObject = new JSONObject(requestBody);
assertEquals(urlString, jsonObject.getString("shipping_callback_url"));
}

@Test
public void createRequestBody_does_not_set_shippingCallbackUri_when_null() throws JSONException {
PayPalCheckoutRequest request = new PayPalCheckoutRequest("1.00", true);

String requestBody = request.createRequestBody(
mock(Configuration.class),
mock(Authorization.class),
"success_url",
"cancel_url",
null
);

JSONObject jsonObject = new JSONObject(requestBody);
assertFalse(jsonObject.has("shipping_callback_url"));
}

@Test
public void createRequestBody_does_not_set_shippingCallbackUri_when_empty() throws JSONException {
PayPalCheckoutRequest request = new PayPalCheckoutRequest("1.00", true);
request.setShippingCallbackUrl(Uri.parse(""));

String requestBody = request.createRequestBody(
mock(Configuration.class),
mock(Authorization.class),
"success_url",
"cancel_url",
null
);

JSONObject jsonObject = new JSONObject(requestBody);
assertFalse(jsonObject.has("shipping_callback_url"));
}

public void createRequestBody_sets_userPhoneNumber_when_not_null() throws JSONException {
PayPalCheckoutRequest request = new PayPalCheckoutRequest("1.00", true);

Expand Down Expand Up @@ -204,4 +258,4 @@ public void createRequestBody_does_not_set_contactInformation_when_contactInform
assertFalse(requestBody.contains("\"recipient_email\":\"[email protected]\""));
assertFalse(requestBody.contains("\"international_phone\":{\"country_code\":\"1\",\"national_number\":\"1234567890\"}"));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package com.braintreepayments.api.paypal

import android.content.Intent
import android.net.Uri
import androidx.activity.ComponentActivity
import com.braintreepayments.api.BrowserSwitchClient
import com.braintreepayments.api.BrowserSwitchException
import com.braintreepayments.api.BrowserSwitchFinalResult
import com.braintreepayments.api.BrowserSwitchOptions
import com.braintreepayments.api.BrowserSwitchStartResult
import com.braintreepayments.api.core.AnalyticsClient
import com.braintreepayments.api.core.AnalyticsEventParams
import com.braintreepayments.api.core.GetReturnLinkUseCase
import com.braintreepayments.api.core.MerchantRepository
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
Expand All @@ -29,13 +33,26 @@ class PayPalLauncherUnitTest {
private val options: BrowserSwitchOptions = mockk(relaxed = true)
private val pendingRequestString = "pending_request_string"
private val analyticsClient: AnalyticsClient = mockk(relaxed = true)
private val merchantRepository = mockk<MerchantRepository>(relaxed = true)
private val getReturnLinkUseCase = mockk<GetReturnLinkUseCase>()
private val returnUrl = "https://return.url"
private val deepLinkScheme = "deepLinkScheme"

private lateinit var sut: PayPalLauncher

@Before
fun setup() {
every { paymentAuthRequestParams.browserSwitchOptions } returns options
sut = PayPalLauncher(browserSwitchClient, lazy { analyticsClient })

val appSwitchReturnUrl = Uri.parse(returnUrl)
every { getReturnLinkUseCase() } returns GetReturnLinkUseCase.ReturnLinkResult.AppLink(
appSwitchReturnUrl
)
sut = PayPalLauncher(
browserSwitchClient,
merchantRepository,
getReturnLinkUseCase,
lazy { analyticsClient })
}

@Test
Expand Down Expand Up @@ -100,7 +117,48 @@ class PayPalLauncherUnitTest {
PayPalPendingRequest.Started(pendingRequestString),
intent
)
verify { analyticsClient.sendEvent(PayPalAnalytics.HANDLE_RETURN_STARTED) }
verify {
analyticsClient.sendEvent(
PayPalAnalytics.HANDLE_RETURN_STARTED,
AnalyticsEventParams(appSwitchUrl = returnUrl)
)
}
}

@Test
@Throws(JSONException::class)
fun `handleReturnToApp with deeplinkScheme sends handle started event with deeplink scheme`() {
every { getReturnLinkUseCase() } returns GetReturnLinkUseCase.ReturnLinkResult.DeepLink(
deepLinkScheme
)
sut.handleReturnToApp(
PayPalPendingRequest.Started(pendingRequestString),
intent
)
verify {
analyticsClient.sendEvent(
PayPalAnalytics.HANDLE_RETURN_STARTED,
AnalyticsEventParams(appSwitchUrl = deepLinkScheme)
)
}
}

@Test
@Throws(JSONException::class)
fun `handleReturnToApp with ReturnLinkResult Failure sends handle started event with null appSwitchUrl`() {
every { getReturnLinkUseCase() } returns GetReturnLinkUseCase.ReturnLinkResult.Failure(
Exception("handle return start failed")
)
sut.handleReturnToApp(
PayPalPendingRequest.Started(pendingRequestString),
intent
)
verify {
analyticsClient.sendEvent(
PayPalAnalytics.HANDLE_RETURN_STARTED,
AnalyticsEventParams()
)
}
}

@Test
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ For an integration offering card payments, add the following dependency in your

```groovy
dependencies {
implementation 'com.braintreepayments.api:card:5.3.0'
implementation 'com.braintreepayments.api:card:5.4.0'
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,17 @@ class ThreeDSecureClient internal constructor(
configuration,
request
) { consumerSessionId: String?, _ ->
if (consumerSessionId != null) {
try {
if (!consumerSessionId.isNullOrEmpty()) {
lookupJSON.put("dfReferenceId", consumerSessionId)
} catch (ignored: JSONException) {
}
} else {
callbackPrepareLookupFailure(
callback,
ThreeDSecurePrepareLookupResult.Failure(
BraintreeException("There was an error retrieving the dfReferenceId.")
)
)

return@initialize
}
braintreeClient.sendAnalyticsEvent(ThreeDSecureAnalytics.LOOKUP_SUCCEEDED)
callback.onPrepareLookupResult(
Expand Down
Loading

0 comments on commit 285c1e8

Please sign in to comment.