feat: add browser-based card collection via Stripe Checkout Session

adds a /pay/{token} redirect endpoint that generates a short URL
pointing to a stripe checkout sessions in setup mode. The TUI shows a QR
code and URL on the payment page so users can add a card via the browser
instead of entering raw card data over SSH.

the checkout.session.completed webhook saves the card to the database.
the TUI polls GET /api/v1/cards every second and auto-proceeds to
confirmation once the new card is detected.

also adds a dev.sh startup script that runs all four required processes.
(SSH server, API server, Stripe CLI listener, ngrok) in a single
terminal
This commit is contained in:
2026-04-08 01:03:43 +02:00
parent 6094e05b6f
commit 062242e346
24 changed files with 1460 additions and 655 deletions

View File

@@ -116,33 +116,25 @@ func (m Model) View() string {
}
content = strings.Join(contentLines, "\n")
// Add vertical spacing around content with fixed height
contentWithPadding := lipgloss.NewStyle().
MarginTop(marginTop).
MarginBottom(marginBottom).
Height(availableContentHeight).
Render(content)
// Build footer
footer := m.BuildFooter()
// Assemble all layers vertically within the container
isViewportPage := m.currentPage == shopPage || m.currentPage == cartPage ||
m.currentPage == shippingPage || m.currentPage == paymentPage ||
m.currentPage == confirmPage
var child string
if breadcrumbs != "" {
child = lipgloss.JoinVertical(
lipgloss.Left,
header,
breadcrumbs,
contentWithPadding,
footer,
)
if isViewportPage {
if breadcrumbs != "" {
child = lipgloss.JoinVertical(lipgloss.Left, header, breadcrumbs, content, footer)
} else {
child = lipgloss.JoinVertical(lipgloss.Left, header, content, footer)
}
} else if breadcrumbs != "" {
ccontentWithPadding := lipgloss.NewStyle().MarginTop(marginTop).MarginBottom(marginBottom).Height(availableContentHeight).Render(content)
child = lipgloss.JoinVertical(lipgloss.Left, header, breadcrumbs, ccontentWithPadding, footer)
} else {
child = lipgloss.JoinVertical(
lipgloss.Left,
header,
contentWithPadding,
footer,
)
ccontentWithPadding := lipgloss.NewStyle().MarginTop(marginTop).MarginBottom(marginBottom).Height(availableContentHeight).Render(content)
child = lipgloss.JoinVertical(lipgloss.Left, header, ccontentWithPadding, footer)
}
// Constrain the assembled layout to the container dimensions
@@ -166,23 +158,16 @@ func (m Model) buildPageContent(height int) string {
case accountPage:
return m.BuildAccountView(height)
case shippingPage:
if m.ShippingView == 0 && m.ShippingForm == nil {
return m.RenderAddressList()
}
return m.RenderShippingForm(m.ShippingForm)
return m.ShippingPageView()
case paymentPage:
if m.CheckingOut {
return " submitting order..."
}
if m.PaymentView == 0 && m.PaymentForm == nil {
return m.RenderCardList()
}
return m.RenderPaymentForm(m.PaymentForm)
return m.PaymentPageView()
case confirmPage:
return m.RenderConfirmation()
return m.ConfirmView()
case cartPage:
return m.BuildCartView()
return m.CartView()
case shopPage:
return m.ShopView()
default:
return m.BuildShopView()
return m.ShopView()
}
}