rework: added shipping validation at address creation

the last implementation just accepted any input for the shipping
validation and then only validated the address on payment. this is now
changed to validation upon creation upon finishing the shipping address
form.

when a user completes the shipping form, a client-side check is first
made to just simply check if there are any empty fields, if this check
passes it gets check server-side by shippo. If the address is not found
it will return an address not found error, and the user will not be able
to continue to the payment form. The invalid address will also not be
saved to the database.

if the check pass, we simply save the address to the database with an
ID. this ID is then passe in when doing the payment, instead of
validating at the payment phase.

this implementation currently only uses Shippo, which i have come to
realize does NOT support shipping in countries outside of the US (i
should have read the friendly manual before using Shippo). This means
that a huge rewrite is due.

i will focus on adding address validation for Norway first since thats
where i live. i plan on using Bring's api for this validation. i will
still keep Shippo for US based address verification even though this
project is focused fon nordic shipping - just for a rainy day if some
poor soul decides to order from AMERICA
This commit is contained in:
2026-02-26 16:32:48 +01:00
parent 7ebc5b24de
commit 6097b8c863
14 changed files with 211 additions and 185 deletions

View File

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

View File

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

View File

@@ -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{

View File

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

View File

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

11
main.go
View File

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

View File

@@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Submodule terminal-shop-source updated: caa24e7ffa...697730e73c