Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
760d0b4
feat: add pay later wordmark assets (#1722)
agedd Oct 15, 2026
42a7198
docs(readme): add Pay Later button section and fix PayPal button meth…
saralvasquez Oct 26, 2026
427ef52
Add Basic Pay Later Buttons (#1723)
agedd Oct 27, 2026
99bdbf6
Add JetPack Compose To Pay Later Button (#1733)
agedd Nov 3, 2026
6c3984a
Merge branch 'main' into pay-later-button-feature
saralvasquez Nov 3, 2026
15f09a2
test: Expand UIComponents unit test coverage (#1749)
saralvasquez Nov 4, 2026
69b6e93
feat: Pay later analytics (#1734)
saralvasquez Nov 4, 2026
7767a0d
Merge branch 'main' into pay-later-button-feature
saralvasquez Nov 4, 2026
75d8fbe
test: Pay later button UI tests (#1751)
saralvasquez Nov 6, 2026
0dde39a
docs: Add Compose PayLater button integration instructions to README
saralvasquez Nov 7, 2026
448f791
fix: add CHANGELOG entry
saralvasquez Nov 7, 2026
9d2e1e7
fix: deliver Compose PayPal and Pay Later results after Activity recr…
agedd Nov 9, 2026
f9595fc
Merge branch 'main' into pay-later-button-feature
saralvasquez Nov 10, 2026
ddab0dd
pr-feedback: update CHANGELOG and add test tear down
saralvasquez Nov 10, 2026
78af4ac
fix: restore pending request before completing xml pay-later-button flow
agedd Nov 10, 2026
4f9f57f
test: stub restorePendingRequest and cover restore in PayLaterButtonU…
agedd Nov 10, 2026
02a955b
fix: deliver compose venmo result after activity recreation and add c…
agedd Nov 10, 2026
b8f42ce
refactor: inline pending request restore in compose buttons
agedd Nov 10, 2026
23b50db
fix: restore pending request before completing xml venmo button flow
agedd Nov 10, 2026
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@

## unreleased

* PayPal
* Add `PayPalLauncher(ActivityResultRegistry, String)` constructor with a `resultKey` parameter, allowing the Auth Tab result to be delivered after the Activity is recreated
* Venmo
* Add `VenmoLauncher(ActivityResultRegistry, String)` constructor with a `resultKey` parameter, allowing the Auth Tab result to be delivered after the Activity is recreated
* UIComponents
* Fix Compose `PayPalButton`, `PayLaterButton`, and `VenmoButton` not returning a result when the Activity is recreated while the browser is open
* Fix `VenmoButton` not re-enabling after failure in `createPaymentAuthRequest`, `venmoLauncher.launch()`, or when the hosting Activity is null
* Fix `PayPalButton` returning a cancel instead of a result when the Activity is recreated while the browser is open
* Fix XML `VenmoButton` returning a cancel instead of a result when the Activity is recreated while the browser is open
* Fix `PayPalButton` and compose `PayPalButton` stuck in loading on failed auth request
* Change `PayPalButton` to re-renable only after tokenization is complete
* Add `PayLaterButton` and compose support to generate a PayPal-PayLater branded button for launching the PayLater flow

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we mention the fixes to the existing PayPalButton behavior, such as:

  • XML re-enabling after createPaymentAuthRequest failure, payPalLauncher.launch() failure, or a null hosting Activity
  • XML keeping the loading indicator until tokenize completes, rather than re-enabling immediately after dispatching tokenization
  • Compose re-enabling after payPalLauncher.launch() failure

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's a good callout. I'll add that to the CHANGELOG

* BraintreeCore
* Update Browser Switch version to 3.7.0

## 5.34.0 (2026-10-29)

Expand Down
4 changes: 2 additions & 2 deletions DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This document lists the runtime dependencies (both internal module dependencies

## browser-switch Dependencies

`com.braintreepayments.api:browser-switch:3.5.1` is declared as a dependency of `BraintreeCore`. Its own runtime dependencies are:
`com.braintreepayments.api:browser-switch:3.7.0` is declared as a dependency of `BraintreeCore`. Its own runtime dependencies are:

| Dependency | Version |
|---|---|
Expand All @@ -31,7 +31,7 @@ This document lists the runtime dependencies (both internal module dependencies
| Dependency | Version |
|---|---|
| `:SharedUtils` | — |
| `com.braintreepayments.api:browser-switch` | 3.5.1 |
| `com.braintreepayments.api:browser-switch` | 3.7.0 |
| `org.jetbrains.kotlinx:kotlinx-coroutines-core` | 1.7.1 |
| `androidx.appcompat:appcompat` | 1.6.0 |
| `androidx.work:work-runtime` | 2.8.1 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ import com.braintreepayments.api.core.PaymentMethodNonce
import com.braintreepayments.api.paypal.PayPalResult
import com.braintreepayments.api.paypal.PayPalTokenizeCallback
import com.braintreepayments.api.uicomponents.PayPalButtonColor
import com.braintreepayments.api.uicomponents.PayLaterButtonColor
import com.braintreepayments.api.uicomponents.compose.PayLaterButton
import com.braintreepayments.api.uicomponents.VenmoButtonColor
import com.braintreepayments.api.uicomponents.cardfields.CardFieldsResult
import com.braintreepayments.api.uicomponents.compose.CardFields
Expand Down Expand Up @@ -73,6 +75,8 @@ class ComposeUIComponentsFragment : BaseFragment() {
): View {
super.onCreateView(inflater, container, savedInstanceState)
val payPalRequest = payPalRequest(requireContext())
// Pay Later gets its own request: PayLaterButton sets shouldOfferPayLater = true on the request it is given.
val payLaterRequest = payPalRequest(requireContext())
return ComposeView(requireContext()).apply {
setContent {
var venmoStyleId by rememberSaveable { mutableIntStateOf(VenmoButtonColor.Blue.key) }
Expand All @@ -89,6 +93,13 @@ class ComposeUIComponentsFragment : BaseFragment() {
else -> PayPalButtonColor.Blue
}

var payLaterStyleId by rememberSaveable { mutableIntStateOf(PayLaterButtonColor.Blue.key) }
val payLaterStyle: PayLaterButtonColor = when (payLaterStyleId) {
1 -> PayLaterButtonColor.Black
2 -> PayLaterButtonColor.White
else -> PayLaterButtonColor.Blue
}

val cardFieldsController = rememberCardFieldsController(
authorization = authStringArg,
// optional customer data
Expand Down Expand Up @@ -153,6 +164,33 @@ class ComposeUIComponentsFragment : BaseFragment() {
}
}

// Pay Later section
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
) {
SectionLabel(stringResource(R.string.paypal_pay_later))
SingleChoiceSegmentedButton(
modifier = Modifier.fillMaxWidth(),
selectedIndex = payLaterStyleId,
onClick = { index -> payLaterStyleId = index }
)
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
PayLaterButton(
style = payLaterStyle,
payPalRequest = payLaterRequest,
authorization = authStringArg,
appLinkReturnUrl = APP_LINK_RETURN_URL.toUri(),
deepLinkFallbackUrlScheme = DEEP_LINK_FALLBACK_SCHEME,
paypalTokenizeCallback = payLaterTokenizeCallback
)
}
}

// Card Fields section
SectionLabel(
text = stringResource(R.string.card_fields),
Expand Down Expand Up @@ -252,10 +290,11 @@ class ComposeUIComponentsFragment : BaseFragment() {
)
}

private fun handleNonce(nonce: PaymentMethodNonce) {
private fun handleNonce(nonce: PaymentMethodNonce, isPayLaterSelected: Boolean = false) {
onPaymentMethodNonceCreated(nonce)
val action = ComposeUIComponentsFragmentDirections
.actionComposeUiComponentsFragmentToDisplayNonceFragment(nonce)
.setIsPayLaterSelected(isPayLaterSelected)
NavHostFragment.findNavController(this).navigate(action)
}

Expand All @@ -273,9 +312,12 @@ class ComposeUIComponentsFragment : BaseFragment() {
null
)

private val paypalTokenizeCallback = PayPalTokenizeCallback { payPalResult ->
private val paypalTokenizeCallback = createPayPalCallback(isPayLaterSelected = false)
private val payLaterTokenizeCallback = createPayPalCallback(isPayLaterSelected = true)

private fun createPayPalCallback(isPayLaterSelected: Boolean) = PayPalTokenizeCallback { payPalResult ->
when (payPalResult) {
is PayPalResult.Success -> handleNonce(payPalResult.nonce)
is PayPalResult.Success -> handleNonce(payPalResult.nonce, isPayLaterSelected)
is PayPalResult.Cancel ->
handleError(Exception("User did not complete PayPal payment flow"))
is PayPalResult.Failure -> handleError(payPalResult.error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class PendingRequestStore {
static final String PREFERENCES_KEY = "PENDING_REQUEST_SHARED_PREFERENCES";

static final String PAYPAL_PENDING_REQUEST_KEY = "PAYPAL_PENDING_REQUEST";
static final String PAYPAL_PAY_LATER_PENDING_REQUEST_KEY = "PAYPAL_PAY_LATER_PENDING_REQUEST";
static final String LOCAL_PAYMENT_PENDING_REQUEST_KEY = "LOCAL_PAYMENT_PENDING_REQUEST";
static final String SEPA_DIRECT_DEBIT_PENDING_REQUEST_KEY = "SEPA_DIRECT_DEBIT_PENDING_REQUEST";
static final String VENMO_PENDING_REQUEST_KEY = "VENMO_PENDING_REQUEST";
Expand Down Expand Up @@ -57,6 +58,23 @@ public void clearPayPalPendingRequest(Context context) {
remove(PAYPAL_PENDING_REQUEST_KEY, context);
}

public void putPayLaterPendingRequest(Context context,
PayPalPendingRequest.Started pendingRequest) {
put(PAYPAL_PAY_LATER_PENDING_REQUEST_KEY, pendingRequest.getPendingRequestString(), context);
}

public PayPalPendingRequest.Started getPayLaterPendingRequest(Context context) {
String requestString = get(PAYPAL_PAY_LATER_PENDING_REQUEST_KEY, context);
if (requestString != null) {
return new PayPalPendingRequest.Started(requestString);
}
return null;
}

public void clearPayLaterPendingRequest(Context context) {
remove(PAYPAL_PAY_LATER_PENDING_REQUEST_KEY, context);
}

public void putLocalPaymentPendingRequest(Context context,
LocalPaymentPendingRequest.Started pendingRequest) {
put(LOCAL_PAYMENT_PENDING_REQUEST_KEY, pendingRequest.getPendingRequestString(), context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import com.braintreepayments.api.core.PaymentMethodNonce
import com.braintreepayments.api.paypal.PayPalPendingRequest
import com.braintreepayments.api.paypal.PayPalRequest
import com.braintreepayments.api.paypal.PayPalResult
import com.braintreepayments.api.paypal.PayPalTokenizeCallback
import com.braintreepayments.api.uicomponents.PayLaterButton
import com.braintreepayments.api.uicomponents.PayLaterButtonColor
import com.braintreepayments.api.uicomponents.PayPalButton
import com.braintreepayments.api.uicomponents.PayPalButtonColor
import com.braintreepayments.api.uicomponents.PayPalLaunchCallback
Expand All @@ -34,6 +37,8 @@ import com.google.android.material.button.MaterialButtonToggleGroup
class UIComponentsFragment : BaseFragment() {
private lateinit var payPalButton: PayPalButton
private lateinit var payPalToggleGroup: MaterialButtonToggleGroup
private lateinit var payLaterButton: PayLaterButton
private lateinit var payLaterToggleGroup: MaterialButtonToggleGroup
private lateinit var venmoButton: VenmoButton
private lateinit var venmoRequest: VenmoRequest
private lateinit var venmoToggleGroup: MaterialButtonToggleGroup
Expand All @@ -46,12 +51,15 @@ class UIComponentsFragment : BaseFragment() {
val view = inflater.inflate(R.layout.fragment_ui_components, container, false)
payPalButton = view.findViewById(R.id.pp_payment_button)
payPalToggleGroup = view.findViewById(R.id.pp_button_toggle_group)
payLaterButton = view.findViewById(R.id.pay_later_payment_button)
payLaterToggleGroup = view.findViewById(R.id.pay_later_button_toggle_group)
venmoButton = view.findViewById(R.id.venmo_payment_button)
venmoToggleGroup = view.findViewById(R.id.venmo_button_toggle_group)

setupCardFields(view)
setupColorToggles()
setupPayPalButton()
setupPayLaterButton()
setupVenmoButton()
return view
}
Expand Down Expand Up @@ -105,6 +113,15 @@ class UIComponentsFragment : BaseFragment() {
}
}
}
payLaterToggleGroup.addOnButtonCheckedListener { _, checkedId, isChecked ->
if (isChecked) {
when (checkedId) {
R.id.button_pay_later_blue -> payLaterButton.setButtonColor(PayLaterButtonColor.Blue)
R.id.button_pay_later_black -> payLaterButton.setButtonColor(PayLaterButtonColor.Black)
R.id.button_pay_later_white -> payLaterButton.setButtonColor(PayLaterButtonColor.White)
}
}
}
venmoToggleGroup.addOnButtonCheckedListener { _, checkedId, isChecked ->
if (isChecked) {
when (checkedId) {
Expand Down Expand Up @@ -140,7 +157,43 @@ class UIComponentsFragment : BaseFragment() {
payPalButton.setPayPalRequest(payPalRequest)
payPalButton.payPalLaunchCallback = PayPalLaunchCallback { request: PayPalPendingRequest ->
when (request) {
is PayPalPendingRequest.Started -> storePayPalPendingRequest(request)
is PayPalPendingRequest.Started -> {
clearPayLaterPendingRequest()
storePayPalPendingRequest(request)
}
is PayPalPendingRequest.Failure -> handleError(request.error)
}
}
}

private fun setupPayLaterButton() {
val payPalCheckoutRequest = PayPalRequestFactory.createPayPalCheckoutRequest(
/* context = */ requireContext(),
/* amount = */ "10.0",
/* buyerEmailAddress = */ null,
/* buyerPhoneCountryCode = */ null,
/* buyerPhoneNationalNumber = */ null,
/* isContactInformationEnabled = */ false,
/* shopperInsightsSessionId = */ null,
/* offerPayLater = */ true,
/* offerCredit = */ false,
/* isAmountBreakdownEnabled = */ false,
/* campaignId = */ null
)

payLaterButton.initialize(
this,
super.getAuthStringArg(),
"https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments".toUri(),
"com.braintreepayments.demo.braintree"
)
payLaterButton.setPayPalRequest(payPalCheckoutRequest)
payLaterButton.payPalLaunchCallback = PayPalLaunchCallback { request: PayPalPendingRequest ->
when (request) {
is PayPalPendingRequest.Started -> {
clearPayPalPendingRequest()
storePayLaterPendingRequest(request)
}
is PayPalPendingRequest.Failure -> handleError(request.error)
}
}
Expand Down Expand Up @@ -187,37 +240,42 @@ class UIComponentsFragment : BaseFragment() {
override fun onResume() {
super.onResume()

// PayPal flow after returning to app
val pendingRequest = getPayPalPendingRequest()
if (pendingRequest != null) {
// PayPal or PayPal Pay Later flow after returning to app
getPayPalPendingRequest()?.let { pending ->
payPalButton.handleReturnToApp(
pendingRequest, requireActivity().intent) { payPalResult: PayPalResult? ->
when (payPalResult) {
is PayPalResult.Success -> handlePayPalResult(payPalResult.nonce)
is PayPalResult.Cancel ->
handleError(Exception("User did not complete payment flow"))
is PayPalResult.Failure -> handleError(payPalResult.error)
else -> Unit
}
}
pending,
requireActivity().intent,
createPayPalCallback(isPayLaterSelected = false)
)
clearPayPalPendingRequest()
requireActivity().intent.data = null
}

getPayLaterPendingRequest()?.let { pending ->
payLaterButton.handleReturnToApp(
pending,
requireActivity().intent,
createPayPalCallback(isPayLaterSelected = true)
)
clearPayLaterPendingRequest()
requireActivity().intent.data = null
}

// Venmo flow after returning to app
val venmoPendingRequest = getVenmoPendingRequest()
if (venmoPendingRequest != null) {
venmoButton.handleReturnToApp(
venmoPendingRequest,
requireActivity().intent) { venmoResult: VenmoResult? ->
when (venmoResult) {
is VenmoResult.Success -> handleVenmoAccountNonce(venmoResult.nonce)
is VenmoResult.Failure -> handleError(venmoResult.error)
is VenmoResult.Cancel ->
handleError(Exception("User did not complete payment flow"))
else -> Unit
}
requireActivity().intent
) { venmoResult: VenmoResult? ->
when (venmoResult) {
is VenmoResult.Success -> handleVenmoAccountNonce(venmoResult.nonce)
is VenmoResult.Failure -> handleError(venmoResult.error)
is VenmoResult.Cancel ->
handleError(Exception("User did not complete payment flow"))
else -> Unit
}
}
clearVenmoPendingRequest()
requireActivity().intent.data = null
}
Expand All @@ -230,11 +288,21 @@ class UIComponentsFragment : BaseFragment() {
NavHostFragment.findNavController(this).navigate(action)
}

private fun handlePayPalResult(paymentMethodNonce: PaymentMethodNonce?) {
private fun createPayPalCallback(isPayLaterSelected: Boolean) = PayPalTokenizeCallback { payPalResult ->
when (payPalResult) {
is PayPalResult.Success -> handlePayPalResult(payPalResult.nonce, isPayLaterSelected)
is PayPalResult.Cancel ->
handleError(Exception("User did not complete payment flow"))
is PayPalResult.Failure -> handleError(payPalResult.error)
}
}

private fun handlePayPalResult(paymentMethodNonce: PaymentMethodNonce?, isPayLaterSelected: Boolean) {
if (paymentMethodNonce != null) {
onPaymentMethodNonceCreated(paymentMethodNonce)
val action = UIComponentsFragmentDirections
.actionUiComponentsFragmentToDisplayNonceFragment(paymentMethodNonce)
.setIsPayLaterSelected(isPayLaterSelected)
NavHostFragment.findNavController(this).navigate(action)
}
}
Expand All @@ -256,4 +324,13 @@ class UIComponentsFragment : BaseFragment() {

private fun clearPayPalPendingRequest() =
PendingRequestStore.getInstance().clearPayPalPendingRequest(requireContext())

private fun storePayLaterPendingRequest(request: PayPalPendingRequest.Started) =
PendingRequestStore.getInstance().putPayLaterPendingRequest(requireContext(), request)

private fun getPayLaterPendingRequest(): PayPalPendingRequest.Started? =
PendingRequestStore.getInstance().getPayLaterPendingRequest(requireContext())

private fun clearPayLaterPendingRequest() =
PendingRequestStore.getInstance().clearPayLaterPendingRequest(requireContext())
}
Loading
Loading