fix: single PayPal button on checkout; Stripe stays visible

Cart checkout had two payment-UX defects:

1. PayPal Smart Buttons rendered three buttons by default: the yellow
   PayPal button, "Pay Later" financing, and "Debit or Credit Card"
   (PayPal-branded card flow). For MPS, credit-card checkout goes
   through Stripe — the PayPal card button is redundant and pushes a
   competing flow into the same panel.
   Fix: append `disable-funding=paylater,card` to the PayPal SDK URL
   so only the single PayPal Smart Button renders.

2. Stripe lost visibility on the right column when the buyer had no
   card on file yet. The "Add a credit card payment method" CTA only
   appeared in the left panel; next to PayPal + crypto on the right,
   the credit-card path looked unsupported.
   Fix: when stripe is enabled and the shop is stripe-ready but the
   buyer has no active card, surface a "Pay with Credit Card — Add
   Card" CTA in the right column, alongside the PayPal Smart Button.
   /billing then runs the existing card-add flow.

Tests:
- test_cart_checkout_for_shop asserts "Pay with Credit Card" renders
  in the right column when no card is on file.
- test_paypal_smart_buttons_collapsed_to_one is a template-grep
  asserting the SDK URL carries disable-funding=paylater,card.
This commit is contained in:
russell@unturf.com 2026-05-13 10:20:34 -04:00
parent 9c68971971
commit 2e1bde37d3
No known key found for this signature in database
2 changed files with 39 additions and 4 deletions

View file

@ -54,6 +54,11 @@
{% include "snippets/csrf.j2" %}
<button type="submit" class="cart-checkout-button mps-button">Yes, Confirm Checkout with Active Credit Card</button>
</form>
{% elif stripe_enabled and request.shop and request.shop.is_stripe_ready and cart.requires_payment %}
{# No card on file yet — give the buyer a prominent CTA on the right
column so Stripe stays visible alongside PayPal and crypto. /billing
adds the card; the buyer returns here to confirm checkout. #}
<a href="/billing" class="cart-checkout-button mps-button">Pay with Credit Card &mdash; Add Card</a>
{% endif %}
{# PayPal checkout button #}
@ -86,7 +91,10 @@
{% endif %}
<div id="paypal-button-container"></div>
<script src="https://www.paypal.com/sdk/js?client-id={{ request.shop.paypal_client_id }}&currency=USD"></script>
{# disable-funding=paylater,card hides the secondary "Pay Later" and
"Debit or Credit Card" Smart Buttons so only the single yellow
PayPal button renders. Credit-card checkout flows through Stripe. #}
<script src="https://www.paypal.com/sdk/js?client-id={{ request.shop.paypal_client_id }}&currency=USD&disable-funding=paylater,card"></script>
<script>
// Store all order IDs for multi-shop support
var allPayPalOrderIds = [];

View file

@ -629,9 +629,13 @@ class AuthenticatedFunctionalTests(_AuthenticatedBase):
# With multiple payment methods enabled (Stripe + PayPal), checkout
# renders directly instead of redirecting to /billing for Stripe setup
self.assertEqual(200, res_csrf_checkout.status_int)
self.assertIn(
"Please confirm your order.", res_csrf_checkout.body.decode()
)
checkout_body = res_csrf_checkout.body.decode()
self.assertIn("Please confirm your order.", checkout_body)
# Stripe stays a visible option on the right column even when the
# buyer has no card on file yet — otherwise the credit-card path
# is buried in the left panel and easy to miss next to PayPal /
# crypto CTAs.
self.assertIn("Pay with Credit Card", checkout_body)
def test_cart_checkout_logic_with_none_stripe_user_shop(self):
"""Test the specific logic that was causing AttributeError in cart checkout.
@ -1750,6 +1754,29 @@ class AuthenticatedFunctionalTests(_AuthenticatedBase):
# Should fail with error
self.assertIn(res.status_int, [400, 404, 500])
def test_paypal_smart_buttons_collapsed_to_one(self):
"""PayPal SDK URL on cart_checkout disables paylater + card
funding so only the single yellow PayPal Smart Button renders.
Without disable-funding, the SDK renders three buttons: PayPal,
Pay Later, and Debit or Credit Card. Credit-card checkout flows
through Stripe in MPS the bottom PayPal-branded card button
is redundant and confusing.
Template-level assertion: the SDK URL in cart_checkout.j2 carries
disable-funding=paylater,card. A full render test was flaky against
the test harness (transaction boundaries vs. session-cart fixture);
this single grep on the file is the smallest stable check.
"""
import os
template_path = os.path.join(
os.path.dirname(__file__), "..", "templates", "cart_checkout.j2",
)
with open(template_path) as fh:
content = fh.read()
self.assertIn("paypal.com/sdk/js", content)
self.assertIn("disable-funding=paylater,card", content)
@mock.patch("smtplib.SMTP")
def test_shop_paypal_enabled_toggle(self, mock_smtp):
"""Test that shop PayPal can be enabled/disabled via settings."""