fix: added user verification to continue checkout after adding card
when the user added their card it would instantly complete the order. this could be unwanted so now i have added an extra input requirement to confirm the payment after adding the card
This commit is contained in:
@@ -2,6 +2,8 @@ package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"terminalShop/pkg/models"
|
||||
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
@@ -42,7 +44,7 @@ func (m Model) RenderConfirmation() string {
|
||||
cartSection += sectionStyle.Render(line) + "\n"
|
||||
}
|
||||
cartSection += sectionStyle.Render(
|
||||
labelStyle.Render("Shipping: ") + valueStyle.Render("$0.00"),
|
||||
labelStyle.Render("Shipping: ")+valueStyle.Render("$0.00"),
|
||||
) + "\n"
|
||||
cartSection += sectionStyle.Render(
|
||||
labelStyle.Render("Total: ") + valueStyle.Render(fmt.Sprintf("$%.2f", float64(m.ConfirmTotal)/100)),
|
||||
@@ -122,3 +124,90 @@ func (m Model) ConfirmUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func formatUSD(cents int) string {
|
||||
return fmt.Sprintf("$%d.%02d", cents/100, cents%100)
|
||||
}
|
||||
|
||||
func (m Model) generateReviewContent() string {
|
||||
view := strings.Builder{}
|
||||
|
||||
if m.ShippingInfo != nil {
|
||||
view.WriteString(m.ShippingInfo.Name + "\n")
|
||||
view.WriteString(m.ShippingInfo.Street + "\n")
|
||||
if m.ShippingInfo.Street2 != "" {
|
||||
view.WriteString(m.ShippingInfo.Street2 + "\n")
|
||||
}
|
||||
view.WriteString(fmt.Sprintf("%s, %s %s, %s\n", m.ShippingInfo.City, m.ShippingInfo.State, m.ShippingInfo.Zip, m.ShippingInfo.Country))
|
||||
}
|
||||
view.WriteString("\n")
|
||||
|
||||
if m.SelectedCard != nil {
|
||||
view.WriteString(fmt.Sprintf("cc: **** **** **** %s\n", m.SelectedCard.Last4))
|
||||
}
|
||||
view.WriteString("\n")
|
||||
|
||||
subtotal := 0
|
||||
for _, item := range m.GetCartItemsSlice() {
|
||||
subtotal += item.Coffee.Price * item.Quantity
|
||||
}
|
||||
view.WriteString(fmt.Sprintf("subtotal: %\n", formatUSD(subtotal)))
|
||||
view.WriteString(m.theme.TextAccent().Render(fmt.Sprintf("total: %s", formatUSD(subtotal))) + "\n")
|
||||
view.WriteString("\n")
|
||||
view.WriteString(m.theme.TextDim().Render("press enter to confirm") + "\n")
|
||||
|
||||
return m.theme.Base().Padding(0, 1).Render(view.String())
|
||||
}
|
||||
|
||||
func (m Model) ReviewView() string {
|
||||
if m.CheckingOut {
|
||||
return m.theme.TextAccent().Bold(true).Padding(1).Render(" submitting order...")
|
||||
}
|
||||
content := m.generateReviewContent()
|
||||
return lipgloss.Place(
|
||||
m.widthContainer,
|
||||
lipgloss.Height(content),
|
||||
lipgloss.Center, lipgloss.Center,
|
||||
content,
|
||||
)
|
||||
}
|
||||
|
||||
func (m Model) ReviewUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case CheckoutResultMsg:
|
||||
m.CheckingOut = false
|
||||
if msg.Err != nil {
|
||||
m.ErrorMsg = fmt.Sprintf("checkout failed: %v", msg.Err)
|
||||
m = m.SwitchPage(paymentPage)
|
||||
m.PaymentView = 0
|
||||
m.PaymentForm = nil
|
||||
return m, nil
|
||||
}
|
||||
m.ConfirmTotal = msg.Total
|
||||
for _, item := range m.GetCartItemsSlice() {
|
||||
m.ConfirmItems = append(m.ConfirmItems, *item)
|
||||
}
|
||||
m.ConfirmShipping = m.ShippingInfo
|
||||
m.Cart = make(map[uint]*models.CartItem)
|
||||
m.CartCursor = 0
|
||||
m.ShippingInfo = nil
|
||||
m.OrdersLoaded = false
|
||||
m = m.SwitchPage(confirmPage)
|
||||
return m, nil
|
||||
|
||||
case tea.KeyMsg:
|
||||
if m.CheckingOut {
|
||||
return m, nil
|
||||
}
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
m = m.SwitchPage(paymentPage)
|
||||
return m, nil
|
||||
|
||||
case "enter":
|
||||
m.CheckingOut = true
|
||||
return m, m.checkoutWithSavedCard()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ const (
|
||||
cartPage
|
||||
shippingPage
|
||||
paymentPage
|
||||
reviewPage
|
||||
confirmPage
|
||||
accountPage
|
||||
splashPage
|
||||
@@ -169,7 +170,7 @@ func (m Model) inCartFlow() bool {
|
||||
return m.currentPage == cartPage ||
|
||||
m.currentPage == shippingPage ||
|
||||
m.currentPage == paymentPage ||
|
||||
m.currentPage == confirmPage
|
||||
m.currentPage == reviewPage
|
||||
}
|
||||
|
||||
func (m Model) checkoutStep() int {
|
||||
@@ -178,7 +179,7 @@ func (m Model) checkoutStep() int {
|
||||
return 1
|
||||
case paymentPage:
|
||||
return 2
|
||||
case confirmPage:
|
||||
case reviewPage:
|
||||
return 3
|
||||
default:
|
||||
return 0
|
||||
@@ -575,6 +576,37 @@ func (m Model) saveCardAndConvert(form PaymentFormCompleteMsg) tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) saveCardOnlyCmd(form PaymentFormCompleteMsg) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if m.APIClient == nil {
|
||||
return CardSavedForReviewMsg{Err: fmt.Errorf("API client not available")}
|
||||
}
|
||||
stripe.Key = m.StripePublicKey
|
||||
tok, err := stripetoken.New(&stripe.TokenParams{
|
||||
Card: &stripe.CardParams{
|
||||
Name: stripe.String(form.CardName),
|
||||
Number: stripe.String(form.CardNumber),
|
||||
ExpMonth: stripe.String(form.ExpiryMonth),
|
||||
ExpYear: stripe.String(form.ExpiryYear),
|
||||
CVC: stripe.String(form.CVC),
|
||||
AddressZip: stripe.String(form.BillingZip),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
if stripeErr, ok := err.(*stripe.Error); ok {
|
||||
return CardSavedForReviewMsg{Err: fmt.Errorf("%s", stripeErr.Msg)}
|
||||
}
|
||||
return CardSavedForReviewMsg{Err: fmt.Errorf("failed to tokenize card: %w", err)}
|
||||
}
|
||||
card, err := m.APIClient.SaveCard(api.SaveCardRequest{Token: tok.ID})
|
||||
if err != nil {
|
||||
return CardSavedForReviewMsg{Err: fmt.Errorf("failed to save card: %w", err)}
|
||||
}
|
||||
return CardSavedForReviewMsg{Card: *card}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// loadCartFromAPI populates the in-memory cart map from an API CartData response
|
||||
// It matches cart items to local Coffees by ID so the Coffee struct is populated
|
||||
func (m *Model) loadCartFromAPI(cartData *api.CartData) {
|
||||
|
||||
@@ -39,6 +39,13 @@ type PaymentFormCompleteMsg struct {
|
||||
BillingZip string
|
||||
}
|
||||
|
||||
// CardSavedForReviewMsg is sent after a new card is tokenized and saved
|
||||
// The user is taken to the review page to confirm before checkout
|
||||
type CardSavedForReviewMsg struct {
|
||||
Card models.Card
|
||||
Err error
|
||||
}
|
||||
|
||||
// PaymentFormErrorMsg is sent when payment form validation fails
|
||||
type PaymentFormErrorMsg struct {
|
||||
Message string
|
||||
@@ -411,44 +418,31 @@ func (m Model) PaymentUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
case PaymentFormCompleteMsg:
|
||||
if m.PaymentForm != nil {
|
||||
m.PaymentForm.submitting = true
|
||||
m.CheckingOut = true
|
||||
return m, m.saveCardAndConvert(msg)
|
||||
return m, m.saveCardOnlyCmd(msg)
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case CheckoutResultMsg:
|
||||
m.CheckingOut = false
|
||||
if msg.Err != nil {
|
||||
m.ErrorMsg = fmt.Sprintf("checkout failed: %v", msg.Err)
|
||||
if len(m.SavedCards) > 0 {
|
||||
m.PaymentView = 0
|
||||
m.PaymentForm = nil
|
||||
return m, nil
|
||||
}
|
||||
// Re-open the form so the user can retry with corrected details.
|
||||
m.PaymentView = 1
|
||||
m.PaymentForm = m.InitPaymentForm()
|
||||
return m, m.PaymentForm.form.Init()
|
||||
}
|
||||
// Snapshot order details before clearing cart/shipping
|
||||
m.ConfirmTotal = msg.Total
|
||||
for _, item := range m.GetCartItemsSlice() {
|
||||
m.ConfirmItems = append(m.ConfirmItems, *item)
|
||||
}
|
||||
m.ConfirmShipping = m.ShippingInfo
|
||||
|
||||
// Clear cart and shipping immediately so they don't appear stale elsewhere
|
||||
m.Cart = make(map[uint]*models.CartItem)
|
||||
m.CartCursor = 0
|
||||
m.ShippingInfo = nil
|
||||
m.OrdersLoaded = false
|
||||
m = m.SwitchPage(confirmPage)
|
||||
return m, nil
|
||||
|
||||
case PaymentFormErrorMsg:
|
||||
m.ErrorMsg = msg.Message
|
||||
return m, nil
|
||||
|
||||
case CardSavedForReviewMsg:
|
||||
if m.PaymentForm != nil {
|
||||
m.PaymentForm.submitting = false
|
||||
}
|
||||
if msg.Err != nil {
|
||||
m.ErrorMsg = fmt.Sprintf("failed to save card: %w", msg.Err)
|
||||
m.PaymentView = 1
|
||||
m.PaymentForm = m.InitPaymentForm()
|
||||
return m, m.PaymentForm.form.Init()
|
||||
}
|
||||
m.SavedCards = append(m.SavedCards, msg.Card)
|
||||
selected := msg.Card
|
||||
m.SelectedCard = &selected
|
||||
m.PaymentForm = nil
|
||||
m = m.SwitchPage(reviewPage)
|
||||
return m, nil
|
||||
|
||||
case PollPaymentInitMsg:
|
||||
url := msg.URL
|
||||
m.CollectURL = &url
|
||||
@@ -469,8 +463,8 @@ func (m Model) PaymentUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
m.CollectURL = nil
|
||||
selected := m.SavedCards[len(m.SavedCards)-1]
|
||||
m.SelectedCard = &selected
|
||||
m.CheckingOut = true
|
||||
return m, m.checkoutWithSavedCard()
|
||||
m = m.SwitchPage(reviewPage)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Card list navigation
|
||||
@@ -497,8 +491,8 @@ func (m Model) PaymentUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
if m.CardCursor < len(m.SavedCards) {
|
||||
selected := m.SavedCards[m.CardCursor]
|
||||
m.SelectedCard = &selected
|
||||
m.CheckingOut = true
|
||||
return m, m.checkoutWithSavedCard()
|
||||
m = m.SwitchPage(reviewPage)
|
||||
return m, nil
|
||||
}
|
||||
if m.CardCursor == len(m.SavedCards) {
|
||||
m.PaymentView = 1
|
||||
|
||||
@@ -25,6 +25,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m = m.updatePaymentViewport()
|
||||
case confirmPage:
|
||||
m = m.updateConfirmViewport()
|
||||
case reviewPage:
|
||||
// no viewport on review page
|
||||
}
|
||||
if m.ShippingForm != nil {
|
||||
m.ShippingForm.form = m.buildShippingForm(m.ShippingForm)
|
||||
@@ -187,6 +189,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m.ShippingUpdate(msg)
|
||||
case paymentPage:
|
||||
return m.PaymentUpdate(msg)
|
||||
case reviewPage:
|
||||
return m.ReviewUpdate(msg)
|
||||
case confirmPage:
|
||||
return m.ConfirmUpdate(msg)
|
||||
case accountPage:
|
||||
|
||||
@@ -160,6 +160,8 @@ func (m Model) buildPageContent(height int) string {
|
||||
return m.ShippingPageView()
|
||||
case paymentPage:
|
||||
return m.PaymentPageView()
|
||||
case reviewPage:
|
||||
return m.ReviewView()
|
||||
case confirmPage:
|
||||
return m.ConfirmView()
|
||||
case cartPage:
|
||||
|
||||
Reference in New Issue
Block a user