From 491f70e9351694a22108e12dcc906fbcbe60121e Mon Sep 17 00:00:00 2001 From: marcusDenslow Date: Sat, 23 May 2026 12:50:35 +0200 Subject: [PATCH] fix: add 2-minute timeout to browser payment poll so abandoned stripe checkout sessions stop polling --- pkg/tui/model.go | 16 +++++++++++----- pkg/tui/payment_form.go | 22 +++++++++++++++++++--- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/pkg/tui/model.go b/pkg/tui/model.go index ae0fb87..e59d03c 100644 --- a/pkg/tui/model.go +++ b/pkg/tui/model.go @@ -171,6 +171,8 @@ type paymentState struct { collectURL *string collectCardCount int collectBaseline time.Time + collectDeadline time.Time + collectTimeOut bool viewport viewport.Model viewportReady bool } @@ -944,6 +946,7 @@ func (m Model) collectCardCmd() tea.Cmd { } const ordersPollInterval = 10 * time.Second +const cardPollTimeout = 2 * time.Minute type OrdersPollTickMsg struct{} @@ -953,14 +956,17 @@ func (m Model) pollOrderCmd() tea.Cmd { }) } -func (m Model) pollCardsCmd(baseline time.Time, baselineCount int) tea.Cmd { - return tea.Tick(time.Second, func(_ time.Time) tea.Msg { +func (m Model) pollCardsCmd(baseline time.Time, baselineCount int, deadline time.Time) tea.Cmd { + return tea.Tick(time.Second, func(now time.Time) tea.Msg { + if now.After(deadline) { + return PollPaymentTimeoutMsg{} + } if m.APIClient == nil { - return PollPaymentStatusMsg{Baseline: baseline, BaselineCount: baselineCount} + return PollPaymentStatusMsg{Baseline: baseline, BaselineCount: baselineCount, Deadline: deadline} } cards, err := m.APIClient.GetCards() if err != nil { - return PollPaymentStatusMsg{Baseline: baseline, BaselineCount: baselineCount} + return PollPaymentStatusMsg{Baseline: baseline, BaselineCount: baselineCount, Deadline: deadline} } var newest time.Time for _, card := range cards { @@ -971,6 +977,6 @@ func (m Model) pollCardsCmd(baseline time.Time, baselineCount int) tea.Cmd { if newest.After(baseline) { return PollPaymentCompleteMsg{Cards: cards, Duplicate: len(cards) == baselineCount} } - return PollPaymentStatusMsg{Baseline: baseline, BaselineCount: baselineCount} + return PollPaymentStatusMsg{Baseline: baseline, BaselineCount: baselineCount, Deadline: deadline} }) } diff --git a/pkg/tui/payment_form.go b/pkg/tui/payment_form.go index 60ce682..7cc2540 100644 --- a/pkg/tui/payment_form.go +++ b/pkg/tui/payment_form.go @@ -62,6 +62,7 @@ type PollPaymentInitMsg struct { type PollPaymentStatusMsg struct { Baseline time.Time BaselineCount int + Deadline time.Time } // PollPaymentCompleteMsg is sent when the card count increases, meaning the @@ -71,6 +72,8 @@ type PollPaymentCompleteMsg struct { Duplicate bool } +type PollPaymentTimeoutMsg struct{} + // buildPaymentForm creates the huh form bound to state's fields. // No field-level validators so Tab/Enter move freely. func (m Model) buildPaymentForm(state *PaymentFormState) *huh.Form { @@ -370,6 +373,10 @@ func (m Model) renderPaymentHttpsView() string { baseStyle := m.theme.TextLabel() accentStyle := m.theme.TextHighlight() + if m.payment.collectTimeOut { + return m.theme.TextError().Bold(true).Render("payment link timed out") + "\n\n" + baseStyle.Render("no card saved after 2 minutes.") + "\n" + baseStyle.Render("press ") + accentStyle.Render("esc") + baseStyle.Render(" to go back and try again") + } + if m.payment.collectURL == nil { return baseStyle.Render(" generating payment link...") } @@ -439,13 +446,19 @@ func (m Model) PaymentUpdate(msg tea.Msg) (Model, tea.Cmd) { case PollPaymentInitMsg: url := msg.URL m.payment.collectURL = &url - return m, m.pollCardsCmd(m.payment.collectBaseline, m.payment.collectCardCount) + return m, m.pollCardsCmd(m.payment.collectBaseline, m.payment.collectCardCount, m.payment.collectDeadline) case PollPaymentStatusMsg: if m.payment.view != 2 { return m, nil // user navigated away, stop polling } - return m, m.pollCardsCmd(msg.Baseline, msg.BaselineCount) + return m, m.pollCardsCmd(msg.Baseline, msg.BaselineCount, msg.Deadline) + case PollPaymentTimeoutMsg: + if m.payment.view != 2 { + return m, nil + } + m.payment.collectTimeOut = true + return m, nil case PollPaymentCompleteMsg: if m.payment.view != 2 { @@ -508,6 +521,7 @@ func (m Model) PaymentUpdate(msg tea.Msg) (Model, tea.Cmd) { // Browser payment m.payment.view = 2 m.payment.collectURL = nil + m.payment.collectTimeOut = false m.payment.collectCardCount = len(m.SavedCards) var maxT time.Time for _, card := range m.SavedCards { @@ -516,6 +530,7 @@ func (m Model) PaymentUpdate(msg tea.Msg) (Model, tea.Cmd) { } } m.payment.collectBaseline = maxT + m.payment.collectDeadline = time.Now().Add(cardPollTimeout) m.footer = []footerCommand{ {key: "esc", value: "back"}, } @@ -544,11 +559,12 @@ func (m Model) PaymentUpdate(msg tea.Msg) (Model, tea.Cmd) { return m, nil } - // HTTPS/browser view avigation + // HTTPS/browser view navigation if m.payment.view == 2 { if keyMsg, ok := msg.(tea.KeyMsg); ok && keyMsg.String() == "esc" { m.payment.view = 0 m.payment.collectURL = nil + m.payment.collectTimeOut = false m.footer = []footerCommand{ {key: "j/k", value: "cards"}, {key: "enter", value: "select"},