diff --git a/api/handlers/addresses.go b/api/handlers/addresses.go index 177bc93..d4ba6b4 100644 --- a/api/handlers/addresses.go +++ b/api/handlers/addresses.go @@ -2,6 +2,7 @@ package handlers import ( "encoding/json" + "log" "net/http" "strconv" @@ -10,14 +11,17 @@ import ( "terminalShop/api/middleware" "terminalShop/pkg/database" "terminalShop/pkg/models" + "terminalShop/pkg/shippo" "terminalShop/pkg/utils" ) -type AddressHandler struct{} +type AddressHandler struct { + shippoKey string +} -// Create a new AddressHandler -func NewAddressHandler() *AddressHandler { - return &AddressHandler{} +// NewAddressHandler creates a new AddressHandler with the Shippo API key +func NewAddressHandler(shippoAPIKey string) *AddressHandler { + return &AddressHandler{shippoKey: shippoAPIKey} } // Retrieve all saved addresses for a user @@ -60,16 +64,48 @@ func (h *AddressHandler) CreateAddress(w http.ResponseWriter, r *http.Request) { return } - address := models.Address{ - UserID: userID, - Name: req.Name, - Street: req.Street, - Street2: req.Street2, - City: req.City, - State: req.State, - Zip: req.Zip, - Country: req.Country, - Phone: req.Phone, + // Validate and normalize address via Shippo + var address models.Address + if h.shippoKey != "" { + client := shippo.NewClient(h.shippoKey) + validated, err := client.ValidateAddress(shippo.Address{ + Name: req.Name, + Street1: req.Street, + Street2: req.Street2, + City: req.City, + State: req.State, + Country: req.Country, + Zip: req.Zip, + Phone: req.Phone, + }) + if err != nil { + log.Printf("shippo address validation failed: %v", err) + utils.RespondError(w, http.StatusBadRequest, "INVALID_ADDRESS", "address is invalid", nil) + return + } + address = models.Address{ + UserID: userID, + Name: validated.Name, + Street: validated.Street1, + Street2: validated.Street2, + City: validated.City, + State: validated.State, + Zip: validated.Zip, + Country: validated.Country, + Phone: validated.Phone, + } + } else { + address = models.Address{ + UserID: userID, + Name: req.Name, + Street: req.Street, + Street2: req.Street2, + City: req.City, + State: req.State, + Zip: req.Zip, + Country: req.Country, + Phone: req.Phone, + } } if err := db.Create(&address).Error; err != nil { @@ -82,7 +118,7 @@ func (h *AddressHandler) CreateAddress(w http.ResponseWriter, r *http.Request) { }) } -// Delete a saved address +// DeleteAddress deltes a saved address func (h *AddressHandler) DeleteAddress(w http.ResponseWriter, r *http.Request) { db := database.GetDB() userID := middleware.UserIDFromContext(r.Context()) diff --git a/api/handlers/checkout.go b/api/handlers/checkout.go index 52ce8d1..8ee6f28 100644 --- a/api/handlers/checkout.go +++ b/api/handlers/checkout.go @@ -3,6 +3,8 @@ package handlers import ( "encoding/json" "fmt" + "log" + "math" "net/http" "github.com/stripe/stripe-go/v78" @@ -31,20 +33,13 @@ type CheckoutCartItem struct { } type CheckoutRequest struct { - StripeToken string `json:"stripe_token"` - Last4 string `json:"last4"` - Brand string `json:"brand"` - ExpMonth int `json:"exp_month"` - ExpYear int `json:"exp_year"` - Items []CheckoutCartItem `json:"items"` - ShippingName string `json:"shipping_name"` - ShippingStreet string `json:"shipping_street"` - ShippingStreet2 string `json:"shipping_street2"` - ShippingCity string `json:"shipping_city"` - ShippingState string `json:"shipping_state"` - ShippingZip string `json:"shipping_zip"` - ShippingCountry string `json:"shipping_country"` - ShippingPhone string `json:"shipping_phone"` + StripeToken string `json:"stripe_token"` + Last4 string `json:"last4"` + Brand string `json:"brand"` + ExpMonth int `json:"exp_month"` + ExpYear int `json:"exp_year"` + Items []CheckoutCartItem `json:"items"` + AddressID uint `json:"address_id"` } func (h *CheckoutHandler) Checkout(w http.ResponseWriter, r *http.Request) { @@ -67,6 +62,17 @@ func (h *CheckoutHandler) Checkout(w http.ResponseWriter, r *http.Request) { return } + if req.AddressID == 0 { + utils.RespondError(w, http.StatusBadRequest, "VALIDATION_ERROR", "address_id is required", nil) + return + } + + var address models.Address + if err := db.Where("id = ? AND user_id = ?", req.AddressID, userID).First(&address).Error; err != nil { + utils.RespondError(w, http.StatusBadRequest, "INVALID_ADDRESS", "address not found", nil) + return + } + var user models.User if err := db.First(&user, userID).Error; err != nil { utils.RespondError(w, http.StatusNotFound, "USER_NOT_FOUND", "user not found", nil) @@ -92,20 +98,27 @@ func (h *CheckoutHandler) Checkout(w http.ResponseWriter, r *http.Request) { return } user.StripeCustomerID = cust.ID - db.Save(&user) + if err := db.Save(&user).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to save stripe customer", nil) + return + } } var subtotal int var orderItems []models.OrderItem for _, item := range req.Items { + if item.Quantity <= 0 { + utils.RespondError(w, http.StatusBadRequest, "VALIDATION_ERROR", "item quantity must be at least 1", nil) + return + } var coffee models.Coffee if err := db.First(&coffee, item.CoffeeID).Error; err != nil { utils.RespondError(w, http.StatusBadRequest, "INVALID_PRODUCT", fmt.Sprintf("product %d not found", item.CoffeeID), nil) return } - priceInCents := int(coffee.Price * 100) + priceInCents := int(math.Round(coffee.Price * 100)) lineTotal := priceInCents * item.Quantity subtotal += lineTotal @@ -135,7 +148,8 @@ func (h *CheckoutHandler) Checkout(w http.ResponseWriter, r *http.Request) { ch, err := charge.New(chargeParams) if err != nil { - utils.RespondError(w, http.StatusPaymentRequired, "PAYMENT_FAILED", fmt.Sprintf("payment failed: %v", err), nil) + log.Printf("stripe charge failed for user %d: %v", user.ID, err) + utils.RespondError(w, http.StatusPaymentRequired, "PAYMENT_FAILED", "payment could not be processed", nil) return } @@ -148,7 +162,10 @@ func (h *CheckoutHandler) Checkout(w http.ResponseWriter, r *http.Request) { ExpYear: req.ExpYear, } - db.Create(&card) + if err := db.Create(&card).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to save card", nil) + return + } order := models.Order{ UserID: user.ID, @@ -157,14 +174,14 @@ func (h *CheckoutHandler) Checkout(w http.ResponseWriter, r *http.Request) { Status: models.OrderStatusPaid, Subtotal: subtotal, Total: total, - ShippingName: req.ShippingName, - ShippingStreet: req.ShippingStreet, - ShippingStreet2: req.ShippingStreet2, - ShippingCity: req.ShippingCity, - ShippingState: req.ShippingState, - ShippingZip: req.ShippingZip, - ShippingCountry: req.ShippingCountry, - ShippingPhone: req.ShippingPhone, + ShippingName: address.Name, + ShippingStreet: address.Street, + ShippingStreet2: address.Street2, + ShippingCity: address.City, + ShippingState: address.State, + ShippingZip: address.Zip, + ShippingCountry: address.Country, + ShippingPhone: address.Phone, Items: orderItems, } @@ -173,7 +190,10 @@ func (h *CheckoutHandler) Checkout(w http.ResponseWriter, r *http.Request) { return } - db.Preload("Items").First(&order, order.ID) + if err := db.Preload("Items").First(&order, order.ID).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to reload order", nil) + return + } utils.RespondSuccess(w, http.StatusCreated, map[string]interface{}{ "order": order, diff --git a/api/main.go b/api/main.go index 5c1d46e..7a27e2c 100644 --- a/api/main.go +++ b/api/main.go @@ -46,7 +46,7 @@ func main() { jwtManager := auth.NewJWTManager(cfg.JWTSecret, 30*time.Minute) // Setup routes - router := routes.SetupRoutes(version, cfg.StripeSecretKey, jwtManager, cfg.AuthFingerprintKey) + router := routes.SetupRoutes(version, cfg.StripeSecretKey, jwtManager, cfg.AuthFingerprintKey, cfg.ShippoAPIKey) // Create HTTP server server := &http.Server{ diff --git a/api/middleware/auth.go b/api/middleware/auth.go index e377a99..af2053c 100644 --- a/api/middleware/auth.go +++ b/api/middleware/auth.go @@ -14,15 +14,15 @@ type contextKey string const userIDKey contextKey = "userID" func Auth(jwtManager *auth.JWTManager) func(http.Handler) http.Handler { - // Auth validates JWT tokens on all requests. - // if a valid token is present, store an userID in context - // if no token, continue as public (no userID). - // If invalid token, returns 401 + // Auth extracts and validates JWT tokens on all requests. + // If a valid token is present, store the userID in context. + // If no token or an invalid token, continue as public (no userID). + // Protected routes use RequireAuth to enforce valid authentication. return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") if authHeader == "" { - // No token - public req + // No token - public request next.ServeHTTP(w, r) return } @@ -30,13 +30,16 @@ func Auth(jwtManager *auth.JWTManager) func(http.Handler) http.Handler { // Expect "Bearer " parts := strings.SplitN(authHeader, " ", 2) if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") { - utils.RespondError(w, http.StatusUnauthorized, "INVALID_AUTH_BEARER", "authorization header must be Bearer ", nil) + // Malformed header - continue as public request + next.ServeHTTP(w, r) return } claims, err := jwtManager.ValidateToken(parts[1]) if err != nil { - utils.RespondError(w, http.StatusUnauthorized, "INVALID_TOKEN", "invalid or expired token", nil) + // Invalid or expired token - continue as public request. + // RequireAuth will reject on protected routes. + next.ServeHTTP(w, r) return } diff --git a/api/routes/routes.go b/api/routes/routes.go index bba5a88..cea864a 100644 --- a/api/routes/routes.go +++ b/api/routes/routes.go @@ -9,7 +9,7 @@ import ( ) // SetupRoutes configures all API routes -func SetupRoutes(version string, stripeSecretKey string, jwtManager *auth.JWTManager, authFingerprintKey string) *chi.Mux { +func SetupRoutes(version string, stripeSecretKey string, jwtManager *auth.JWTManager, authFingerprintKey string, shippoAPIKey string) *chi.Mux { r := chi.NewRouter() // Apply global middleware @@ -24,7 +24,7 @@ func SetupRoutes(version string, stripeSecretKey string, jwtManager *auth.JWTMan authHandler := handlers.NewAuthHandler(jwtManager, authFingerprintKey) checkoutHandler := handlers.NewCheckoutHandler(stripeSecretKey) orderHandler := handlers.NewOrderHandler() - addressHandler := handlers.NewAddressHandler() + addressHandler := handlers.NewAddressHandler(shippoAPIKey) // API v1 routes r.Route("/api/v1", func(r chi.Router) { diff --git a/main.go b/main.go index 598e8ec..1add16b 100644 --- a/main.go +++ b/main.go @@ -27,7 +27,10 @@ const ( port = "23456" ) -var authService *auth.SSHAuthService +var ( + authService *auth.SSHAuthService + apiURL string +) func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { // Extract SSH public key from session @@ -54,7 +57,7 @@ func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { } // Create TUI model with user context (no registration screen needed!) - m := tui.NewModelWithAuth(user, false, s.PublicKey(), token) + m := tui.NewModelWithAuth(user, false, s.PublicKey(), token, apiURL) return m, []tea.ProgramOption{tea.WithAltScreen()} } @@ -84,6 +87,10 @@ func main() { // Initialize SSH auth service authService = auth.NewSSHAuthService(db, jwtManager) + // Set API URL from config for TUI connections + apiURL = cfg.APIURL + log.Printf("TUI will connect to API at: %s", apiURL) + // Create SSH server s, err := wish.NewServer( wish.WithAddress(fmt.Sprintf("%s:%s", host, port)), diff --git a/pkg/api/client.go b/pkg/api/client.go index 3b54c40..da98e38 100644 --- a/pkg/api/client.go +++ b/pkg/api/client.go @@ -169,20 +169,13 @@ func (c *Client) RegisterUser(username, sshPublicKey, sshKeyFingerprint string) // CheckoutRequest represents the checkout payload sent to the API type CheckoutRequest struct { - StripeToken string `json:"stripe_token"` - Last4 string `json:"last4"` - Brand string `json:"brand"` - ExpMonth int `json:"exp_month"` - ExpYear int `json:"exp_year"` - Items []CheckoutCartItem `json:"items"` - ShippingName string `json:"shipping_name"` - ShippingStreet string `json:"shipping_street"` - ShippingStreet2 string `json:"shipping_street2,omitempty"` - ShippingCountry string `json:"shipping_country"` - ShippingCity string `json:"shipping_city"` - ShippingState string `json:"shipping_state"` - ShippingZip string `json:"shipping_zip"` - ShippingPhone string `json:"shipping_phone,omitempty"` + StripeToken string `json:"stripe_token"` + Last4 string `json:"last4"` + Brand string `json:"brand"` + ExpMonth int `json:"exp_month"` + ExpYear int `json:"exp_year"` + Items []CheckoutCartItem `json:"items"` + AddressID uint `json:"address_id"` } type CheckoutCartItem struct { diff --git a/pkg/config/config.go b/pkg/config/config.go index 0fdf987..628b4c5 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -10,6 +10,7 @@ import ( // Config holds all application configuration type Config struct { APIPort string + APIURL string SSHPort string Environment string DatabaseURL string @@ -22,16 +23,21 @@ type Config struct { // Load reads configuration from environment variables func Load() (*Config, error) { - // Load .env file in non-production environments + // Load .env file in non-production environments. + // Try current directory first, then parent directory (for when + // running from subdirectories like api/). if os.Getenv("ENVIRONMENT") != "production" { if err := godotenv.Load(); err != nil { - // .env is optional in production - fmt.Println("No .env file found, using environment variables") + if err := godotenv.Load("../.env"); err != nil { + fmt.Println("No .env file found, using environment variables") + } } } + apiPort := getEnvOrDefault("API_PORT", "8080") cfg := &Config{ - APIPort: getEnvOrDefault("API_PORT", "8080"), + APIPort: apiPort, + APIURL: getEnvOrDefault("API_URL", fmt.Sprintf("http://localhost:%s", apiPort)), SSHPort: getEnvOrDefault("SSH_PORT", "23457"), Environment: getEnvOrDefault("ENVIRONMENT", "development"), DatabaseURL: getEnvOrDefault("DATABASE_URL", "terminalshop.db"), diff --git a/pkg/shippo/shippo.go b/pkg/shippo/shippo.go index 6c7562e..ab6bab7 100644 --- a/pkg/shippo/shippo.go +++ b/pkg/shippo/shippo.go @@ -53,8 +53,11 @@ type addressRequest struct { } // validationResult is Shippo's validation response nested under validation_results. +// IsValid is a pointer to distinguish between absent (nil) and explicitly false. +// Shippo returns empty validation_results for international addresses, which +// means "no data to validate" rather than "invalid". type validationResult struct { - IsValid bool `json:"is_valid"` + IsValid *bool `json:"is_valid"` } // addressResponse is the response from Shippo's address endpoint. @@ -118,8 +121,12 @@ func (c *Client) ValidateAddress(addr Address) (*Address, error) { return nil, fmt.Errorf("failed to parse response: %w", err) } - // Accept if test mode or validation says valid - if !result.Test && (result.ValidationResults == nil || !result.ValidationResults.IsValid) { + // TODO: Re-add !result.Test bypass before production if needed. + // Removed test mode bypass so we can verify Shippo validation in development. + // + // Shippo returns empty validation_results (no is_valid field) for international + // addresses. We only reject when is_valid is explicitly false. + if result.ValidationResults != nil && result.ValidationResults.IsValid != nil && !*result.ValidationResults.IsValid { return nil, fmt.Errorf("address is invalid") } diff --git a/pkg/tui/menu_test.go b/pkg/tui/menu_test.go index 0cce043..e5aaa3d 100644 --- a/pkg/tui/menu_test.go +++ b/pkg/tui/menu_test.go @@ -44,7 +44,6 @@ func TestModel_BuildMenuView(t *testing.T) { ShippingView int AddressCursor int StripeKey string - ShippoKey string Orders []models.Order OrdersLoaded bool } @@ -92,7 +91,6 @@ func TestModel_BuildMenuView(t *testing.T) { ShippingView: tt.fields.ShippingView, AddressCursor: tt.fields.AddressCursor, StripeKey: tt.fields.StripeKey, - ShippoKey: tt.fields.ShippoKey, Orders: tt.fields.Orders, OrdersLoaded: tt.fields.OrdersLoaded, } diff --git a/pkg/tui/model.go b/pkg/tui/model.go index c6a4baf..64c9001 100644 --- a/pkg/tui/model.go +++ b/pkg/tui/model.go @@ -3,6 +3,7 @@ package tui import ( "encoding/json" "fmt" + "log" "math" "net/http" "os" @@ -14,7 +15,6 @@ import ( "terminalShop/pkg/api" "terminalShop/pkg/models" - "terminalShop/pkg/shippo" ) // termSize represents the terminal size category for responsive layout. @@ -87,7 +87,6 @@ type Model struct { ShippingView int AddressCursor int StripeKey string // Stripe publishable key for client-side tokenization - ShippoKey string // Shippo API key for address validation // Order history Orders []models.Order @@ -131,10 +130,17 @@ type ProductsMsg struct { // fetchProductsCmd fetches products from the API func (m Model) fetchProductsCmd() tea.Msg { if m.APIClient == nil { + log.Println("[TUI] APIClient is nil, using fallback products") return ProductsMsg{Err: nil} // No API client, use fallback } + log.Printf("[TUI] Fetching products from API at %s", m.APIClient.BaseURL) products, err := m.APIClient.GetProducts() + if err != nil { + log.Printf("[TUI] Failed to fetch products: %v", err) + } else { + log.Printf("[TUI] Loaded %d products from API", len(products)) + } return ProductsMsg{Products: products, Err: err} } @@ -204,17 +210,17 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.ViewingCart && m.CheckoutStep == 1 { switch msg := msg.(type) { case ShippingFormCompleteMsg: - return m, m.validateAddress(msg) - - case ShippoValidatedMsg: - m.ShippingInfo = &msg.Address - m.ShippingForm = nil - m.CheckoutStep = 2 - m.PaymentForm = m.InitPaymentForm() - if msg.SaveAddress { - return m, tea.Batch(m.PaymentForm.form.Init(), m.saveAddressCmd(msg.Address)) + addr := models.Address{ + Name: msg.Name, + Street: msg.Street1, + Street2: msg.Street2, + City: msg.City, + State: msg.State, + Country: msg.Country, + Zip: msg.Zip, + Phone: msg.Phone, } - return m, m.PaymentForm.form.Init() + return m, m.saveAddressCmd(addr) case AddressesMsg: if msg.Err != nil || len(msg.Addresses) == 0 { @@ -229,15 +235,21 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.ShippingForm = nil return m, nil - case ShippoValidationErrMsg: - if m.ShippingForm != nil { - m.ShippingForm.submitting = false - m.ShippingForm.form = m.buildShippingForm(m.ShippingForm) + case AddressSavedMsg: + if msg.Err != nil { + if m.ShippingForm != nil { + m.ShippingForm.submitting = false + m.ShippingForm.form = m.buildShippingForm(m.ShippingForm) + } m.ErrorMsg = fmt.Sprintf("Invalid address: %v", msg.Err) return m, m.ShippingForm.form.Init() } - m.ErrorMsg = fmt.Sprintf("Invalid address: %v", msg.Err) - return m, nil + m.ShippingInfo = &msg.Address + m.ShippingForm = nil + m.CheckoutStep = 2 + m.PaymentForm = m.InitPaymentForm() + return m, m.PaymentForm.form.Init() + case ShippingFormErrorMsg: m.ErrorMsg = msg.Message return m, nil @@ -388,22 +400,36 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case ShippingFormCompleteMsg: - return m, m.validateAddress(msg) + addr := models.Address{ + Name: msg.Name, + Street: msg.Street1, + Street2: msg.Street2, + City: msg.City, + State: msg.State, + Country: msg.Country, + Zip: msg.Zip, + Phone: msg.Phone, + } + return m, m.saveAddressCmd(addr) - case ShippoValidatedMsg: + case AddressSavedMsg: + if msg.Err != nil { + if m.ShippingForm != nil { + m.ShippingForm.submitting = false + m.ShippingForm.form = m.buildShippingForm(m.ShippingForm) + } + m.ErrorMsg = fmt.Sprintf("Invalid address: %v", msg.Err) + if m.ShippingForm != nil { + return m, m.ShippingForm.form.Init() + } + return m, nil + } m.ShippingInfo = &msg.Address m.ShippingForm = nil m.CheckoutStep = 2 m.PaymentForm = m.InitPaymentForm() - if msg.SaveAddress { - return m, tea.Batch(m.PaymentForm.form.Init(), m.saveAddressCmd(msg.Address)) - } return m, m.PaymentForm.form.Init() - case ShippoValidationErrMsg: - m.ErrorMsg = fmt.Sprintf("invalid address: %v", msg.Err) - return m, nil - case ShippingFormErrorMsg: m.ErrorMsg = msg.Message return m, nil @@ -661,68 +687,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -// ShippoValidatedMsg is sent when Shippo address validation succeeds -type ShippoValidatedMsg struct { - Address models.Address - SaveAddress bool -} - -// ShippoValidationErrMsg is sent when Shippo address validation fails -type ShippoValidationErrMsg struct { - Err error -} - -// validateAddress sends the shipping address to Shippo for validation -func (m Model) validateAddress(info ShippingFormCompleteMsg) tea.Cmd { - return func() tea.Msg { - if m.ShippoKey == "" { - // No Shippo key configured, skip validation - return ShippoValidatedMsg{ - Address: models.Address{ - Name: info.Name, - Street: info.Street1, - Street2: info.Street2, - City: info.City, - State: info.State, - Country: info.Country, - Zip: info.Zip, - Phone: info.Phone, - }, - SaveAddress: info.SaveAddress, - } - } - - client := shippo.NewClient(m.ShippoKey) - validated, err := client.ValidateAddress(shippo.Address{ - Name: info.Name, - Street1: info.Street1, - Street2: info.Street2, - City: info.City, - State: info.State, - Country: info.Country, - Zip: info.Zip, - Phone: info.Phone, - }) - if err != nil { - return ShippoValidationErrMsg{Err: err} - } - - return ShippoValidatedMsg{ - Address: models.Address{ - Name: validated.Name, - Street: validated.Street1, - Street2: validated.Street2, - City: validated.City, - State: validated.State, - Country: validated.Country, - Zip: validated.Zip, - Phone: validated.Phone, - }, - SaveAddress: info.SaveAddress, - } - } -} - type StripeTokenMsg struct { TokenID string Last4 string @@ -880,17 +844,10 @@ func (m Model) submitCheckout(tok StripeTokenMsg) tea.Cmd { } req := api.CheckoutRequest{ - StripeToken: tok.TokenID, - Last4: tok.Last4, - Items: cartItems, - ShippingName: m.ShippingInfo.Name, - ShippingStreet: m.ShippingInfo.Street, - ShippingStreet2: m.ShippingInfo.Street2, - ShippingCity: m.ShippingInfo.City, - ShippingState: m.ShippingInfo.State, - ShippingZip: m.ShippingInfo.Zip, - ShippingCountry: m.ShippingInfo.Country, - ShippingPhone: m.ShippingInfo.Phone, + StripeToken: tok.TokenID, + Last4: tok.Last4, + Items: cartItems, + AddressID: m.ShippingInfo.ID, } order, err := m.APIClient.Checkout(req) @@ -999,14 +956,13 @@ func NewModel(username string) Model { Loading: true, APIClient: api.NewClient("http://localhost:8080", ""), StripeKey: os.Getenv("STRIPE_PUBLIC_KEY"), - ShippoKey: os.Getenv("SHIPPO_API_KEY"), } m.updateLayout(120, 30) return m } // NewModelWithAuth creates a new model with user authentication context -func NewModelWithAuth(user *models.User, isNewUser bool, pubKey gossh.PublicKey, token string) Model { +func NewModelWithAuth(user *models.User, isNewUser bool, pubKey gossh.PublicKey, token string, apiURL string) Model { username := "guest" if user != nil { username = user.Name @@ -1017,7 +973,11 @@ func NewModelWithAuth(user *models.User, isNewUser bool, pubKey gossh.PublicKey, m.IsNewUser = isNewUser m.SSHPublicKey = pubKey m.AccessToken = token - m.APIClient = api.NewClient("http://localhost:8080", token) + if apiURL != "" { + m.APIClient = api.NewClient(apiURL, token) + } else { + m.APIClient = api.NewClient("http://localhost:8080", token) + } return m } diff --git a/pkg/tui/model_test.go b/pkg/tui/model_test.go index 5271428..9ca6c8f 100644 --- a/pkg/tui/model_test.go +++ b/pkg/tui/model_test.go @@ -44,7 +44,6 @@ func TestModel_updateLayout(t *testing.T) { ShippingView int AddressCursor int StripeKey string - ShippoKey string Orders []models.Order OrdersLoaded bool } @@ -96,7 +95,6 @@ func TestModel_updateLayout(t *testing.T) { ShippingView: tt.fields.ShippingView, AddressCursor: tt.fields.AddressCursor, StripeKey: tt.fields.StripeKey, - ShippoKey: tt.fields.ShippoKey, Orders: tt.fields.Orders, OrdersLoaded: tt.fields.OrdersLoaded, } diff --git a/pkg/tui/shipping_form.go b/pkg/tui/shipping_form.go index 97edbdd..171e3cc 100644 --- a/pkg/tui/shipping_form.go +++ b/pkg/tui/shipping_form.go @@ -57,7 +57,6 @@ type ShippingFormCompleteMsg struct { Country string Zip string Phone string - SaveAddress bool } // ShippingFormErrorMsg is sent when form validation fails at submission time @@ -227,7 +226,6 @@ func (m Model) UpdateShippingForm(msg tea.Msg, state *ShippingFormState) tea.Cmd Country: state.Country, Zip: state.Zip, Phone: phone, - SaveAddress: true, } } } diff --git a/terminal-shop-source b/terminal-shop-source index caa24e7..697730e 160000 --- a/terminal-shop-source +++ b/terminal-shop-source @@ -1 +1 @@ -Subproject commit caa24e7ffaaf6fa49289ebaf08a1b8c116fd6b0c +Subproject commit 697730e73cba0ef0bab943bbd75e5bcd64f5f5d3