fix: add 2-minute timeout to browser payment poll so abandoned stripe checkout sessions stop polling

This commit is contained in:
2026-05-23 12:50:35 +02:00
parent 153e487155
commit 491f70e935
2 changed files with 30 additions and 8 deletions

View File

@@ -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}
})
}

View File

@@ -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"},