fix(cards): advance tui after duplicate card detection by pollin on updated_at instead of count
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
2026-05-22 22:09:55 +02:00
parent 96400ede14
commit b2bd6ed623
4 changed files with 51 additions and 14 deletions

View File

@@ -244,6 +244,11 @@ func (h *WebhookHandler) handleCheckoutSessionCompleted(ctx context.Context, eve
if _, derr := paymentmethod.Detach(pm.ID, nil); derr != nil {
webhookLog().Warn("failed to detach duplicate pm", "user_id", user.ID, "error", derr)
}
// Bump UpdatedAt so the TUI poll signal can detect that the webhook
// fired even though no new row was inserted.
if uerr := db.Model(&dup).Update("updated_at", time.Now()).Error; uerr != nil {
webhookLog().Warn("failed to bump dup card updated_at", "card_id", dup.ID, "error", uerr)
}
webhookLog().Info("skipped duplicate card via local-db match", "user_id", user.ID, "existing_card_id", dup.ID)
return
}

View File

@@ -132,7 +132,11 @@ func (m Model) generateReviewContent() string {
view := strings.Builder{}
if m.review.cardJustAdded {
view.WriteString(m.theme.TextSuccess().Bold(true).Render("card added successfully") + "\n\n")
if m.review.cardWasDuplicate {
view.WriteString(m.theme.TextDim().Render("identical card already saved — using existing card") + "\n\n")
} else {
view.WriteString(m.theme.TextSuccess().Bold(true).Render("card added successfully") + "\n\n")
}
}
if m.ShippingInfo != nil {

View File

@@ -170,6 +170,7 @@ type paymentState struct {
cardCursor int
collectURL *string
collectCardCount int
collectBaseline time.Time
viewport viewport.Model
viewportReady bool
}
@@ -191,8 +192,9 @@ type accountState struct {
}
type reviewState struct {
cardJustAdded bool
success bool
cardJustAdded bool
cardWasDuplicate bool
success bool
}
type confirmState struct {
@@ -951,18 +953,24 @@ func (m Model) pollOrderCmd() tea.Cmd {
})
}
func (m Model) pollCardsCmd(cardCount int) tea.Cmd {
func (m Model) pollCardsCmd(baseline time.Time, baselineCount int) tea.Cmd {
return tea.Tick(time.Second, func(_ time.Time) tea.Msg {
if m.APIClient == nil {
return PollPaymentStatusMsg{CardCount: cardCount}
return PollPaymentStatusMsg{Baseline: baseline, BaselineCount: baselineCount}
}
cards, err := m.APIClient.GetCards()
if err != nil {
return PollPaymentStatusMsg{CardCount: cardCount}
return PollPaymentStatusMsg{Baseline: baseline, BaselineCount: baselineCount}
}
if len(cards) > cardCount {
return PollPaymentCompleteMsg{Cards: cards}
var newest time.Time
for _, card := range cards {
if card.UpdatedAt.After(newest) {
newest = card.UpdatedAt
}
}
return PollPaymentStatusMsg{CardCount: cardCount}
if newest.After(baseline) {
return PollPaymentCompleteMsg{Cards: cards, Duplicate: len(cards) == baselineCount}
}
return PollPaymentStatusMsg{Baseline: baseline, BaselineCount: baselineCount}
})
}

View File

@@ -3,6 +3,7 @@ package tui
import (
"fmt"
"strings"
"time"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
@@ -59,13 +60,15 @@ type PollPaymentInitMsg struct {
// PollPaymentStatusMsg is sent each polling tick while waiting for the browser
// card entry to complete.
type PollPaymentStatusMsg struct {
CardCount int
Baseline time.Time
BaselineCount int
}
// PollPaymentCompleteMsg is sent when the card count increases, meaning the
// webhook saved the new card
type PollPaymentCompleteMsg struct {
Cards []models.Card
Cards []models.Card
Duplicate bool
}
// buildPaymentForm creates the huh form bound to state's fields.
@@ -436,13 +439,13 @@ 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.collectCardCount)
return m, m.pollCardsCmd(m.payment.collectBaseline, m.payment.collectCardCount)
case PollPaymentStatusMsg:
if m.payment.view != 2 {
return m, nil // user navigated away, stop polling
}
return m, m.pollCardsCmd(msg.CardCount)
return m, m.pollCardsCmd(msg.Baseline, msg.BaselineCount)
case PollPaymentCompleteMsg:
if m.payment.view != 2 {
@@ -451,9 +454,19 @@ func (m Model) PaymentUpdate(msg tea.Msg) (Model, tea.Cmd) {
m.SavedCards = msg.Cards
m.payment.view = 0
m.payment.collectURL = nil
selected := m.SavedCards[len(m.SavedCards)-1]
var selected models.Card
if msg.Duplicate {
for _, card := range m.SavedCards {
if selected.ID == 0 || card.UpdatedAt.After(selected.UpdatedAt) {
selected = card
}
}
} else {
selected = m.SavedCards[len(m.SavedCards)-1]
}
m.SelectedCard = &selected
m.review.cardJustAdded = true
m.review.cardWasDuplicate = msg.Duplicate
return m.ReviewSwitch()
}
@@ -496,6 +509,13 @@ func (m Model) PaymentUpdate(msg tea.Msg) (Model, tea.Cmd) {
m.payment.view = 2
m.payment.collectURL = nil
m.payment.collectCardCount = len(m.SavedCards)
var maxT time.Time
for _, card := range m.SavedCards {
if card.UpdatedAt.After(maxT) {
maxT = card.UpdatedAt
}
}
m.payment.collectBaseline = maxT
m.footer = []footerCommand{
{key: "esc", value: "back"},
}