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 %}

{{message|safe}}

+{%- 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),