diff --git a/make_post_sell/request_methods.py b/make_post_sell/request_methods.py
index ec4e5dc..557187c 100644
--- a/make_post_sell/request_methods.py
+++ b/make_post_sell/request_methods.py
@@ -506,3 +506,20 @@ def includeme(config):
config.add_request_method(
add_has_doge_refund_address, "has_doge_refund_address", reify=True
)
+
+ def flash_once(request, message, level="info", queue=""):
+ """request.session.flash() that skips the append if (message,
+ level) is already in the queue. Use for messages emitted on
+ polling endpoints / redirect loops where the same status
+ otherwise piles up dozens of copies in the flash queue.
+ Pyramid's flash queue is a list; we peek first and bail on
+ duplicates.
+ """
+ for queued in request.session.peek_flash(queue):
+ # queued is the (message, level) tuple as previously
+ # flashed (or just the message if level wasn't set).
+ if queued == (message, level):
+ return
+ request.session.flash((message, level), queue=queue)
+
+ config.add_request_method(flash_once, "flash_once")
diff --git a/make_post_sell/templates/snippets/flash-alerts.j2 b/make_post_sell/templates/snippets/flash-alerts.j2
index b141dec..070578e 100644
--- a/make_post_sell/templates/snippets/flash-alerts.j2
+++ b/make_post_sell/templates/snippets/flash-alerts.j2
@@ -5,12 +5,21 @@
-{%- if request.session.peek_flash() -%}
-{%- for message, level in request.session.pop_flash() %}
+{#- Dedupe identical (message, level) pairs queued during this session.
+ Redirect loops (e.g. the crypto-payment status poll flashing
+ "Payment received! Waiting for confirmations..." on every refresh)
+ could pile up dozens of copies of the same alert. Pop everything off
+ the queue once, then render each unique pair only the first time
+ it's seen. -#}
+{%- set _queued = request.session.pop_flash() -%}
+{%- set _seen = namespace(items=[]) -%}
+{%- for message, level in _queued %}
+{%- if (message, level) not in _seen.items %}
+{%- if _seen.items.append((message, level)) %}{% endif %}
+{%- endif -%}
{%- endfor %}
-{%- endif %}
diff --git a/make_post_sell/views/crypto.py b/make_post_sell/views/crypto.py
index 607204d..ba30df2 100644
--- a/make_post_sell/views/crypto.py
+++ b/make_post_sell/views/crypto.py
@@ -815,11 +815,14 @@ def crypto_xmr_status(request):
if not crypto_payment:
return HTTPBadRequest("payment not found")
- # Flash messages for status (without storing in session)
+ # Flash messages for status. flash_once dedupes at the queue
+ # level — this endpoint is hit by the front-end status poll on
+ # every refresh, so a plain flash() would stack the same alert
+ # dozens of times.
current_status = crypto_payment.status
if current_status == "received":
- request.session.flash(
- ("Payment received! Waiting for confirmations...", "info")
+ request.flash_once(
+ "Payment received! Waiting for confirmations...", "info"
)
elif current_status == "confirmed":
# Check if it's a digital product by looking at line items
@@ -831,27 +834,27 @@ def crypto_xmr_status(request):
break
if has_digital:
- request.session.flash(
- ("Purchase completed! Download your files now!", "success")
+ request.flash_once(
+ "Purchase completed! Download your files now!", "success"
)
else:
- request.session.flash(("Purchase completed!", "success"))
+ request.flash_once("Purchase completed!", "success")
elif current_status == "confirmed-overpay":
- request.session.flash(
- ("Purchase completed! Overpayment will be refunded.", "success")
+ request.flash_once(
+ "Purchase completed! Overpayment will be refunded.", "success"
)
elif current_status == "expired":
- request.session.flash(("Payment quote expired.", "warning"))
+ request.flash_once("Payment quote expired.", "warning")
elif current_status in [
"expired-refunded",
"underpaid-refunded",
"out-of-stock-refunded",
]:
- request.session.flash(
- ("Refund initiated. Please allow time for processing.", "info")
+ request.flash_once(
+ "Refund initiated. Please allow time for processing.", "info"
)
elif current_status == "cancelled":
- request.session.flash(("Payment cancelled.", "info"))
+ request.flash_once("Payment cancelled.", "info")
payload = {
"payment_id": str(crypto_payment.id),
@@ -904,11 +907,12 @@ def crypto_doge_status(request):
if not crypto_payment:
return HTTPBadRequest("payment not found")
- # Flash messages for status (without storing in session)
+ # Flash messages for status. flash_once dedupes the queue so the
+ # poll endpoint doesn't pile up identical alerts on every refresh.
current_status = crypto_payment.status
if current_status == "received":
- request.session.flash(
- ("Payment received! Waiting for confirmations...", "info")
+ request.flash_once(
+ "Payment received! Waiting for confirmations...", "info"
)
elif current_status == "confirmed":
# Check if it's a digital product by looking at line items
@@ -920,27 +924,27 @@ def crypto_doge_status(request):
break
if has_digital:
- request.session.flash(
- ("Purchase completed! Download your files now!", "success")
+ request.flash_once(
+ "Purchase completed! Download your files now!", "success"
)
else:
- request.session.flash(("Purchase completed!", "success"))
+ request.flash_once("Purchase completed!", "success")
elif current_status == "confirmed-overpay":
- request.session.flash(
- ("Purchase completed! Overpayment will be refunded.", "success")
+ request.flash_once(
+ "Purchase completed! Overpayment will be refunded.", "success"
)
elif current_status == "expired":
- request.session.flash(("Payment quote expired.", "warning"))
+ request.flash_once("Payment quote expired.", "warning")
elif current_status in [
"expired-refunded",
"underpaid-refunded",
"out-of-stock-refunded",
]:
- request.session.flash(
- ("Refund initiated. Please allow time for processing.", "info")
+ request.flash_once(
+ "Refund initiated. Please allow time for processing.", "info"
)
elif current_status == "cancelled":
- request.session.flash(("Payment cancelled.", "info"))
+ request.flash_once("Payment cancelled.", "info")
payload = {
"payment_id": str(crypto_payment.id),