Skip to content

feature(PayPal): collect and send device_info to determine app switch eligibility - #1600

Merged
jaxdesmarais merged 10 commits into
braintree:mainfrom
santugowda:paypal-app-switch-device-info
Jul 22, 2026
Merged

feature(PayPal): collect and send device_info to determine app switch eligibility#1600
jaxdesmarais merged 10 commits into
braintree:mainfrom
santugowda:paypal-app-switch-device-info

Conversation

@santugowda

@santugowda santugowda commented May 14, 2026

Copy link
Copy Markdown
Contributor

Summary of changes

Collects and sends device information as app_switch_context.device_info in PayPal Hermes requests when enablePayPalAppSwitch is enabled. This data allows the PayPal server to determine app switch eligibility, primarily for low-RAM devices.

Background

The PayPal App Switch flow launches the PayPal native app directly, bypassing browser checkout. On low-RAM devices, running two apps simultaneously (the merchant app + PayPal app) can cause the OS to kill the merchant app in the background due to memory pressure — leading to a broken checkout experience when the user tries to return.

The PayPal server uses memory_available_mb and memory_total_mb to gate app switch eligibility — if the device doesn't have enough RAM, the server skips app switch and falls back to browser checkout (Chrome Custom Tab), which is lighter on memory.

This PR adds collection and injection of:

  • model — device model (e.g. SM-A166U) for any device-specific overrides the server may have
  • memory_available_mb — free RAM at request time
  • memory_total_mb — total device RAM

These are injected into the request body under app_switch_context.device_info, giving the server everything it needs to decide whether to serve an app switch URL or a browser URL in the response.

JSON structure sent to BTGW

Note: The flat top-level os_type, os_version, and merchant_app_return_url keys are intentionally retained. BTGW reads these flat fields to determine app switch eligibility — sending only the nested structure broke the billing/vault app-switch flow (see commit 434accf). The nested app_switch_context now carries only device_info; the previously-nested native_app block (os_type/os_version/app_url) was removed because it simply duplicated the flat fields and is not consumed by BTGW. Retiring the flat fields depends on a future BTGW migration, tracked separately.

{
  "launch_paypal_app": true,
  "os_type": "Android",
  "os_version": "36",
  "merchant_app_return_url": "https://...",
  "app_switch_context": {
    "device_info": {
      "model": "SM-A166U",
      "memory_available_mb": 350,
      "memory_total_mb": 4096
    }
  }
}

Changes:

  • PayPalRequest: removed the now-unused NATIVE_APP_KEY, APP_URL_KEY, and OS_TYPE_VALUE constants
  • PayPalCheckoutRequest / PayPalVaultRequest: removed the nested app_switch_context.native_app block (redundant with the retained flat os_type/os_version/merchant_app_return_url keys)
  • PayPalInternalClient: injectDeviceInfo() adds device_info (model, memory_available_mb, memory_total_mb) into app_switch_context, creating the object when absent
  • Updated unit tests to verify the new request shape

Steps to Test

  1. Set enablePayPalAppSwitch = true on PayPalCheckoutRequest or PayPalVaultRequest
  2. Ensure the merchant app is configured with an App Link return URL
  3. Trigger a PayPal checkout or vault flow
  4. Intercept the request to paypal_hermes/create_payment_resource or paypal_hermes/setup_billing_agreement
  5. Confirm app_switch_context.device_info is present in the request body with model, memory_available_mb, and memory_total_mb
  6. On a device with sufficient RAM, confirm the server returns an app switch URL (PayPal app launches)
  7. On a low-RAM device, confirm the server falls back to browser checkout (Chrome Custom Tab launches)

AI Usage

Which AI Agent Was Used?

  • Claude

How was AI used?
Code generation, cross-codebase analysis (Android SDK, BTGW, backend), refactoring the request structure, on-device request/response verification, and writing tests.

Estimated AI Code Contribution

  • less than 30%
  • 30 - 60%
  • 60 - 100%

Checklist

  • Added a changelog entry
  • Tested and confirmed payment flows affected by this change are functioning as expected

Authors


Inner Source Process

Internal to PayPal contributors should fill out this section. All others can delete.

PR should follow these steps before codeowners review will begin:

  1. Comment /inner source on this PR — this will automatically add the inner source and tech lead review required labels. Open the PR in a draft state.
  2. PR should be reviewed by and approved by your team's technical lead, we do not allow LGTM reviews, there should be comments and feedback provided on all PR reviews
  3. Once the above steps are completed, comment /ready on this PR — this will automatically remove the tech lead review required label. Move the PR to ready to review.
  4. PR comments must be addressed within 24 hours, if you are unable to address within this timeframe, move the PR back to a draft state so our team knows not to review

Demo

Request body sent (key fields):

{
  "launch_paypal_app": true,
  "os_type": "Android",
  "os_version": "36",
  "merchant_app_return_url": "https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments",
  "app_switch_context": {
    "device_info": {
      "model": "SM-S936U",
      "memory_available_mb": 350,
      "memory_total_mb": 11113
    }
  }
}

token=232345522J779231L

EC flow

paypal_appswitch_test.mp4

Billing Successful app-switch

BillingWithSupportedDevice.mp4

Billing on Low Ram device not app-switching

BillingWithLowRamDevice.mp4

Inner Source Checklist

  • Added all labels to the PR
  • Provide steps to test the flows changed, if applicable in the summary
  • Demo video of the functionality, if applicable
  • All upstream dependencies are merged in and this PR can be released at any time; PRs should not be opened until this is true
  • Unit tests and builds have been run locally and pass/compile as expected

@saralvasquez saralvasquez added inner source This PR is internal to PP but external to the mobile SDK team tech lead review required labels May 18, 2026
@santugowda
santugowda force-pushed the paypal-app-switch-device-info branch from 0025eb8 to f30f4ca Compare June 9, 2026 17:29
@santugowda
santugowda marked this pull request as ready for review June 9, 2026 17:29
@santugowda
santugowda requested a review from a team June 9, 2026 17:29
@santugowda
santugowda requested a review from a team as a code owner June 9, 2026 17:29
@jaxdesmarais

Copy link
Copy Markdown
Contributor

@santugowda please follow the Inner Source Process in the PR description.

@jaxdesmarais
jaxdesmarais marked this pull request as draft June 10, 2026 14:25
santugowda added a commit to santugowda/braintree_android that referenced this pull request Jun 13, 2026
- Wrap ActivityManager.getMemoryInfo() in withContext(Dispatchers.IO) to
  avoid Binder IPC on Dispatchers.Main
- Switch getJSONObject → optJSONObject in injectDeviceInfo for defense-in-depth
  (outer guard already ensures the key exists, but this makes the function safe
  in isolation)
- Extract hardcoded "ANDROID" string to OS_TYPE_VALUE constant in PayPalRequest
- Rename MODEL_KEY → DEVICE_MODEL_KEY for consistency with sibling constants
- Promote activityManager to class field in unit tests with non-zero default
  memory values in beforeEach, removing per-test mock duplication
- Add instrumentation test for PayPalVaultRequest app switch nested structure
- Add unit test for vault path device_info injection
- Add unit test for no-op when app switch enabled but no app link present

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@santugowda
santugowda marked this pull request as ready for review June 13, 2026 02:20
@santugowda
santugowda marked this pull request as draft June 13, 2026 02:22
@santugowda
santugowda marked this pull request as ready for review June 13, 2026 18:38
@santugowda

Copy link
Copy Markdown
Contributor Author

/inner source

@santugowda
santugowda marked this pull request as draft June 26, 2026 16:49
@santugowda

santugowda commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

@anbojorquez @ayer-ribeiro could you please review this PR? Thanks!

Comment thread PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalCheckoutRequest.kt Outdated
Comment thread PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalInternalClient.kt Outdated
Comment thread PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalInternalClient.kt Outdated

@ayer-ribeiro ayer-ribeiro left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM

@santugowda
santugowda marked this pull request as ready for review June 26, 2026 21:10
@santugowda
santugowda requested a review from ayer-ribeiro June 26, 2026 21:11
@santugowda

Copy link
Copy Markdown
Contributor Author

/ready

@saralvasquez

Copy link
Copy Markdown
Contributor

In testing the PayPal checkout flows I noticed that single payment with app switch works as expected with the new fields. However, when I enable PP app switch and try to checkout with vault I get an error PayPal is currently unable to handle the request. Were you able to get the SDK demo app vault flow to work as expected?

@santugowda

santugowda commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

In testing the PayPal checkout flows I noticed that single payment with app switch works as expected with the new fields. However, when I enable PP app switch and try to checkout with vault I get an error PayPal is currently unable to handle the request. Were you able to get the SDK demo app vault flow to work as expected?

@saralvasquez This was existing issue in BT Demo App(I see this issue on Main branch as well).
So to test vault flow, I created a new BT SDK - integrated in XOSphere(PayPal - internal demo app) please check Billing Successful app-switch video in the PR description.

@saralvasquez

Copy link
Copy Markdown
Contributor

I get different behavior on the main branch vs this branch. On the main branch when the vault option is selected app switch happens and then inside the PP app the payment can't complete. On this branch when the vault option is selected there is no app switch at all and we simply get the error inside the demo app

@santugowda

Copy link
Copy Markdown
Contributor Author

I get different behavior on the main branch vs this branch. On the main branch when the vault option is selected app switch happens and then inside the PP app the payment can't complete. On this branch when the vault option is selected there is no app switch at all and we simply get the error inside the demo app

Ok, I do see the error now on this branch alone. (had to use a physical device)
But on using BT-SDK in XOSphere app - AppSwitch is working for Billing/Vault flows.

Below is the error response I get -
java.io.IOException: Unexpected code Response{protocol=h2, code=503, message=, url=https://api.braintreegateway.com/merchants/dfy45jdj3dxkmz5m/client_api/v1/paypal_hermes/setup_billing_agreement} with body {"error":"PayPal is currently unable to handle the request"}

I don't see any token or correlationID to investigate on this error on BE side - any pointers on this.
FYI via Postman the /setup_billing_agreement is working as expected. Let me know, we can have a working session if needed.

@santugowda

Copy link
Copy Markdown
Contributor Author

I get different behavior on the main branch vs this branch. On the main branch when the vault option is selected app switch happens and then inside the PP app the payment can't complete. On this branch when the vault option is selected there is no app switch at all and we simply get the error inside the demo app

Ok, I do see the error now on this branch alone. (had to use a physical device) But on using BT-SDK in XOSphere app - AppSwitch is working for Billing/Vault flows.

Below is the error response I get - java.io.IOException: Unexpected code Response{protocol=h2, code=503, message=, url=https://api.braintreegateway.com/merchants/dfy45jdj3dxkmz5m/client_api/v1/paypal_hermes/setup_billing_agreement} with body {"error":"PayPal is currently unable to handle the request"}

I don't see any token or correlationID to investigate on this error on BE side - any pointers on this. FYI via Postman the /setup_billing_agreement is working as expected. Let me know, we can have a working session if needed.

@saralvasquez Found the issue - it was missing fields in the request body. Will push this fix in a bit.

@santugowda

Copy link
Copy Markdown
Contributor Author

I get different behavior on the main branch vs this branch. On the main branch when the vault option is selected app switch happens and then inside the PP app the payment can't complete. On this branch when the vault option is selected there is no app switch at all and we simply get the error inside the demo app

Ok, I do see the error now on this branch alone. (had to use a physical device) But on using BT-SDK in XOSphere app - AppSwitch is working for Billing/Vault flows.
Below is the error response I get - java.io.IOException: Unexpected code Response{protocol=h2, code=503, message=, url=https://api.braintreegateway.com/merchants/dfy45jdj3dxkmz5m/client_api/v1/paypal_hermes/setup_billing_agreement} with body {"error":"PayPal is currently unable to handle the request"}
I don't see any token or correlationID to investigate on this error on BE side - any pointers on this. FYI via Postman the /setup_billing_agreement is working as expected. Let me know, we can have a working session if needed.

@saralvasquez Found the issue - it was missing fields in the request body. Will push this fix in a bit.

Fixed this in the lastest commit i.e. #1600 434accf
Attaching video for reference as well, please review when you get a chance.

BillingFlow_BTDemoApp.mp4

@noguier
noguier removed the request for review from ayer-ribeiro July 8, 2026 18:25
@saralvasquez

Copy link
Copy Markdown
Contributor

I noticed the same behavior when re-testing just now

@santugowda

santugowda commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

ok, should be a error on BE. It's the same experience on main branch as well.
Let me debug this flow at my end.

@santugowda

santugowda commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Single Payment is working now.

SinglePaymentInProd.mp4

Slack thread has the info about yesterday's error, yet to close out on the root cause.

.build()
}

private fun injectDeviceInfo(context: Context, requestBody: String): String {

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.

nit: will defer to android folks but append may be a better name vs inject

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.

Yeah I agree here. Append is preferable

@jaxdesmarais

Copy link
Copy Markdown
Contributor

@santugowda instrumentation tests are failing here but passing on other branches. Is this something you want to take a look at?

@saralvasquez saralvasquez left a comment

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.

I've tested both checkout and vault and both work as expected. I just have a couple nits about comments and I'd like to see the change Jax mentioned about inject vs append. I'll approve once we have a clean build besides the known security check failure

santugowda added a commit to santugowda/braintree_android that referenced this pull request Jul 14, 2026
…edundant test comments

Address review feedback on PR braintree#1600:
- Rename injectDeviceInfo -> appendDeviceInfo per reviewer preference
- Remove unnecessary explanatory comments in PayPal request unit tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@santugowda

Copy link
Copy Markdown
Contributor Author

Pushed the changes addressing all the above nit comments, append refactor and instrumentation changes.

@saralvasquez saralvasquez left a comment

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.

Once we get a clean build I'd say this is ready to go in

@santugowda

Copy link
Copy Markdown
Contributor Author

CI is 🟢 except for the known Dependency Review failure.

@jaxdesmarais

Copy link
Copy Markdown
Contributor

@santugowda can you please update with the latest from main and move the changelog entry under a new unreleased section?

Santhosh Bhutegowda and others added 10 commits July 22, 2026 10:02
… eligibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Wrap ActivityManager.getMemoryInfo() in withContext(Dispatchers.IO) to
  avoid Binder IPC on Dispatchers.Main
- Switch getJSONObject → optJSONObject in injectDeviceInfo for defense-in-depth
  (outer guard already ensures the key exists, but this makes the function safe
  in isolation)
- Extract hardcoded "ANDROID" string to OS_TYPE_VALUE constant in PayPalRequest
- Rename MODEL_KEY → DEVICE_MODEL_KEY for consistency with sibling constants
- Promote activityManager to class field in unit tests with non-zero default
  memory values in beforeEach, removing per-test mock duplication
- Add instrumentation test for PayPalVaultRequest app switch nested structure
- Add unit test for vault path device_info injection
- Add unit test for no-op when app switch enabled but no app link present

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eInfo

ActivityManager.getMemoryInfo(), Build.MODEL, and JSON operations are
not I/O-bound; no dispatcher switch is needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The gateway still requires the top-level os_version, os_type, and
merchant_app_return_url fields for PayPal app switch. Sending only the
nested app_switch_context caused a 503 "PayPal is currently unable to
handle the request". Restore the flat fields while keeping the nested
app_switch_context (native_app + device_info).

Update PayPalCheckoutRequest/PayPalVaultRequest unit tests to assert
both the flat and nested app switch parameters.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Use a safe cast for the ACTIVITY_SERVICE lookup and return the original
request body if the service is unavailable, so device_info injection
never crashes a merchant's checkout on the request path.

Add a unit test covering the null-service fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the nested native_app block (os_type/os_version/app_url) from the
PayPal app-switch request. Those values duplicate the flat top-level
fields (os_type/os_version/merchant_app_return_url) that BTGW reads, so the
nested copy was redundant. app_switch_context now carries only device_info.

injectDeviceInfo now creates app_switch_context when absent, since the
request no longer builds it. Updated unit tests to expect the new shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…edundant test comments

Address review feedback on PR braintree#1600:
- Rename injectDeviceInfo -> appendDeviceInfo per reviewer preference
- Remove unnecessary explanatory comments in PayPal request unit tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…text.native_app

createRequestBody no longer emits the nested app_switch_context.native_app
structure, so the instrumentation tests now assert the flat app switch fields
(os_type, os_version, merchant_app_return_url) and that app_switch_context is
absent. Fixes the failing PayPal instrumentation tests on this branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@santugowda
santugowda force-pushed the paypal-app-switch-device-info branch from 1f34dd6 to 0d1d28a Compare July 22, 2026 15:30
@santugowda

Copy link
Copy Markdown
Contributor Author

@santugowda can you please update with the latest from main and move the changelog entry under a new unreleased section?

@jaxdesmarais - rebased and updated Changelog.md. Hoping to get this merged today.

@jaxdesmarais
jaxdesmarais merged commit 6c32508 into braintree:main Jul 22, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

inner source This PR is internal to PP but external to the mobile SDK team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants