Skip to content

Braintree Apple Pay: AbstractButton constructor eagerly calls Braintree API for a client token before checking isActive() (unlike the Google Pay equivalent in the same module) #41113

Description

@sevka

Preconditions and environment

  1. Magento Open Source 2.4.9
  2. paypal/module-braintree-core 4.8.0 (braintree/braintree_php 6.32.0)
  3. Braintree payment method enabled, with Apple Pay enabled:
    Stores > Configuration > Sales > Payment Methods > Braintree > Apple Pay > Enable Apple Pay = Yes
  4. ApplePay button on minicart is disabled by default

Steps to reproduce

Method A — direct, on any Product Page (no dependency on any other module):

  1. With the above config, open any product detail page on the storefront.
  2. Watch the PHP call stack (Xdebug breakpoint, log, or APM trace) on \Braintree\Http::_doRequest, or watch outbound HTTPS traffic to the Braintree gateway during that single page request.
  3. Observe a live Braintree API request firing synchronously as part of building the PDP response, on this call stack — even when "Display Apple Pay button on Product Page" would end up evaluating to false, or the product/quote wouldn't otherwise qualify:
Braintree\Http::_doRequest
PayPal\Braintree\Model\ApplePay\Ui\ConfigProvider::getClientToken
PayPal\Braintree\Model\ApplePay\Auth::get
PayPal\Braintree\Block\ApplePay\AbstractButton::__construct
PayPal\Braintree\Block\ApplePay\Shortcut\ProductPage (extends Shortcut\Button extends AbstractButton)
(block declared statically in vendor/paypal/module-braintree-core/view/frontend/layout/catalog_product_view.xml)
  1. This block is declared directly in layout XML (catalog_product_view.xml, under product.info.addtocart), so it is constructed on every single PDP view, unconditionally — the isActive() check (which factors in displayApplePayButtonOnPdp()) only runs afterwards, inside _toHtml(), by which point the Braintree call has already happened.

Method B — reproduce the same bug via the mini-cart shortcut-button path, from any page (e.g. a category/listing page), using only the browser console:

  1. On any storefront page, open the browser console and run:
require(['Magento_Customer/js/customer-data'], function (customerData) {
    customerData.reload(['cart'], true);
});

(Note: it must be the cart section — reloading customer alone does not exercise this code path.)

  1. This triggers Magento\Checkout\CustomerData\Cart::getSectionData() → Magento\Catalog\Block\ShortcutButtons::toHtml() → the shortcut_buttons_container event → PayPal\Braintree\Observer\AddMethodsShortcut, which constructs PayPal\Braintree\Block\ApplePay\Shortcut\Button (also extends AbstractButton) whenever Apple Pay is active in config.
  2. Same result: a live Braintree API call fires from AbstractButton::__construct(), regardless of whether displayApplePayButtonOnCart() would make the button actually render.

Both Shortcut\Button (mini-cart/cart) and Shortcut\ProductPage (PDP) extend the same PayPal\Braintree\Block\ApplePay\AbstractButton, so both manifestations share the same root cause.

For comparison: PayPal\Braintree\Block\GooglePay\AbstractButton — used by the equivalent Google Pay blocks in the exact same layout files — does not have this problem in either context. Its constructor only stores the injected Auth $auth object; getClientToken() / getMerchantId() / etc. resolve it lazily, and are only ever invoked from the .phtml template, which is only reached once _toHtml() has already confirmed isActive() is true.

Expected result

Constructing an Apple Pay button block (Shortcut\Button or Shortcut\ProductPage) should not, by itself, trigger a network call to Braintree. Like the Google Pay implementation in the same module, the client token / auth payload should be resolved lazily — only once isActive() has confirmed the button will actually render.

Actual result

PayPal\Braintree\Block\ApplePay\AbstractButton::__construct() (vendor/paypal/module-braintree-core/Block/ApplePay/AbstractButton.php, line 68) unconditionally executes:

$this->auth = $auth->get();
Auth::get() calls ConfigProvider::getClientToken(), which calls $this->adapter->generate($params) — a synchronous, live HTTP request to Braintree's API to generate a client token. This runs at construction time, before isActive() is ever evaluated (isActive(), which also factors in the display_on_pdp / display_on_cart config toggles, is only checked later, inside _toHtml()).

Because this block is instantiated:

unconditionally on every Product Page view (static layout declaration in catalog_product_view.xml), and
on every recomputation of the cart customer-data section (mini-cart / cart sidebar), whenever Apple Pay is enabled,
this results in an extra, avoidable live Braintree API request on effectively every PDP view, and potentially on other pages too depending on how often the cart section is recomputed — regardless of whether the button ends up rendering at all.

Additional information

  • ConfigProvider::getClientToken() does cache the token on the instance property for the lifetime of that instance, so the practical impact is "one extra live HTTP call to Braintree per HTTP request" rather than per block instantiation — but that's still an avoidable outbound call to a third-party payment gateway added to the response time of every single PDP request, plus unnecessary load on the merchant's Braintree API usage.
  • Suggested fix: mirror the Google Pay implementation — store the injected Auth $auth in the constructor without calling it, and defer resolving it to the point where getClientToken() / getMerchantName() / getActionSuccess() / isCustomerLoggedIn() are actually called (i.e. from the template, after _toHtml() has already gated on isActive()).
  • Severity: performance/efficiency, not a functional break — but very broad blast radius, since it deterministically affects every single Product Page view (and potentially more, via the mini-cart path) for any merchant with Apple Pay enabled.

Release note

No response

Triage and priority

  • Severity: S0 - Affects critical data or functionality and leaves users without workaround.
  • Severity: S1 - Affects critical data or functionality and forces users to employ a workaround.
  • Severity: S2 - Affects non-critical data or functionality and forces users to employ a workaround.
  • Severity: S3 - Affects non-critical data or functionality and does not force users to employ a workaround.
  • Severity: S4 - Affects aesthetics, professional look and feel, “quality” or “usability”.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status
    Ready for Confirmation

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions