fix: offer page — seller sees a shareable pay link, buyer pay button stands out

When an offer is accepted but unpaid, the seller's view now has an
"Awaiting payment" block: the buyer's name (linked to their profile), the
agreed amount, and the offer URL pre-filled in a read-only input plus a
"Copy link" button (capability-driven — the input is selectable for
manual copy when JS is absent or clipboard API is blocked). The notice
banner also says "waiting on the buyer to pay $X" in the seller's view.

The buyer's pay-now button is now mps-button-green so it's unmistakable
as the call-to-action.

Tests: TestOfferCheckout gains two render tests — seller sees the
awaiting-payment block + URL but not a pay-now form; buyer sees the
$75.00 pay-now form but not the awaiting block. 9 in class pass.
This commit is contained in:
russell@unturf.com 2026-05-13 06:28:18 -04:00
parent 8b40e90d58
commit ebeb8afa87
No known key found for this signature in database
3 changed files with 58 additions and 8 deletions

View file

@ -1635,6 +1635,22 @@ div.edit-page > section.edit-card-full {
border-radius: var(--radius-md, 8px);
}
/* Accepted-offer pay-link row shown to the seller: an input pre-filled
with the buyer's pay URL plus a copy-to-clipboard button. */
.offer-pay-link-row {
display: grid;
grid-template-columns: 1fr auto;
gap: var(--space-2, 8px);
margin-top: var(--space-3, 12px);
}
.offer-pay-link {
width: 100%;
box-sizing: border-box;
padding: var(--space-2, 8px) var(--space-3, 12px);
font-family: var(--font-mono, ui-monospace, monospace);
font-size: var(--text-sm, 0.875rem);
}
/* Design-system settings form: stacked label + control + hint per
field, two-up grid on wider viewports. Grid only no flex. */
.settings-form {

View file

@ -46,7 +46,7 @@
</div>
{% elif is_accepted %}
<div class="alert alert-success offer-state-notice" name="alert">
<p class="alert-message">Offer accepted{% if request.user and request.user.uuid_str == buyer_id %} &mdash; pay now to complete your purchase{% endif %}.</p>
<p class="alert-message">Offer accepted{% if request.user and request.user.uuid_str == buyer_id %} &mdash; pay now to complete your purchase{% elif not is_paid %} &mdash; waiting on the buyer to pay ${{ "%.2f"|format(current_amount) }}{% endif %}.</p>
</div>
{% elif can_act %}
<div class="alert alert-info offer-state-notice" name="alert">
@ -97,13 +97,27 @@
</section>
{% endif %}
{# Pay-now CTA when the offer is accepted and the viewer is the buyer. #}
{% if is_accepted and request.user and request.user.uuid_str == buyer_id %}
<section class="well">
<form method="post" action="/o/{{ id }}/checkout">
<input type="submit" class="mps-submit" value="Pay ${{ "%.2f"|format(current_amount) }} now" />
</form>
</section>
{# Accepted-but-not-paid: the buyer gets the pay-now CTA; the seller
(or shop editor) gets a shareable link to send the buyer, since the
seller cannot pay on the buyer's behalf. #}
{% if is_accepted and not is_paid %}
{% if request.user and request.user.uuid_str == buyer_id %}
<section class="well offer-pay-cta">
<form method="post" action="/o/{{ id }}/checkout">
<input type="submit" class="mps-submit mps-button-green" value="Pay ${{ "%.2f"|format(current_amount) }} now" />
</form>
</section>
{% elif request.user %}
<section class="well offer-await-pay">
<h3 class="type-title">Awaiting payment</h3>
<p>You accepted <a href="/profile/{{ buyer_handle }}?shop={{ shop_id }}">{{ buyer_name }}</a>&rsquo;s offer at <strong>${{ "%.2f"|format(current_amount) }}</strong>. They need to sign in and pay from this same page &mdash; send them the link:</p>
<div class="offer-pay-link-row">
<input type="text" readonly class="offer-pay-link" value="{{ request.scheme }}://{{ request.host }}/o/{{ id }}" onclick="this.select();" />
<button type="button" class="mps-button-small js-only"
onclick="if(navigator.clipboard){navigator.clipboard.writeText(this.previousElementSibling.value);this.textContent='Copied!';}else{this.previousElementSibling.select();}">Copy link</button>
</div>
</section>
{% endif %}
{% endif %}
<section class="offer-timeline well">

View file

@ -6810,6 +6810,26 @@ class TestOfferCheckout(_AuthenticatedBase):
# ...and the redirect lands on that cart by id.
self.assertIn(f"/cart/{match[0].uuid_str}", res.location)
def test_accepted_offer_seller_sees_pay_link_to_share(self):
offer_id = self._accepted_offer()
# _accepted_offer leaves user1 (seller / shop owner) logged in.
body = self.testapp.get(f"/o/{offer_id}", status=200).body.decode()
self.assertIn("Awaiting payment", body)
self.assertIn("offer-pay-link", body)
self.assertIn("waiting on the buyer to pay $75.00", body)
# Seller never sees the buyer's pay-now form (they cannot pay).
self.assertNotIn(f"/o/{offer_id}/checkout", body)
def test_accepted_offer_buyer_sees_pay_now(self):
offer_id = self._accepted_offer()
self.testapp.get("/log-out")
self.log_in_user(self.user2_creds)
body = self.testapp.get(f"/o/{offer_id}", status=200).body.decode()
self.assertIn(f"/o/{offer_id}/checkout", body)
self.assertIn("Pay $75.00 now", body)
# Buyer view does not show the seller's awaiting-payment block.
self.assertNotIn("Awaiting payment", body)
def test_non_buyer_checkout_blocked(self):
offer_id = self._accepted_offer()
# Logged in as user1 (seller). Should be blocked.