@php /** * SAVED PAYMENT METHODS — the one manager in the product. * * Rendered on the customer's payment page and inside the dashboard account page, because a diner * keeping a card for ordering and an owner keeping one for placements are the same thing and must * not become two screens that drift apart. The forms post to /account/payment/*, which any signed-in * role may reach and which scopes every row to the signed-in user. * * THE RULES LIVE IN THE MODEL, NOT HERE. Whether a method can be removed, whether it still works, * which one leads the list — all of that is PaymentMethod's, and this asks rather than restates it. * A screen that keeps its own copy of a rule is a screen that will disagree with the server. * * @var array $methods this user's methods, best first * @var array $savable Stripe payment_method_types this site can keep on file */ $methods = $methods ?? []; $savable = $savable ?? []; $cardModel = new \App\Models\PaymentMethod(); $hasPaypal = false; foreach ($methods as $m) { if ((string) ($m['gateway'] ?? '') === 'paypal') { $hasPaypal = true; } } // A method belongs in the add list only if it can be charged again later with the payer absent. $canCard = $savable !== []; $canPaypal = \App\Services\Payment::isEnabled('paypal') && !$hasPaypal; @endphp @if (count($methods) > 0)
@php /* The shared card anatomy the rest of this page uses — a .qm-card-head over a .qm-card-body — so these sections sit on the same rhythm as Profile details rather than inventing their own spacing. */ @endphp

{{ t_raw('account.saved_cards_title') }}

@php /* ONE LINE PER METHOD, and only what identifies it. A brand, what distinguishes this one from the next, and when it runs out. The controls sit at the end of the line rather than under it, so five methods read as a list instead of five competing panels. */ @endphp @partial('payment-method-rows', ['methods' => $methods])
@endif

{{ t_raw('account.add_method_title') }}

@if ($canCard || $canPaypal) @php /* A NAME AND A MARK IS THE WHOLE ROW. Explaining each method in a sentence underneath tripled the height of this list and told the reader what the name already told them; anything genuinely method-specific belongs in the step that follows, where it applies. */ @endphp
@foreach ($savable as $pmType) @php $row = payment_method_row($pmType); @endphp @endforeach @if ($canPaypal) @php /* PayPal is a link because connecting one is a journey to PayPal and back; it changes nothing here until the payer returns having agreed. */ @endphp {!! card_brand_mark('paypal', 20) !!} {{ t_raw('account.add_method_paypal') }} @endif
@else

{!! $hasPaypal && !\App\Services\Payment::isEnabled('stripe') ? t('account.add_method_all_added') : t('account.add_card_unavailable') !!}

@endif
@if ($canCard) @php /* THE ENTRY STEP IS A DIALOG OVER THE PAGE, not a section that replaces the list. Adding a method is a short errand: you come from the list and you go back to it, and swapping the page out underneath made a two-field job feel like a departure. It uses body.modal-open, which the dashboard chrome already understands. */ @endphp @endif @if ($canCard) @php /* WHERE A STRAIGHT-THROUGH METHOD MOUNTS. Cash App and Amazon Pay ask this page for nothing — the gateway takes over with a QR to scan or a page to sign into. Opening our own dialog first put two boxes on screen, one of which said only "you are about to see a box". The element still has to exist for the gateway to be confirmed against, so it lives here, off-screen, and the gateway's own window is the only thing the reader meets. */ @endphp @endif @if ($savable !== []) @php /* Stripe.js and the mount script travel WITH the manager, so wherever it is rendered the fields work. Loaded only when this site can save something — a site with no card gateway never contacts Stripe at all. The client secret is fetched per method when one is picked, so nothing is minted for a page that is only glanced at. */ @endphp @endif