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 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		reportShipping(form, msg)
261		return false
262	}
263
264	addToCart(js.Value{}, []js.Value{
265		js.ValueOf(sh.ID()),
266		js.ValueOf(sh.Cents),
267		js.ValueOf(1),
268	})
269	return false
270}
271
272// reportShipping says why a shipping line was refused, on the amount field
273// where the browser already shows its own min= message — so a refusal from
274// here looks like a refusal from the form itself.
275func reportShipping(form js.Value, msg string) {
276	if !form.Truthy() {
277		return
278	}
279	el := form.Call("querySelector", "[name='shipping-price']")
280	if !el.Truthy() {
281		return
282	}
283	el.Call("setCustomValidity", msg)
284	el.Call("reportValidity")
285	// Cleared on the next edit, or the message sticks to a field the reader
286	// has since corrected and the form can never be submitted again.
287	var clear js.Func
288	clear = js.FuncOf(func(js.Value, []js.Value) any {
289		el.Call("setCustomValidity", "")
290		clear.Release()
291		return nil
292	})
293	el.Call("addEventListener", "input", clear, map[string]any{"once": true})
294}
295
296var (
297	elements       js.Value
298	stripeValue    js.Value
299	stripe         js.Value
300	checkoutStripe = doc.Call("getElementById", "stripecheckout")
301)
302
303func goToCheckout(this js.Value, args []js.Value) any {
304	if stripeValue.IsUndefined() {
305		log.Println(`js.Global().Get("Stripe")`)
306		stripeValue = js.Global().Get("Stripe")
307		if stripeValue.IsUndefined() {
308			log.Println(`Stripe is undefined, attempting to load Stripe.js`)
309
310			doc := js.Global().Get("document")
311			head := doc.Call("querySelector", "head")
312			script := doc.Call("createElement", "script")
313			script.Set("src", "https://js.stripe.com/v3/")
314			script.Set("defer", true)
315
316			done := make(chan bool)
317			script.Call("addEventListener", "load", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
318				log.Println(wasmName+":", "Stripe.js script has been loaded")
319				done <- true
320				return nil
321			}))
322
323			head.Call("appendChild", script)
324
325			<-done
326
327			stripeValue = js.Global().Get("Stripe")
328			if stripeValue.IsUndefined() {
329				log.Println(wasmName+":", "Failed to load Stripe.js")
330				return nil
331			}
332		}
333	}
334
335	log.Println(wasmName+":", "Stripe.js loaded successfully")
336
337	if stripe.IsUndefined() {
338		log.Println(wasmName+":", "Invoking Stripe")
339		stripe = stripeValue.Invoke(stripePK)
340		if stripe.IsUndefined() {
341			log.Println(wasmName+":", "Failed to invoke Stripe")
342			return nil
343		}
344	}
345
346	log.Println(wasmName+":", "Stripe initialized")
347	checkoutStripe = doc.Call("getElementById", "stripecheckout")
348	if checkoutStripe.IsUndefined() {
349		log.Println(wasmName+":", "element with ID stripecheckout not found")
350	}
351
352	checkoutStripe.Call("showModal")
353	log.Println(wasmName+":", "initializePayment()")
354	initializePayment()
355
356	return nil
357}
358
359func cancelCheckout(this js.Value, args []js.Value) any {
360	log.Println(wasmName+":", "Canceling checkout ; closing dialog")
361	checkoutStripe.Call("close")
362	updateCartDisplay()
363	return nil
364}
365
366func initializePayment() {
367	type cItem struct {
368		ID     string `json:"id"`
369		Amount int    `json:"amount"`
370	}
371	type checkout struct {
372		Items []cItem `json:"items"`
373	}
374	payload := checkout{
375		Items: func() []cItem {
376			var items []cItem
377			for _, it := range cart {
378				items = append(items, cItem{ID: it.ID + " X " + strconv.Itoa(it.Qty), Amount: it.Amount})
379			}
380			return items
381		}(),
382	}
383	payloadJSON, err := json.Marshal(payload)
384	if err != nil {
385		log.Println(wasmName+":", "Error marshaling JSON:", err)
386		return
387	}
388	fetchInit := map[string]interface{}{
389		"method": "POST",
390		"headers": map[string]interface{}{
391			"Content-Type": "application/json",
392		},
393		"body": string(payloadJSON),
394	}
395
396	log.Println(wasmName+":", "fetch  /create-payment-intent")
397	js.Global().Call("fetch", "/create-payment-intent", js.ValueOf(fetchInit)).
398		Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
399			response := args[0]
400			log.Println(wasmName+":", "got response from fetch /create-payment-intent")
401			if !response.Get("ok").Bool() {
402				log.Println(wasmName+":", "Fetch request failed with status:", response.Get("status").Int())
403				showMessage("Failed to create payment intent: " + response.Get("status").String())
404				return nil
405			}
406			response.Call("json").Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
407				clientSecret := args[0].Get("clientSecret").String()
408				log.Println(wasmName+":", "Client secret received:", clientSecret)
409				setupStripeElements(clientSecret)
410				return nil
411			})).Call("catch", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
412				log.Println(wasmName+":", "Error parsing JSON response:", args[0])
413				showMessage("Failed to parse payment intent response.")
414				return nil
415			}))
416			return nil
417		})).
418		Call("catch", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
419			log.Println(wasmName+":", "Error in fetch request:", args[0])
420			showMessage("Failed to communicate with the server.")
421			return nil
422		}))
423}
424
425func setupStripeElements(clientSecret string) {
426	elements = stripe.Call("elements", map[string]interface{}{
427		"clientSecret": clientSecret,
428	})
429	if elements.IsUndefined() {
430		log.Println(wasmName+":", "Failed to initialize Stripe Elements")
431		showMessage("Failed to initialize payment elements.")
432		return
433	}
434	paymentElement := elements.Call("create", "payment", map[string]interface{}{
435		"layout": "tabs",
436	})
437	if paymentElement.IsUndefined() {
438		log.Println(wasmName+":", "Failed to create payment element")
439		showMessage("Failed to create payment element.")
440		return
441	}
442	paymentElement.Call("mount", "#payment-element")
443	submitButton := doc.Call("getElementById", "submit")
444	submitButton.Call("addEventListener", "click", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
445		args[0].Call("preventDefault")
446		showSpinner(true)
447		confirmPayment(clientSecret)
448		return nil
449	}))
450}
451
452func confirmPayment(clientSecret string) {
453
454	windowLocation := js.Global().Get("window").Get("location")
455	protocol := windowLocation.Get("protocol").String()
456	hostname := windowLocation.Get("hostname").String()
457	port := windowLocation.Get("port").String()
458
459	baseURL := protocol + "//" + hostname
460	if port != "" {
461		baseURL += ":" + port
462	}
463	//	path := windowLocation.Get("pathname").String()
464	//    baseURL += strings.Split(path, "?")[0]
465	//    log.Println(wasmName+":","return url ", baseURL)
466
467	returnURL := baseURL + "/complete"
468	returnURL += "?payment_intent=" + clientSecret // + "#complete"
469	log.Println(wasmName+":", "Return URL for payment:", returnURL)
470
471	stripe.Call("confirmPayment", map[string]interface{}{
472		"elements": elements,
473		"confirmParams": map[string]interface{}{
474			"return_url": returnURL,
475		},
476	}).Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
477		result := args[0]
478		if result.Get("error").IsUndefined() {
479			log.Println(wasmName+":", "Payment successful:", result)
480			showMessage("Payment successful! Thank you for your order.")
481		} else {
482			log.Println(wasmName+":", "Payment error:", result.Get("error").Get("message").String())
483			showMessage("Payment failed: " + result.Get("error").Get("message").String())
484		}
485
486		showSpinner(false)
487		return nil
488	}))
489}
490
491func showMessage(message string) {
492	messageElement := doc.Call("getElementById", "payment-message")
493	messageElement.Set("innerText", message)
494	messageElement.Set("className", "")
495}
496
497func showSpinner(isLoading bool) {
498	spinner := doc.Call("getElementById", "spinner")
499	buttonText := doc.Call("getElementById", "button-text")
500
501	if isLoading {
502		spinner.Set("className", "")
503		buttonText.Set("className", "hidden")
504	} else {
505		spinner.Set("className", "hidden")
506		buttonText.Set("className", "")
507	}
508}
509
510
511// ===== complete.go =====
512//go:build js && wasm
513
514// Package main complete.go
515package main
516
517import (
518	"encoding/json"
519	"log"
520	"syscall/js"
521)
522
523func completeLogic() {
524	initializeStripe()
525}
526
527func initializeStripe() {
528	if stripeValue.IsUndefined() {
529		log.Println(`js.Global().Get("Stripe")`)
530		stripeValue = js.Global().Get("Stripe")
531		if stripeValue.IsUndefined() {
532			log.Println(`Stripe is undefined, attempting to load Stripe.js`)
533
534			doc := js.Global().Get("document")
535			head := doc.Call("querySelector", "head")
536			script := doc.Call("createElement", "script")
537			script.Set("src", "https://js.stripe.com/v3/")
538			script.Set("defer", true)
539
540			done := make(chan bool)
541			script.Call("addEventListener", "load", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
542				log.Println(wasmName+":", "Stripe.js script has been loaded")
543				done <- true
544				return nil
545			}))
546
547			head.Call("appendChild", script)
548
549			<-done
550
551			stripeValue = js.Global().Get("Stripe")
552			if stripeValue.IsUndefined() {
553				log.Println(wasmName+":", "Failed to load Stripe.js")
554				return
555			}
556		}
557	}
558
559	log.Println(wasmName+":", "Stripe.js loaded successfully")
560
561	if stripe.IsUndefined() {
562		log.Println(wasmName+":", "Invoking Stripe")
563		stripe = stripeValue.Invoke(stripePK)
564		if stripe.IsUndefined() {
565			log.Println(wasmName+":", "Failed to invoke Stripe")
566			return
567		}
568	}
569
570	log.Println(wasmName+":", "Stripe initialized")
571	checkStatus()
572}
573
574var (
575	successIcon = `<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
576		<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"/>
577	</svg>`
578
579	errorIcon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
580		<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"/>
581	</svg>`
582
583	infoIcon = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
584		<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"/>
585		<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"/>
586		<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"/>
587	</svg>`
588)
589
590func setErrorState() {
591	js.Global().Get("document").Call("querySelector", "#status-icon").Set("style", map[string]interface{}{"backgroundColor": "#DF1B41"})
592	js.Global().Get("document").Call("querySelector", "#status-icon").Set("innerHTML", errorIcon)
593	js.Global().Get("document").Call("querySelector", "#status-text").Set("textContent", "Something went wrong, please try again.")
594	js.Global().Get("document").Call("querySelector", "#details-table").Call("classList").Call("add", "hidden")
595	js.Global().Get("document").Call("querySelector", "#view-details").Call("classList").Call("add", "hidden")
596}
597
598func checkStatus() {
599	clientSecret := js.Global().Get("URLSearchParams").New(js.Global().Get("window").Get("location").Get("search")).Call("get", "payment_intent_client_secret").String()
600
601	if clientSecret == "" {
602		setErrorState()
603		return
604	}
605
606	if stripe.IsUndefined() {
607		log.Println(wasmName+":", "Stripe is not initialized")
608		setErrorState()
609		return
610	}
611
612	stripe.Call("retrievePaymentIntent", clientSecret).Call("then", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
613		paymentIntent := p[0].Get("paymentIntent")
614		setPaymentDetails(paymentIntent)
615		return nil
616	})).Call("catch", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
617		setErrorState()
618		return nil
619	}))
620}
621
622func getAllLocalStorageData() map[string]interface{} {
623	localStorage := js.Global().Get("localStorage")
624	keys := js.Global().Get("Object").Call("keys", localStorage)
625	data := make(map[string]interface{})
626
627	for i := 0; i < keys.Length(); i++ {
628		key := keys.Index(i).String()
629		value := localStorage.Call("getItem", key).String()
630		var parsedValue interface{}
631		err := json.Unmarshal([]byte(value), &parsedValue)
632		if err != nil {
633			parsedValue = value // If not JSON, store raw value
634		}
635		data[key] = parsedValue
636	}
637	return data
638}
639
640func submitOrder(localStorageData map[string]interface{}, paymentIntentId string) {
641	orderData := map[string]interface{}{
642		"localStorageData": localStorageData,
643		"paymentIntentId":  paymentIntentId,
644	}
645
646	body, err := json.Marshal(orderData)
647	if err != nil {
648		log.Println(wasmName+":", "Error marshaling order data:", err)
649		return
650	}
651
652	fetch := js.Global().Get("fetch")
653	if fetch.IsUndefined() {
654		log.Println(wasmName+":", "Fetch API is not available")
655		return
656	}
657
658	options := map[string]interface{}{
659		"method": "POST",
660		"headers": map[string]interface{}{
661			"Content-Type": "application/json",
662		},
663		"body": string(body),
664	}
665
666	fetch.Invoke("/submit-order", js.ValueOf(options)).Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
667		response := args[0]
668		if !response.Get("ok").Bool() {
669			response.Call("text").Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
670				errText := args[0].String()
671				log.Println(wasmName+":", "Order submit failed:", errText)
672				js.Global().Call("alert", "Order submission failed:\n"+errText)
673				return nil
674			}))
675			return nil
676		}
677		response.Call("json").Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
678			data := args[0]
679			log.Println(wasmName+":", "Order submitted successfully:", data)
680			return nil
681		}))
682		return nil
683	}))
684}
685
686func setPaymentDetails(intent js.Value) {
687	// Every path through the switch below sets this, including its default,
688	// so there is nothing to fall back to. iconColor and icon do fall back:
689	// the cases that only change the wording leave them red.
690	var statusText string
691	iconColor := "#DF1B41"
692	icon := errorIcon
693
694	if !intent.IsUndefined() {
695		intentStatus := intent.Get("status").String()
696		intentID := intent.Get("id").String()
697
698		allLocalStorageData := getAllLocalStorageData()
699
700		switch intentStatus {
701		case "succeeded":
702			statusText = "Payment succeeded"
703			iconColor = "#30B130"
704			icon = successIcon
705			if len(allLocalStorageData) > 0 {
706				submitOrder(allLocalStorageData, intentID)
707			} else {
708				log.Println(wasmName+":", "No data found in localStorage; order not submitted.")
709			}
710		case "processing":
711			statusText = "Your payment is processing."
712			iconColor = "#6D6E78"
713			icon = infoIcon
714			if len(allLocalStorageData) > 0 {
715				submitOrder(allLocalStorageData, intentID)
716			} else {
717				log.Println(wasmName+":", "No data found in localStorage; order not submitted.")
718			}
719		case "requires_payment_method":
720			statusText = "Your payment was not successful, please try again."
721		default:
722			statusText = "Unknown payment status."
723		}
724
725		// Update the status icon, text, and links
726		js.Global().Get("document").Call("querySelector", "#status-icon").Set("style", map[string]interface{}{"backgroundColor": iconColor})
727		js.Global().Get("document").Call("querySelector", "#status-icon").Set("innerHTML", icon)
728		js.Global().Get("document").Call("querySelector", "#status-text").Set("textContent", statusText)
729		js.Global().Get("document").Call("querySelector", "#intent-id").Set("textContent", intentID)
730		js.Global().Get("document").Call("querySelector", "#intent-status").Set("textContent", intentStatus)
731		js.Global().Get("document").Call("querySelector", "#view-details").Set("href", "https://dashboard.stripe.com/payments/"+intentID)
732
733		// Update the "Order Details" link with the paymentIntent ID
734		orderDetailsLink := js.Global().Get("document").Call("querySelector", "#order-details-link")
735		orderDetailsLink.Set("href", "/order/"+intentID)
736		orderDetailsLink.Set("onclick", nil) // Allow default behavior (navigation)
737
738	} else {
739		setErrorState()
740	}
741}
742
743
744// ===== crypto.go =====
745//go:build js && wasm
746
747package main
748
749// Paying in coin from the page.
750//
751// The same /create-invoice endpoint the terminal uses (pkg/tui/crypto.go);
752// this only renders what the server decided. The server validates every
753// line against its own catalog, prices the order, and allocates the
754// address — nothing here is trusted with any of that.
755
756import (
757	"encoding/json"
758	"log"
759	"strconv"
760	"syscall/js"
761)
762
763// invoiceView mirrors what pkg/web/invoice.go hands a client.
764type invoiceView struct {
765	ID          string `json:"id"`
766	Coin        string `json:"coin"`
767	Address     string `json:"address"`
768	Amount      string `json:"amount"`
769	URI         string `json:"uri"`
770	Status      string `json:"status"`
771	Paid        string `json:"paid"`
772	Outstanding string `json:"outstanding"`
773	TxID        string `json:"txid"`
774	Error       string `json:"error"`
775}
776
777var coinInvoiceID string
778
779// payWithCoin asks the store for an invoice and shows it in the checkout
780// dialog, then polls until it settles.
781func payWithCoin(js.Value, []js.Value) any {
782	if len(cart) == 0 {
783		showMessage("Your cart is empty.")
784		return nil
785	}
786	type cItem struct {
787		ID     string `json:"id"`
788		Amount int    `json:"amount"`
789	}
790	var items []cItem
791	for _, it := range cart {
792		items = append(items, cItem{ID: it.ID + " X " + strconv.Itoa(it.Qty), Amount: it.Amount})
793	}
794	body, err := json.Marshal(map[string]any{"coin": "SKY", "items": items})
795	if err != nil {
796		showMessage("Could not build the request.")
797		return nil
798	}
799
800	checkoutStripe = doc.Call("getElementById", "stripecheckout")
801	checkoutStripe.Call("showModal")
802	setCoinBody("<p>Asking the store for an address…</p>")
803
804	fetchJSON("/create-invoice", string(body), func(inv *invoiceView, err error) {
805		if err != nil {
806			setCoinBody("<p>Could not create the invoice: " + err.Error() + "</p>")
807			return
808		}
809		coinInvoiceID = inv.ID
810		renderCoinInvoice(inv)
811		pollCoinInvoice()
812	})
813	return nil
814}
815
816// pollCoinInvoice re-reads the invoice every ten seconds while the dialog
817// is open. The server polls the chain when asked.
818func pollCoinInvoice() {
819	js.Global().Call("setTimeout", js.FuncOf(func(js.Value, []js.Value) any {
820		if coinInvoiceID == "" || !checkoutStripe.Get("open").Bool() {
821			return nil
822		}
823		fetchJSON("/invoice/"+coinInvoiceID, "", func(inv *invoiceView, err error) {
824			if err != nil {
825				pollCoinInvoice()
826				return
827			}
828			renderCoinInvoice(inv)
829			if inv.Status != "paid" && inv.Status != "expired" {
830				pollCoinInvoice()
831			}
832		})
833		return nil
834	}), 10000)
835}
836
837func renderCoinInvoice(inv *invoiceView) {
838	h := "<p>Send <b>" + inv.Amount + " " + inv.Coin + "</b> to:</p>" +
839		"<p style='word-break:break-all'><code>" + inv.Address + "</code></p>" +
840		"<p><a href='" + inv.URI + "'>open in a wallet</a></p>"
841	switch inv.Status {
842	case "paid":
843		h += "<p><b>Paid — thank you.</b> Your order is printing.</p>"
844		if inv.TxID != "" {
845			h += "<p style='word-break:break-all'><small>txid " + inv.TxID + "</small></p>"
846		}
847		emptyCart(js.Value{}, nil)
848	case "underpaid":
849		h += "<p>Part paid: " + inv.Paid + " received, <b>" + inv.Outstanding + "</b> still owed.</p>"
850	case "expired":
851		h += "<p>The quote expired. Close this and start again for a fresh price.</p>"
852	default:
853		h += "<p>Waiting for the payment to confirm…</p>"
854	}
855	setCoinBody(h)
856}
857
858func setCoinBody(html string) {
859	el := doc.Call("getElementById", "checkout-container")
860	if el.Truthy() {
861		el.Set("innerHTML", html)
862	}
863}
864
865// fetchJSON POSTs (or GETs, with an empty body) and decodes an invoice.
866func fetchJSON(path, body string, done func(*invoiceView, error)) {
867	init := map[string]any{}
868	if body != "" {
869		init["method"] = "POST"
870		init["headers"] = map[string]any{"Content-Type": "application/json"}
871		init["body"] = body
872	}
873	js.Global().Call("fetch", path, js.ValueOf(init)).
874		Call("then", js.FuncOf(func(_ js.Value, args []js.Value) any {
875			args[0].Call("text").Call("then", js.FuncOf(func(_ js.Value, a []js.Value) any {
876				var inv invoiceView
877				if err := json.Unmarshal([]byte(a[0].String()), &inv); err != nil {
878					done(nil, errString("the store did not answer with an invoice"))
879					return nil
880				}
881				if inv.Error != "" {
882					done(nil, errString(inv.Error))
883					return nil
884				}
885				done(&inv, nil)
886				return nil
887			}))
888			return nil
889		})).
890		Call("catch", js.FuncOf(func(_ js.Value, args []js.Value) any {
891			log.Println(wasmName+":", "invoice fetch failed:", args[0])
892			done(nil, errString("could not reach the store"))
893			return nil
894		}))
895}
896
897type errString string
898
899func (e errString) Error() string { return string(e) }
900
901
902// ===== main.go =====
903//go:build js && wasm
904
905package main
906
907import (
908	"log"
909	"syscall/js"
910)
911
912// set client pk on compile
913var stripePK string
914
915type item struct {
916	ID     string `json:"id"`
917	Amount int    `json:"amount"`
918	Qty    int    `json:"quantity"`
919}
920
921var (
922	wasmName string
923	doc      = js.Global().Get("document")
924	cart     []item
925)
926
927func main() {
928	ready := make(chan struct{})
929
930	document := js.Global().Get("document")
931	readyState := document.Get("readyState").String()
932	if readyState == "interactive" || readyState == "complete" {
933		log.Println(wasmName+":", "WASM: DOM already fully loaded")
934		close(ready)
935	} else {
936		cb := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
937			log.Println(wasmName+":", "WASM: DOM fully loaded and parsed")
938			close(ready)
939			return nil
940		})
941		defer cb.Release()
942
943		document.Call("addEventListener", "DOMContentLoaded", cb)
944		log.Println(wasmName+":", "WASM: waiting for DOM to load")
945	}
946
947	<-ready
948
949	c := make(chan struct{})
950	if stripePK == "" {
951		log.Fatal("Stripe PK not found!")
952	}
953	window := js.Global().Get("window")
954	location := window.Get("location")
955	pathname := location.Get("pathname").String()
956
957	switch pathname {
958	case "/complete":
959		completeLogic()
960	default:
961		defaultLogic()
962	}
963	<-c
964}
965
966func defaultLogic() {
967	js.Global().Set("addToCart", js.FuncOf(addUnToCart))
968	js.Global().Set("clearStorage", js.FuncOf(clearAll))
969	js.Global().Set("emptyCart", js.FuncOf(emptyCart))
970	js.Global().Set("updateItemQuantity", js.FuncOf(updateItemQuantity))
971	js.Global().Set("removeFromCart", js.FuncOf(removeFromCart))
972	js.Global().Set("addShippingInfo", js.FuncOf(addShippingInfo))
973	js.Global().Set("goToCheckout", js.FuncOf(goToCheckout))
974	js.Global().Set("payWithCoin", js.FuncOf(payWithCoin))
975	js.Global().Set("cancelCheckout", js.FuncOf(cancelCheckout))
976	js.Global().Set("callUpdateCartDisplay", js.FuncOf(updateCartDisplayWrapper))
977	loadCart()
978	updateCartDisplay()
979}
980
981