1// ===== cart.go =====
   2//go:build js && wasm
   3
   4// Package main complete.go
   5package main
   6
   7import (
   8	"encoding/json"
   9	"fmt"
  10	"log"
  11	"strconv"
  12	"strings"
  13	"syscall/js"
  14
  15	"github.com/0magnet/m2/pkg/storepage"
  16)
  17
  18func updateCartDisplayWrapper(this js.Value, args []js.Value) interface{} {
  19	updateCartDisplay()
  20	return nil
  21}
  22
  23func saveCart() {
  24	cartJSON, err := json.Marshal(cart)
  25	if err != nil {
  26		log.Println(wasmName+":", "Error saving cart:", err)
  27		return
  28	}
  29	js.Global().Get("localStorage").Call("setItem", "cartItems", string(cartJSON))
  30	updateCartDisplay()
  31}
  32
  33// addToCart is called from Go rather than registered with js.FuncOf, so it
  34// keeps the callback shape its callers build but returns nothing.
  35//
  36// The guard reads three arguments, not two: qty is args[2]. Checking for two
  37// and then indexing the third is an out-of-range panic for any caller that
  38// passes exactly two, which is what the check was there to prevent.
  39func addToCart(_ js.Value, args []js.Value) {
  40	if len(args) < 3 {
  41		log.Println(wasmName+":", "addToCart: missing arguments")
  42		return
  43	}
  44	var cartItem item
  45	index := -1
  46	id := args[0].String()
  47	qty := args[2].Int()
  48	if qty == 0 {
  49		qty = 1
  50	}
  51	amount := int(args[1].Float()) * qty
  52	for i := range cart {
  53		if strings.Split(cart[i].ID, "|")[0] == strings.Split(id, "|")[0] {
  54			index = i
  55		}
  56	}
  57	if index > -1 {
  58		// update shipping
  59		if storepage.IsShippingID(cart[index].ID) {
  60			cart[index].ID = id
  61			cart[index].Qty = 1
  62			cart[index].Amount = amount
  63		} else {
  64			cart[index].Qty = cart[index].Qty + qty
  65			cart[index].Amount = cart[index].Amount + amount
  66		}
  67	} else {
  68		cartItem = item{
  69			ID:     id,
  70			Amount: amount,
  71			Qty:    qty,
  72		}
  73		cart = append(cart, cartItem)
  74	}
  75	saveCart()
  76}
  77
  78func addUnToCart(this js.Value, args []js.Value) interface{} {
  79	if len(args) < 2 {
  80		return "Error: Missing arguments"
  81	}
  82	id := args[0].String()
  83	price := args[1].Float()
  84	quantityInput := doc.Call("getElementById", fmt.Sprintf("qty-%s", id))
  85	if !quantityInput.Truthy() {
  86		log.Println(wasmName+":", "Error: Quantity input not found for item", id)
  87		return nil
  88	}
  89	quantity, err := strconv.Atoi(quantityInput.Get("value").String())
  90	if err != nil || quantity < 1 {
  91		quantity = 1
  92	}
  93
  94	addToCart(js.Value{}, []js.Value{
  95		js.ValueOf(id),
  96		js.ValueOf(int(price * 100)),
  97		js.ValueOf(quantity),
  98	})
  99	return nil
 100}
 101
 102func removeFromCart(this js.Value, inputs []js.Value) interface{} {
 103	id := inputs[0].String()
 104	newCart := []item{}
 105	for _, m := range cart {
 106		if m.ID != id {
 107			newCart = append(newCart, m)
 108		}
 109	}
 110	cart = newCart
 111	saveCart()
 112	return nil
 113}
 114
 115func loadCart() {
 116	storedCart := js.Global().Get("localStorage").Call("getItem", "cartItems")
 117	if !storedCart.IsUndefined() && !storedCart.IsNull() {
 118		err := json.Unmarshal([]byte(storedCart.String()), &cart)
 119		if err != nil {
 120			log.Println(`can't unmarshal cart from local storage`)
 121			cart = []item{}
 122		}
 123	}
 124}
 125
 126func emptyCart(this js.Value, inputs []js.Value) interface{} {
 127	js.Global().Get("localStorage").Call("removeItem", "cartItems")
 128	cart = []item{}
 129	updateCartDisplay()
 130	return nil
 131}
 132
 133func clearAll(this js.Value, inputs []js.Value) interface{} {
 134	js.Global().Get("localStorage").Call("clear")
 135	cart = []item{}
 136	updateCartDisplay()
 137	return nil
 138}
 139
 140func updateCartDisplay() {
 141	cartContainer := doc.Call("getElementById", "cart-items")
 142	totalPriceElement := doc.Call("getElementById", "total-price")
 143	table := cartContainer.Call("querySelector", "table")
 144	if table.IsNull() {
 145		table = doc.Call("createElement", "table")
 146		thead := doc.Call("createElement", "thead")
 147		thead.Set("innerHTML", `<tr><th>Item</th><th>Price</th><th>Quantity</th><th>Actions</th></tr>`)
 148		table.Call("appendChild", thead)
 149		tbody := doc.Call("createElement", "tbody")
 150		tbody.Set("id", "cart-tbody")
 151		table.Call("appendChild", tbody)
 152		cartContainer.Call("appendChild", table)
 153	}
 154	tbody := doc.Call("getElementById", "cart-tbody")
 155	tbody.Set("innerHTML", "")
 156
 157	total := 0
 158	hasShipping := false
 159	for _, m := range cart {
 160		total += m.Amount
 161		row := doc.Call("createElement", "tr")
 162
 163		row.Set("innerHTML", fmt.Sprintf(`<td>%s</td><td>$%.2f</td><td>%s</td><td><button onclick='removeFromCart("%s")'>Remove</button></td>`,
 164			func() string {
 165				parts := strings.Split(m.ID, "|")
 166				if len(parts) < 8 {
 167					return m.ID
 168				}
 169				hasShipping = true
 170				return fmt.Sprintf("%s:<br>%s<br>%s<br>%s, %s %s<br>%s<br>%s", parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6], parts[7])
 171			}(),
 172			float64(m.Amount)/100,
 173			func() string {
 174				if len(strings.Split(m.ID, "|")) == 8 {
 175					return ""
 176				}
 177				return fmt.Sprintf(`<input type='number' value='%d' min='1' onchange='updateItemQuantity("%s", this.value)'>`, m.Qty, m.ID)
 178			}(),
 179			m.ID,
 180		))
 181		tbody.Call("appendChild", row)
 182	}
 183	totalPriceElement.Set("textContent", fmt.Sprintf("Total: $%.2f", float64(total)/100))
 184
 185	checkoutbutton := doc.Call("getElementById", "checkout-button")
 186	if !checkoutbutton.Truthy() {
 187		return
 188	}
 189
 190	if len(cart) > 1 && hasShipping {
 191		checkoutbutton.Call("removeAttribute", "disabled")
 192	} else {
 193		checkoutbutton.Call("setAttribute", "disabled", "true")
 194	}
 195}
 196
 197func updateItemQuantity(this js.Value, args []js.Value) interface{} {
 198	id := args[0].String()
 199	qty, err := strconv.Atoi(args[1].String())
 200	if err != nil {
 201		log.Println(err)
 202	}
 203	for i := range cart {
 204		if cart[i].ID == id {
 205			unitPrice := cart[i].Amount / cart[i].Qty
 206			cart[i].Qty = qty
 207			cart[i].Amount = unitPrice * qty
 208			break
 209		}
 210	}
 211	saveCart()
 212	return nil
 213}
 214
 215
 216// ===== checkout.go =====
 217//go:build js && wasm
 218
 219package main
 220
 221import (
 222	"encoding/json"
 223	"fmt"
 224	"log"
 225	"strconv"
 226	"syscall/js"
 227
 228	"github.com/0magnet/m2/pkg/storepage"
 229)
 230
 231func addShippingInfo(this js.Value, args []js.Value) interface{} {
 232	event := args[0]
 233	form := args[1]
 234	event.Call("preventDefault")
 235	getFormValue := func(name string) string {
 236		return form.Call("querySelector", fmt.Sprintf("[name='%s']", name)).Get("value").String()
 237	}
 238	// Read by the shared field list, so the control names here and the ones
 239	// the form was generated from are one list. The cart line's field order
 240	// is storepage's too; this no longer spells it out.
 241	var sh storepage.Shipping
 242	for _, f := range storepage.ShippingFields {
 243		if f.Kind != storepage.FieldMoney {
 244			sh.Set(f.Name, getFormValue(f.Name))
 245		}
 246	}
 247	priceStr := getFormValue("shipping-price")
 248	price, err := strconv.ParseFloat(priceStr, 64)
 249	if err != nil {
 250		// This was price = 0.0, which quietly put free shipping in the cart
 251		// whenever the amount could not be read. The terminal refused the
 252		// same input; refusing is the answer that agrees.
 253		log.Println(wasmName+":", "Error: Failed to parse shipping price")
 254		reportShipping(form, "shipping-price", "shipping amount is not a number")
 255		return false
 256	}
 257	sh.Cents = int(price * 100)
 258	if msg := sh.Validate(); msg != "" {
 259		log.Println(wasmName+":", msg)
 260		field := "shipping-price"
 261		if sh.Email == "" || !storepage.ValidEmail(sh.Email) {
 262			field = "shipping-email"
 263		}
 264		reportShipping(form, field, msg)
 265		return false
 266	}
 267
 268	addToCart(js.Value{}, []js.Value{
 269		js.ValueOf(sh.ID()),
 270		js.ValueOf(sh.Cents),
 271		js.ValueOf(1),
 272	})
 273	return false
 274}
 275
 276// reportShipping says why a shipping line was refused, on the field it is
 277// about, where the browser already shows its own min= and required messages
 278// — so a refusal from here looks like a refusal from the form itself.
 279func reportShipping(form js.Value, name, msg string) {
 280	if !form.Truthy() {
 281		return
 282	}
 283	el := form.Call("querySelector", "[name='"+name+"']")
 284	if !el.Truthy() {
 285		return
 286	}
 287	el.Call("setCustomValidity", msg)
 288	el.Call("reportValidity")
 289	// Cleared on the next edit, or the message sticks to a field the reader
 290	// has since corrected and the form can never be submitted again.
 291	var clear js.Func
 292	clear = js.FuncOf(func(js.Value, []js.Value) any {
 293		el.Call("setCustomValidity", "")
 294		clear.Release()
 295		return nil
 296	})
 297	el.Call("addEventListener", "input", clear, map[string]any{"once": true})
 298}
 299
 300var (
 301	elements       js.Value
 302	stripeValue    js.Value
 303	stripe         js.Value
 304	checkoutStripe = doc.Call("getElementById", "stripecheckout")
 305)
 306
 307// shippingProblem says why the cart cannot be paid for yet, or "" when it
 308// can: it needs a shipping line, and that line an email to confirm the order
 309// to. The server refuses the same carts; this says so before a dialog opens.
 310func shippingProblem() string {
 311	for _, it := range cart {
 312		if !storepage.IsShippingID(it.ID) {
 313			continue
 314		}
 315		sh, ok := storepage.ParseShipping(it.ID)
 316		if !ok || !storepage.ValidEmail(sh.Email) {
 317			return "Please add an email address under Add Shipping Info — it is where your order confirmation goes."
 318		}
 319		return ""
 320	}
 321	return "Please fill in Add Shipping Info before checking out."
 322}
 323
 324func goToCheckout(this js.Value, args []js.Value) any {
 325	if msg := shippingProblem(); msg != "" {
 326		js.Global().Call("alert", msg)
 327		return nil
 328	}
 329	if stripeValue.IsUndefined() {
 330		log.Println(`js.Global().Get("Stripe")`)
 331		stripeValue = js.Global().Get("Stripe")
 332		if stripeValue.IsUndefined() {
 333			log.Println(`Stripe is undefined, attempting to load Stripe.js`)
 334
 335			doc := js.Global().Get("document")
 336			head := doc.Call("querySelector", "head")
 337			script := doc.Call("createElement", "script")
 338			script.Set("src", "https://js.stripe.com/v3/")
 339			script.Set("defer", true)
 340
 341			done := make(chan bool)
 342			script.Call("addEventListener", "load", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
 343				log.Println(wasmName+":", "Stripe.js script has been loaded")
 344				done <- true
 345				return nil
 346			}))
 347
 348			head.Call("appendChild", script)
 349
 350			<-done
 351
 352			stripeValue = js.Global().Get("Stripe")
 353			if stripeValue.IsUndefined() {
 354				log.Println(wasmName+":", "Failed to load Stripe.js")
 355				return nil
 356			}
 357		}
 358	}
 359
 360	log.Println(wasmName+":", "Stripe.js loaded successfully")
 361
 362	if stripe.IsUndefined() {
 363		log.Println(wasmName+":", "Invoking Stripe")
 364		stripe = stripeValue.Invoke(stripePK)
 365		if stripe.IsUndefined() {
 366			log.Println(wasmName+":", "Failed to invoke Stripe")
 367			return nil
 368		}
 369	}
 370
 371	log.Println(wasmName+":", "Stripe initialized")
 372	checkoutStripe = doc.Call("getElementById", "stripecheckout")
 373	if checkoutStripe.IsUndefined() {
 374		log.Println(wasmName+":", "element with ID stripecheckout not found")
 375	}
 376
 377	checkoutStripe.Call("showModal")
 378	log.Println(wasmName+":", "initializePayment()")
 379	initializePayment()
 380
 381	return nil
 382}
 383
 384func cancelCheckout(this js.Value, args []js.Value) any {
 385	log.Println(wasmName+":", "Canceling checkout ; closing dialog")
 386	checkoutStripe.Call("close")
 387	updateCartDisplay()
 388	return nil
 389}
 390
 391func initializePayment() {
 392	type cItem struct {
 393		ID     string `json:"id"`
 394		Amount int    `json:"amount"`
 395	}
 396	type checkout struct {
 397		Items []cItem `json:"items"`
 398	}
 399	payload := checkout{
 400		Items: func() []cItem {
 401			var items []cItem
 402			for _, it := range cart {
 403				items = append(items, cItem{ID: it.ID + " X " + strconv.Itoa(it.Qty), Amount: it.Amount})
 404			}
 405			return items
 406		}(),
 407	}
 408	payloadJSON, err := json.Marshal(payload)
 409	if err != nil {
 410		log.Println(wasmName+":", "Error marshaling JSON:", err)
 411		return
 412	}
 413	fetchInit := map[string]interface{}{
 414		"method": "POST",
 415		"headers": map[string]interface{}{
 416			"Content-Type": "application/json",
 417		},
 418		"body": string(payloadJSON),
 419	}
 420
 421	log.Println(wasmName+":", "fetch  /create-payment-intent")
 422	js.Global().Call("fetch", "/create-payment-intent", js.ValueOf(fetchInit)).
 423		Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
 424			response := args[0]
 425			log.Println(wasmName+":", "got response from fetch /create-payment-intent")
 426			if !response.Get("ok").Bool() {
 427				log.Println(wasmName+":", "Fetch request failed with status:", response.Get("status").Int())
 428				showMessage("Failed to create payment intent: " + response.Get("status").String())
 429				return nil
 430			}
 431			response.Call("json").Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
 432				clientSecret := args[0].Get("clientSecret").String()
 433				log.Println(wasmName+":", "Client secret received:", clientSecret)
 434				setupStripeElements(clientSecret)
 435				return nil
 436			})).Call("catch", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
 437				log.Println(wasmName+":", "Error parsing JSON response:", args[0])
 438				showMessage("Failed to parse payment intent response.")
 439				return nil
 440			}))
 441			return nil
 442		})).
 443		Call("catch", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
 444			log.Println(wasmName+":", "Error in fetch request:", args[0])
 445			showMessage("Failed to communicate with the server.")
 446			return nil
 447		}))
 448}
 449
 450func setupStripeElements(clientSecret string) {
 451	elements = stripe.Call("elements", map[string]interface{}{
 452		"clientSecret": clientSecret,
 453	})
 454	if elements.IsUndefined() {
 455		log.Println(wasmName+":", "Failed to initialize Stripe Elements")
 456		showMessage("Failed to initialize payment elements.")
 457		return
 458	}
 459	paymentElement := elements.Call("create", "payment", map[string]interface{}{
 460		"layout": "tabs",
 461	})
 462	if paymentElement.IsUndefined() {
 463		log.Println(wasmName+":", "Failed to create payment element")
 464		showMessage("Failed to create payment element.")
 465		return
 466	}
 467	paymentElement.Call("mount", "#payment-element")
 468	submitButton := doc.Call("getElementById", "submit")
 469	submitButton.Call("addEventListener", "click", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
 470		args[0].Call("preventDefault")
 471		showSpinner(true)
 472		confirmPayment(clientSecret)
 473		return nil
 474	}))
 475}
 476
 477func confirmPayment(clientSecret string) {
 478
 479	windowLocation := js.Global().Get("window").Get("location")
 480	protocol := windowLocation.Get("protocol").String()
 481	hostname := windowLocation.Get("hostname").String()
 482	port := windowLocation.Get("port").String()
 483
 484	baseURL := protocol + "//" + hostname
 485	if port != "" {
 486		baseURL += ":" + port
 487	}
 488	//	path := windowLocation.Get("pathname").String()
 489	//    baseURL += strings.Split(path, "?")[0]
 490	//    log.Println(wasmName+":","return url ", baseURL)
 491
 492	returnURL := baseURL + "/complete"
 493	returnURL += "?payment_intent=" + clientSecret // + "#complete"
 494	log.Println(wasmName+":", "Return URL for payment:", returnURL)
 495
 496	stripe.Call("confirmPayment", map[string]interface{}{
 497		"elements": elements,
 498		"confirmParams": map[string]interface{}{
 499			"return_url": returnURL,
 500		},
 501	}).Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
 502		result := args[0]
 503		if result.Get("error").IsUndefined() {
 504			log.Println(wasmName+":", "Payment successful:", result)
 505			showMessage("Payment successful! Thank you for your order.")
 506		} else {
 507			log.Println(wasmName+":", "Payment error:", result.Get("error").Get("message").String())
 508			showMessage("Payment failed: " + result.Get("error").Get("message").String())
 509		}
 510
 511		showSpinner(false)
 512		return nil
 513	}))
 514}
 515
 516func showMessage(message string) {
 517	messageElement := doc.Call("getElementById", "payment-message")
 518	messageElement.Set("innerText", message)
 519	messageElement.Set("className", "")
 520}
 521
 522func showSpinner(isLoading bool) {
 523	spinner := doc.Call("getElementById", "spinner")
 524	buttonText := doc.Call("getElementById", "button-text")
 525
 526	if isLoading {
 527		spinner.Set("className", "")
 528		buttonText.Set("className", "hidden")
 529	} else {
 530		spinner.Set("className", "hidden")
 531		buttonText.Set("className", "")
 532	}
 533}
 534
 535
 536// ===== complete.go =====
 537//go:build js && wasm
 538
 539// Package main complete.go
 540package main
 541
 542import (
 543	"encoding/json"
 544	"log"
 545	"syscall/js"
 546)
 547
 548func completeLogic() {
 549	initializeStripe()
 550}
 551
 552func initializeStripe() {
 553	if stripeValue.IsUndefined() {
 554		log.Println(`js.Global().Get("Stripe")`)
 555		stripeValue = js.Global().Get("Stripe")
 556		if stripeValue.IsUndefined() {
 557			log.Println(`Stripe is undefined, attempting to load Stripe.js`)
 558
 559			doc := js.Global().Get("document")
 560			head := doc.Call("querySelector", "head")
 561			script := doc.Call("createElement", "script")
 562			script.Set("src", "https://js.stripe.com/v3/")
 563			script.Set("defer", true)
 564
 565			done := make(chan bool)
 566			script.Call("addEventListener", "load", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
 567				log.Println(wasmName+":", "Stripe.js script has been loaded")
 568				done <- true
 569				return nil
 570			}))
 571
 572			head.Call("appendChild", script)
 573
 574			<-done
 575
 576			stripeValue = js.Global().Get("Stripe")
 577			if stripeValue.IsUndefined() {
 578				log.Println(wasmName+":", "Failed to load Stripe.js")
 579				return
 580			}
 581		}
 582	}
 583
 584	log.Println(wasmName+":", "Stripe.js loaded successfully")
 585
 586	if stripe.IsUndefined() {
 587		log.Println(wasmName+":", "Invoking Stripe")
 588		stripe = stripeValue.Invoke(stripePK)
 589		if stripe.IsUndefined() {
 590			log.Println(wasmName+":", "Failed to invoke Stripe")
 591			return
 592		}
 593	}
 594
 595	log.Println(wasmName+":", "Stripe initialized")
 596	checkStatus()
 597}
 598
 599var (
 600	successIcon = `<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
 601		<path fill-rule="evenodd" clip-rule="evenodd" d="M15.4695 0.232963C15.8241 0.561287 15.8454 1.1149 15.5171 1.46949L6.14206 11.5945C5.97228 11.7778 5.73221 11.8799 5.48237 11.8748C5.23253 11.8698 4.99677 11.7582 4.83452 11.5681L0.459523 6.44311C0.145767 6.07557 0.18937 5.52327 0.556912 5.20951C0.924454 4.89575 1.47676 4.93936 1.79051 5.3069L5.52658 9.68343L14.233 0.280522C14.5613 -0.0740672 15.1149 -0.0953599 15.4695 0.232963Z" fill="white"/>
 602	</svg>`
 603
 604	errorIcon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
 605		<path fill-rule="evenodd" clip-rule="evenodd" d="M1.25628 1.25628C1.59799 0.914573 2.15201 0.914573 2.49372 1.25628L8 6.76256L13.5063 1.25628C13.848 0.914573 14.402 0.914573 14.7437 1.25628C15.0854 1.59799 15.0854 2.15201 14.7437 2.49372L9.23744 8L14.7437 13.5063C15.0854 13.848 15.0854 14.402 14.7437 14.7437C14.402 15.0854 13.848 15.0854 13.5063 14.7437L8 9.23744L2.49372 14.7437C2.15201 15.0854 1.59799 15.0854 1.25628 14.7437C0.914573 14.402 0.914573 13.848 1.25628 13.5063L6.76256 8L1.25628 2.49372C0.914573 2.15201 0.914573 1.59799 1.25628 1.25628Z" fill="white"/>
 606	</svg>`
 607
 608	infoIcon = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
 609		<path fill-rule="evenodd" clip-rule="evenodd" d="M10 1.5H4C2.61929 1.5 1.5 2.61929 1.5 4V10C1.5 11.3807 2.61929 12.5 4 12.5H10C11.3807 12.5 12.5 11.3807 12.5 10V4C12.5 2.61929 11.3807 1.5 10 1.5ZM4 0C1.79086 0 0 1.79086 0 4V10C0 12.2091 1.79086 14 4 14H10C12.2091 14 14 12.2091 14 10V4C14 1.79086 12.2091 0 10 0H4Z" fill="white"/>
 610		<path fill-rule="evenodd" clip-rule="evenodd" d="M5.25 7C5.25 6.58579 5.58579 6.25 6 6.25H7.25C7.66421 6.25 8 6.58579 8 7V10.5C8 10.9142 7.66421 11.25 7.25 11.25C6.83579 11.25 6.5 10.9142 6.5 10.5V7.75H6C5.58579 7.75 5.25 7.41421 5.25 7Z" fill="white"/>
 611		<path d="M5.75 4C5.75 3.31075 6.31075 2.75 7 2.75C7.68925 2.75 8.25 3.31075 8.25 4C8.25 4.68925 7.68925 5.25 7 5.25C6.31075 5.25 5.75 4.68925 5.75 4Z" fill="white"/>
 612	</svg>`
 613)
 614
 615func setErrorState() {
 616	js.Global().Get("document").Call("querySelector", "#status-icon").Set("style", map[string]interface{}{"backgroundColor": "#DF1B41"})
 617	js.Global().Get("document").Call("querySelector", "#status-icon").Set("innerHTML", errorIcon)
 618	js.Global().Get("document").Call("querySelector", "#status-text").Set("textContent", "Something went wrong, please try again.")
 619	js.Global().Get("document").Call("querySelector", "#details-table").Call("classList").Call("add", "hidden")
 620	js.Global().Get("document").Call("querySelector", "#view-details").Call("classList").Call("add", "hidden")
 621}
 622
 623func checkStatus() {
 624	clientSecret := js.Global().Get("URLSearchParams").New(js.Global().Get("window").Get("location").Get("search")).Call("get", "payment_intent_client_secret").String()
 625
 626	if clientSecret == "" {
 627		setErrorState()
 628		return
 629	}
 630
 631	if stripe.IsUndefined() {
 632		log.Println(wasmName+":", "Stripe is not initialized")
 633		setErrorState()
 634		return
 635	}
 636
 637	stripe.Call("retrievePaymentIntent", clientSecret).Call("then", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
 638		paymentIntent := p[0].Get("paymentIntent")
 639		setPaymentDetails(paymentIntent)
 640		return nil
 641	})).Call("catch", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
 642		setErrorState()
 643		return nil
 644	}))
 645}
 646
 647func getAllLocalStorageData() map[string]interface{} {
 648	localStorage := js.Global().Get("localStorage")
 649	keys := js.Global().Get("Object").Call("keys", localStorage)
 650	data := make(map[string]interface{})
 651
 652	for i := 0; i < keys.Length(); i++ {
 653		key := keys.Index(i).String()
 654		value := localStorage.Call("getItem", key).String()
 655		var parsedValue interface{}
 656		err := json.Unmarshal([]byte(value), &parsedValue)
 657		if err != nil {
 658			parsedValue = value // If not JSON, store raw value
 659		}
 660		data[key] = parsedValue
 661	}
 662	return data
 663}
 664
 665func submitOrder(localStorageData map[string]interface{}, paymentIntentId string) {
 666	orderData := map[string]interface{}{
 667		"localStorageData": localStorageData,
 668		"paymentIntentId":  paymentIntentId,
 669	}
 670
 671	body, err := json.Marshal(orderData)
 672	if err != nil {
 673		log.Println(wasmName+":", "Error marshaling order data:", err)
 674		return
 675	}
 676
 677	fetch := js.Global().Get("fetch")
 678	if fetch.IsUndefined() {
 679		log.Println(wasmName+":", "Fetch API is not available")
 680		return
 681	}
 682
 683	options := map[string]interface{}{
 684		"method": "POST",
 685		"headers": map[string]interface{}{
 686			"Content-Type": "application/json",
 687		},
 688		"body": string(body),
 689	}
 690
 691	fetch.Invoke("/submit-order", js.ValueOf(options)).Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
 692		response := args[0]
 693		if !response.Get("ok").Bool() {
 694			response.Call("text").Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
 695				errText := args[0].String()
 696				log.Println(wasmName+":", "Order submit failed:", errText)
 697				js.Global().Call("alert", "Order submission failed:\n"+errText)
 698				return nil
 699			}))
 700			return nil
 701		}
 702		response.Call("json").Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
 703			data := args[0]
 704			log.Println(wasmName+":", "Order submitted successfully:", data)
 705			return nil
 706		}))
 707		return nil
 708	}))
 709}
 710
 711func setPaymentDetails(intent js.Value) {
 712	// Every path through the switch below sets this, including its default,
 713	// so there is nothing to fall back to. iconColor and icon do fall back:
 714	// the cases that only change the wording leave them red.
 715	var statusText string
 716	iconColor := "#DF1B41"
 717	icon := errorIcon
 718
 719	if !intent.IsUndefined() {
 720		intentStatus := intent.Get("status").String()
 721		intentID := intent.Get("id").String()
 722
 723		allLocalStorageData := getAllLocalStorageData()
 724
 725		switch intentStatus {
 726		case "succeeded":
 727			statusText = "Payment succeeded"
 728			iconColor = "#30B130"
 729			icon = successIcon
 730			if len(allLocalStorageData) > 0 {
 731				submitOrder(allLocalStorageData, intentID)
 732			} else {
 733				log.Println(wasmName+":", "No data found in localStorage; order not submitted.")
 734			}
 735		case "processing":
 736			statusText = "Your payment is processing."
 737			iconColor = "#6D6E78"
 738			icon = infoIcon
 739			if len(allLocalStorageData) > 0 {
 740				submitOrder(allLocalStorageData, intentID)
 741			} else {
 742				log.Println(wasmName+":", "No data found in localStorage; order not submitted.")
 743			}
 744		case "requires_payment_method":
 745			statusText = "Your payment was not successful, please try again."
 746		default:
 747			statusText = "Unknown payment status."
 748		}
 749
 750		// Update the status icon, text, and links
 751		js.Global().Get("document").Call("querySelector", "#status-icon").Set("style", map[string]interface{}{"backgroundColor": iconColor})
 752		js.Global().Get("document").Call("querySelector", "#status-icon").Set("innerHTML", icon)
 753		js.Global().Get("document").Call("querySelector", "#status-text").Set("textContent", statusText)
 754		js.Global().Get("document").Call("querySelector", "#intent-id").Set("textContent", intentID)
 755		js.Global().Get("document").Call("querySelector", "#intent-status").Set("textContent", intentStatus)
 756		js.Global().Get("document").Call("querySelector", "#view-details").Set("href", "https://dashboard.stripe.com/payments/"+intentID)
 757
 758		// Update the "Order Details" link with the paymentIntent ID
 759		orderDetailsLink := js.Global().Get("document").Call("querySelector", "#order-details-link")
 760		orderDetailsLink.Set("href", "/order/"+intentID)
 761		orderDetailsLink.Set("onclick", nil) // Allow default behavior (navigation)
 762
 763	} else {
 764		setErrorState()
 765	}
 766}
 767
 768
 769// ===== crypto.go =====
 770//go:build js && wasm
 771
 772package main
 773
 774// Paying in coin from the page.
 775//
 776// The same /create-invoice endpoint the terminal uses (pkg/tui/crypto.go);
 777// this only renders what the server decided. The server validates every
 778// line against its own catalog, prices the order, and allocates the
 779// address — nothing here is trusted with any of that.
 780
 781import (
 782	"encoding/json"
 783	"log"
 784	"strconv"
 785	"syscall/js"
 786)
 787
 788// invoiceView mirrors what pkg/web/invoice.go hands a client.
 789type invoiceView struct {
 790	ID          string `json:"id"`
 791	Coin        string `json:"coin"`
 792	Address     string `json:"address"`
 793	Amount      string `json:"amount"`
 794	URI         string `json:"uri"`
 795	Status      string `json:"status"`
 796	Paid        string `json:"paid"`
 797	Outstanding string `json:"outstanding"`
 798	TxID        string `json:"txid"`
 799	Error       string `json:"error"`
 800}
 801
 802var coinInvoiceID string
 803
 804// payWithCoin asks the store for an invoice and shows it in the checkout
 805// dialog, then polls until it settles.
 806func payWithCoin(js.Value, []js.Value) any {
 807	if len(cart) == 0 {
 808		showMessage("Your cart is empty.")
 809		return nil
 810	}
 811	if msg := shippingProblem(); msg != "" {
 812		js.Global().Call("alert", msg)
 813		return nil
 814	}
 815	type cItem struct {
 816		ID     string `json:"id"`
 817		Amount int    `json:"amount"`
 818	}
 819	var items []cItem
 820	for _, it := range cart {
 821		items = append(items, cItem{ID: it.ID + " X " + strconv.Itoa(it.Qty), Amount: it.Amount})
 822	}
 823	body, err := json.Marshal(map[string]any{"coin": "SKY", "items": items})
 824	if err != nil {
 825		showMessage("Could not build the request.")
 826		return nil
 827	}
 828
 829	checkoutStripe = doc.Call("getElementById", "stripecheckout")
 830	checkoutStripe.Call("showModal")
 831	setCoinBody("<p>Asking the store for an address…</p>")
 832
 833	fetchJSON("/create-invoice", string(body), func(inv *invoiceView, err error) {
 834		if err != nil {
 835			setCoinBody("<p>Could not create the invoice: " + err.Error() + "</p>")
 836			return
 837		}
 838		coinInvoiceID = inv.ID
 839		renderCoinInvoice(inv)
 840		pollCoinInvoice()
 841	})
 842	return nil
 843}
 844
 845// pollCoinInvoice re-reads the invoice every ten seconds while the dialog
 846// is open. The server polls the chain when asked.
 847func pollCoinInvoice() {
 848	js.Global().Call("setTimeout", js.FuncOf(func(js.Value, []js.Value) any {
 849		if coinInvoiceID == "" || !checkoutStripe.Get("open").Bool() {
 850			return nil
 851		}
 852		fetchJSON("/invoice/"+coinInvoiceID, "", func(inv *invoiceView, err error) {
 853			if err != nil {
 854				pollCoinInvoice()
 855				return
 856			}
 857			renderCoinInvoice(inv)
 858			if inv.Status != "paid" && inv.Status != "expired" {
 859				pollCoinInvoice()
 860			}
 861		})
 862		return nil
 863	}), 10000)
 864}
 865
 866func renderCoinInvoice(inv *invoiceView) {
 867	h := "<p>Send <b>" + inv.Amount + " " + inv.Coin + "</b> to:</p>" +
 868		"<p style='word-break:break-all'><code>" + inv.Address + "</code></p>" +
 869		"<p><a href='" + inv.URI + "'>open in a wallet</a></p>"
 870	switch inv.Status {
 871	case "paid":
 872		h += "<p><b>Paid — thank you.</b> Your order is printing.</p>"
 873		if inv.TxID != "" {
 874			h += "<p style='word-break:break-all'><small>txid " + inv.TxID + "</small></p>"
 875		}
 876		emptyCart(js.Value{}, nil)
 877	case "underpaid":
 878		h += "<p>Part paid: " + inv.Paid + " received, <b>" + inv.Outstanding + "</b> still owed.</p>"
 879	case "expired":
 880		h += "<p>The quote expired. Close this and start again for a fresh price.</p>"
 881	default:
 882		h += "<p>Waiting for the payment to confirm…</p>"
 883	}
 884	setCoinBody(h)
 885}
 886
 887func setCoinBody(html string) {
 888	el := doc.Call("getElementById", "checkout-container")
 889	if el.Truthy() {
 890		el.Set("innerHTML", html)
 891	}
 892}
 893
 894// fetchJSON POSTs (or GETs, with an empty body) and decodes an invoice.
 895func fetchJSON(path, body string, done func(*invoiceView, error)) {
 896	init := map[string]any{}
 897	if body != "" {
 898		init["method"] = "POST"
 899		init["headers"] = map[string]any{"Content-Type": "application/json"}
 900		init["body"] = body
 901	}
 902	js.Global().Call("fetch", path, js.ValueOf(init)).
 903		Call("then", js.FuncOf(func(_ js.Value, args []js.Value) any {
 904			args[0].Call("text").Call("then", js.FuncOf(func(_ js.Value, a []js.Value) any {
 905				var inv invoiceView
 906				if err := json.Unmarshal([]byte(a[0].String()), &inv); err != nil {
 907					done(nil, errString("the store did not answer with an invoice"))
 908					return nil
 909				}
 910				if inv.Error != "" {
 911					done(nil, errString(inv.Error))
 912					return nil
 913				}
 914				done(&inv, nil)
 915				return nil
 916			}))
 917			return nil
 918		})).
 919		Call("catch", js.FuncOf(func(_ js.Value, args []js.Value) any {
 920			log.Println(wasmName+":", "invoice fetch failed:", args[0])
 921			done(nil, errString("could not reach the store"))
 922			return nil
 923		}))
 924}
 925
 926type errString string
 927
 928func (e errString) Error() string { return string(e) }
 929
 930
 931// ===== main.go =====
 932//go:build js && wasm
 933
 934package main
 935
 936import (
 937	"log"
 938	"syscall/js"
 939)
 940
 941// set client pk on compile
 942var stripePK string
 943
 944type item struct {
 945	ID     string `json:"id"`
 946	Amount int    `json:"amount"`
 947	Qty    int    `json:"quantity"`
 948}
 949
 950var (
 951	wasmName string
 952	doc      = js.Global().Get("document")
 953	cart     []item
 954)
 955
 956func main() {
 957	ready := make(chan struct{})
 958
 959	document := js.Global().Get("document")
 960	readyState := document.Get("readyState").String()
 961	if readyState == "interactive" || readyState == "complete" {
 962		log.Println(wasmName+":", "WASM: DOM already fully loaded")
 963		close(ready)
 964	} else {
 965		cb := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
 966			log.Println(wasmName+":", "WASM: DOM fully loaded and parsed")
 967			close(ready)
 968			return nil
 969		})
 970		defer cb.Release()
 971
 972		document.Call("addEventListener", "DOMContentLoaded", cb)
 973		log.Println(wasmName+":", "WASM: waiting for DOM to load")
 974	}
 975
 976	<-ready
 977
 978	c := make(chan struct{})
 979	if stripePK == "" {
 980		log.Fatal("Stripe PK not found!")
 981	}
 982	window := js.Global().Get("window")
 983	location := window.Get("location")
 984	pathname := location.Get("pathname").String()
 985
 986	switch pathname {
 987	case "/complete":
 988		completeLogic()
 989	default:
 990		defaultLogic()
 991	}
 992	<-c
 993}
 994
 995func defaultLogic() {
 996	js.Global().Set("addToCart", js.FuncOf(addUnToCart))
 997	js.Global().Set("clearStorage", js.FuncOf(clearAll))
 998	js.Global().Set("emptyCart", js.FuncOf(emptyCart))
 999	js.Global().Set("updateItemQuantity", js.FuncOf(updateItemQuantity))
1000	js.Global().Set("removeFromCart", js.FuncOf(removeFromCart))
1001	js.Global().Set("addShippingInfo", js.FuncOf(addShippingInfo))
1002	js.Global().Set("goToCheckout", js.FuncOf(goToCheckout))
1003	js.Global().Set("payWithCoin", js.FuncOf(payWithCoin))
1004	js.Global().Set("cancelCheckout", js.FuncOf(cancelCheckout))
1005	js.Global().Set("callUpdateCartDisplay", js.FuncOf(updateCartDisplayWrapper))
1006	loadCart()
1007	updateCartDisplay()
1008}
1009
1010