Files changed (2) hide show
  1. app.py +14 -4
  2. managed_auth.py +24 -8
app.py CHANGED
@@ -815,6 +815,13 @@ def _process_pending_cancel() -> None:
815
  st.rerun()
816
 
817
 
 
 
 
 
 
 
 
818
  def _process_pending_resume() -> None:
819
  task_id = str(st.session_state.get("sp_pending_resume_task_id") or "").strip()
820
  if not task_id:
@@ -1728,8 +1735,9 @@ def render_workspace_controls(user):
1728
  entitlement_exhausted = bool(product_ui_ready and selected_remaining is not None and selected_remaining <= 0)
1729
  if entitlement_exhausted:
1730
  st.warning(tr(lang_code, "product_entitlement_required"))
1731
- if st.button(tr(lang_code, "payment_open"), key="btn_open_payment_for_selected_tier"):
1732
- render_payment_purchase(lang_code)
 
1733
  can_start = bool(
1734
  preflight.get("can_start_generation")
1735
  and product_ui_ready
@@ -1792,11 +1800,13 @@ if isinstance(_notice, dict) and _notice.get("message"):
1792
  _process_pending_cancel()
1793
  _process_pending_resume()
1794
  _process_pending_launch(lang_code)
 
1795
 
1796
  if st.session_state.get("sp_payment_required"):
1797
  st.warning(tr(lang_code, "product_entitlement_required"))
1798
- if st.button(tr(lang_code, "payment_open"), key="btn_open_payment_after_rejection"):
1799
- render_payment_purchase(lang_code)
 
1800
 
1801
  # R3.2.4 keeps the approved task-monitor location. Active progress is updated
1802
  # inside the browser by read-only edge polling; Streamlit reruns only on a
 
815
  st.rerun()
816
 
817
 
818
+ def _process_pending_payment_dialog(lang_code: str) -> None:
819
+ """Reopen purchase dialog until the initial browser bridge creates its trusted order."""
820
+ if not bool(st.session_state.get("sp_payment_dialog_pending")):
821
+ return
822
+ render_payment_purchase(lang_code)
823
+
824
+
825
  def _process_pending_resume() -> None:
826
  task_id = str(st.session_state.get("sp_pending_resume_task_id") or "").strip()
827
  if not task_id:
 
1735
  entitlement_exhausted = bool(product_ui_ready and selected_remaining is not None and selected_remaining <= 0)
1736
  if entitlement_exhausted:
1737
  st.warning(tr(lang_code, "product_entitlement_required"))
1738
+ if st.button(f"💳 {tr(lang_code, 'payment_open')}", key="btn_open_payment_for_selected_tier"):
1739
+ st.session_state["sp_payment_dialog_pending"] = True
1740
+ st.rerun()
1741
  can_start = bool(
1742
  preflight.get("can_start_generation")
1743
  and product_ui_ready
 
1800
  _process_pending_cancel()
1801
  _process_pending_resume()
1802
  _process_pending_launch(lang_code)
1803
+ _process_pending_payment_dialog(lang_code)
1804
 
1805
  if st.session_state.get("sp_payment_required"):
1806
  st.warning(tr(lang_code, "product_entitlement_required"))
1807
+ if st.button(f"💳 {tr(lang_code, 'payment_open')}", key="btn_open_payment_after_rejection"):
1808
+ st.session_state["sp_payment_dialog_pending"] = True
1809
+ st.rerun()
1810
 
1811
  # R3.2.4 keeps the approved task-monitor location. Active progress is updated
1812
  # inside the browser by read-only edge polling; Streamlit reruns only on a
managed_auth.py CHANGED
@@ -141,6 +141,7 @@ def _clear_generation_session_state() -> None:
141
  "up_asq_data", "sp_payment_required", "sp_payment_order",
142
  "sp_payment_request_id", "sp_payment_package_sku",
143
  "sp_payment_channel", "sp_payment_order_channel",
 
144
  ):
145
  st.session_state.pop(key, None)
146
 
@@ -150,6 +151,7 @@ def _clear_session() -> None:
150
  "sp_session_expires_at", "sp_user",
151
  "sp_last_verified_at", "sp_edge_assertion", "sp_auth_api_origin",
152
  "sp_product_ui_projection", "sp_product_tier",
 
153
  ):
154
  st.session_state.pop(key, None)
155
 
@@ -322,14 +324,22 @@ def get_product_ui_projection() -> dict[str, Any]:
322
 
323
 
324
  def get_account_snapshot(*, block: bool = False) -> dict[str, Any] | None:
325
- bucket = int(time.time() // 30)
 
 
 
 
326
  revision = int(st.session_state.get("sp_account_revision") or 0)
327
- command_id = f"account:{revision}:{bucket}"
 
 
 
328
  result = _edge_call("account", command_id, {}, key_prefix="sp_account")
329
  if result is None:
330
  if block:
331
  st.stop()
332
  return None
 
333
  if not result.get("ok"):
334
  if block:
335
  _raise_edge_error(result, "account_query_failed")
@@ -651,6 +661,12 @@ def _render_payment_purchase_body(language: str) -> None:
651
  st.session_state["sp_product_ui_projection"] = _normalize_product_ui_projection(projected)
652
 
653
  order = st.session_state.get("sp_payment_order") or {}
 
 
 
 
 
 
654
  qr_code = str(order.get("qr_code") or "").strip()
655
  if qr_code:
656
  payment_provider = str(order.get("payment_provider") or "").strip().upper()
@@ -827,8 +843,8 @@ def render_logged_in_account(
827
  padding: 0.48rem clamp(0.55rem, 0.85vw, 0.9rem);
828
  border: 1px solid rgba(148, 163, 184, 0.30);
829
  border-radius: 13px;
830
- background: rgba(30, 41, 59, 0.58);
831
- color: var(--text-color, #f3f4f6);
832
  font-size: clamp(0.76rem, 0.84vw, 0.92rem);
833
  font-weight: 500;
834
  line-height: 1.25;
@@ -866,8 +882,8 @@ def render_logged_in_account(
866
  padding: 0.45rem 0.72rem;
867
  border: 1px solid rgba(148, 163, 184, 0.30);
868
  border-radius: 0.5rem;
869
- background: transparent;
870
- color: var(--text-color, #f3f4f6) !important;
871
  text-decoration: none !important;
872
  white-space: nowrap !important;
873
  overflow-wrap: normal !important;
@@ -882,8 +898,8 @@ def render_logged_in_account(
882
 
883
  .sp-logout-link:hover {
884
  border-color: rgba(255, 75, 75, 0.78);
885
- background: rgba(255, 255, 255, 0.035);
886
- color: var(--text-color, #f3f4f6) !important;
887
  }
888
 
889
  .sp-logout-link:focus-visible {
 
141
  "up_asq_data", "sp_payment_required", "sp_payment_order",
142
  "sp_payment_request_id", "sp_payment_package_sku",
143
  "sp_payment_channel", "sp_payment_order_channel",
144
+ "sp_payment_dialog_pending",
145
  ):
146
  st.session_state.pop(key, None)
147
 
 
151
  "sp_session_expires_at", "sp_user",
152
  "sp_last_verified_at", "sp_edge_assertion", "sp_auth_api_origin",
153
  "sp_product_ui_projection", "sp_product_tier",
154
+ "sp_account_pending_command_id",
155
  ):
156
  st.session_state.pop(key, None)
157
 
 
324
 
325
 
326
  def get_account_snapshot(*, block: bool = False) -> dict[str, Any] | None:
327
+ # External entitlement mutations (administrator adjustment/payment credit/
328
+ # another device) cannot increment this browser's local revision. Keep one
329
+ # command id stable only for the browser-bridge handshake, then retire it
330
+ # after a completed response so the next Streamlit rerun performs a fresh
331
+ # Worker/D1 account read instead of reusing a time-bucketed cached result.
332
  revision = int(st.session_state.get("sp_account_revision") or 0)
333
+ command_id = str(st.session_state.get("sp_account_pending_command_id") or "").strip()
334
+ if not command_id:
335
+ command_id = f"account:{revision}:{secrets.token_urlsafe(10)}"
336
+ st.session_state["sp_account_pending_command_id"] = command_id
337
  result = _edge_call("account", command_id, {}, key_prefix="sp_account")
338
  if result is None:
339
  if block:
340
  st.stop()
341
  return None
342
+ st.session_state.pop("sp_account_pending_command_id", None)
343
  if not result.get("ok"):
344
  if block:
345
  _raise_edge_error(result, "account_query_failed")
 
661
  st.session_state["sp_product_ui_projection"] = _normalize_product_ui_projection(projected)
662
 
663
  order = st.session_state.get("sp_payment_order") or {}
664
+ if str(order.get("merchant_order_no") or "").strip():
665
+ # The app-level pending flag exists only to survive the initial
666
+ # browser-bridge handshake. Once the trusted order exists, clear it
667
+ # inside the dialog fragment so manually closing the dialog does not
668
+ # cause an unrelated later rerun to reopen it.
669
+ st.session_state["sp_payment_dialog_pending"] = False
670
  qr_code = str(order.get("qr_code") or "").strip()
671
  if qr_code:
672
  payment_provider = str(order.get("payment_provider") or "").strip().upper()
 
843
  padding: 0.48rem clamp(0.55rem, 0.85vw, 0.9rem);
844
  border: 1px solid rgba(148, 163, 184, 0.30);
845
  border-radius: 13px;
846
+ background: #1e293b;
847
+ color: #f8fafc;
848
  font-size: clamp(0.76rem, 0.84vw, 0.92rem);
849
  font-weight: 500;
850
  line-height: 1.25;
 
882
  padding: 0.45rem 0.72rem;
883
  border: 1px solid rgba(148, 163, 184, 0.30);
884
  border-radius: 0.5rem;
885
+ background: #1e293b;
886
+ color: #f8fafc !important;
887
  text-decoration: none !important;
888
  white-space: nowrap !important;
889
  overflow-wrap: normal !important;
 
898
 
899
  .sp-logout-link:hover {
900
  border-color: rgba(255, 75, 75, 0.78);
901
+ background: #334155;
902
+ color: #f8fafc !important;
903
  }
904
 
905
  .sp-logout-link:focus-visible {