// screens-payment.jsx — simulated Stripe checkout + confirmation. Exports: PaymentScreen, ConfirmationScreen function luhnish(num) { const s = (num || "").replace(/\s/g, ""); return /^[0-9]{13,19}$/.test(s); } function formatCard(v) { return (v || "").replace(/\D/g, "").slice(0, 19).replace(/(.{4})/g, "$1 ").trim(); } function formatExp(v) { const d = (v || "").replace(/\D/g, "").slice(0, 4); if (d.length <= 2) return d; return d.slice(0, 2) + "/" + d.slice(2); } function validExp(v) { const m = (v || "").match(/^(\d{2})\/(\d{2})$/); if (!m) return false; const mo = +m[1]; return mo >= 1 && mo <= 12; } function PayField({ label, error, children, full }) { return React.createElement(window.Field, { label, error }, children); } function PaymentScreen({ t, lang, cart, prefill, onPaid, onBack }) { const [d, setD] = React.useState(() => prefill || { name: "", email: "", address: "", address2: "", zip: "", city: "", country: lang === "en" ? "France" : "France", card: "", exp: "", cvc: "", cardName: "", }); const [touched, setTouched] = React.useState({}); const [attempt, setAttempt] = React.useState(false); const [processing, setProcessing] = React.useState(false); const set = (k, v) => setD((s) => ({ ...s, [k]: v })); const blur = (k) => setTouched((tt) => ({ ...tt, [k]: true })); const isDemo = window.paymentsIsDemo ? window.paymentsIsDemo() : true; const errs = {}; if (!d.name.trim()) errs.name = t("err_required"); if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(d.email.trim())) errs.email = d.email ? t("err_email") : t("err_required"); if (!d.address.trim()) errs.address = t("err_required"); if (!d.zip.trim()) errs.zip = t("err_required"); if (!d.city.trim()) errs.city = t("err_required"); if (isDemo) { // Card is only collected in demo mode; with Stripe, the card is entered on // Stripe's own secure page, so we don't validate it here. if (!luhnish(d.card)) errs.card = d.card ? t("err_card") : t("err_required"); if (!validExp(d.exp)) errs.exp = d.exp ? t("err_exp") : t("err_required"); if (!/^[0-9]{3,4}$/.test(d.cvc)) errs.cvc = d.cvc ? t("err_cvc") : t("err_required"); } const showErr = (k) => (touched[k] || attempt) && errs[k]; function pay() { setAttempt(true); if (Object.keys(errs).length) { window.scrollTo({ top: 0, behavior: "smooth" }); return; } setProcessing(true); if (isDemo) { setTimeout(() => { onPaid(d); }, 1600); return; } // Real payment: hand off to Stripe (redirects the browser). window.startCheckout({ items: cart.items, customer: d, total: cart.subtotal, lang }) .then((r) => { if (!r || !r.redirected) { setProcessing(false); } }) .catch(() => { setProcessing(false); alert(lang === "en" ? "Payment could not be started. Please try again." : "Le paiement n'a pas pu démarrer. Merci de réessayer."); }); } const total = cart.subtotal; return React.createElement( "div", { className: "screen pay-page" }, React.createElement("div", { className: "container", style: { paddingTop: 28, paddingBottom: 90 } }, React.createElement("button", { className: "btn btn--quiet", style: { marginLeft: -14, marginBottom: 6 }, onClick: onBack }, "‹ " + t("cart_title")), React.createElement("h1", { className: "h2", style: { marginBottom: 6 } }, t("pay_title")), React.createElement("div", { className: "pill", style: { marginBottom: 22 } }, React.createElement("span", { className: "dot" }), isDemo ? t("pay_test") : t("pay_stripe_badge")), React.createElement("div", { className: "pay-layout" }, // form React.createElement("div", { className: "pay-form" }, // contact React.createElement("div", { className: "card pay-block" }, React.createElement("div", { className: "pay-block__title" }, t("pay_contact")), React.createElement("div", { className: "row-2" }, React.createElement(window.Field, { label: t("pay_name"), error: showErr("name") ? errs.name : null }, React.createElement("input", { className: "input" + (showErr("name") ? " has-error" : ""), value: d.name, onChange: (e) => set("name", e.target.value), onBlur: () => blur("name"), placeholder: "Marie Durand" })), React.createElement(window.Field, { label: t("pay_email"), error: showErr("email") ? errs.email : null }, React.createElement("input", { className: "input" + (showErr("email") ? " has-error" : ""), value: d.email, onChange: (e) => set("email", e.target.value), onBlur: () => blur("email"), placeholder: "marie@email.com", inputMode: "email" })), ), ), // shipping React.createElement("div", { className: "card pay-block" }, React.createElement("div", { className: "pay-block__title" }, t("pay_ship")), React.createElement(window.Field, { label: t("pay_address"), error: showErr("address") ? errs.address : null }, React.createElement("input", { className: "input" + (showErr("address") ? " has-error" : ""), value: d.address, onChange: (e) => set("address", e.target.value), onBlur: () => blur("address"), placeholder: lang === "en" ? "12 Rue de la Paix" : "12 Rue de la Paix" })), React.createElement("div", { style: { marginTop: 14 } }, React.createElement(window.Field, { label: t("pay_address2"), opt: t("optional") }, React.createElement("input", { className: "input", value: d.address2, onChange: (e) => set("address2", e.target.value) }))), React.createElement("div", { className: "pay-grid-3", style: { marginTop: 14 } }, React.createElement(window.Field, { label: t("pay_zip"), error: showErr("zip") ? errs.zip : null }, React.createElement("input", { className: "input" + (showErr("zip") ? " has-error" : ""), value: d.zip, onChange: (e) => set("zip", e.target.value), onBlur: () => blur("zip"), placeholder: "75002" })), React.createElement(window.Field, { label: t("pay_city"), error: showErr("city") ? errs.city : null }, React.createElement("input", { className: "input" + (showErr("city") ? " has-error" : ""), value: d.city, onChange: (e) => set("city", e.target.value), onBlur: () => blur("city"), placeholder: "Paris" })), React.createElement(window.Field, { label: t("pay_country") }, React.createElement("input", { className: "input", value: d.country, onChange: (e) => set("country", e.target.value) })), ), ), // payment (card fields only in demo — Stripe collects the card itself) isDemo ? React.createElement("div", { className: "card pay-block" }, React.createElement("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" } }, React.createElement("div", { className: "pay-block__title", style: { margin: 0 } }, t("pay_method")), React.createElement("div", { className: "cardbrands" }, ["#1A1F71", "#EB001B", "#F79E1B"].map((c, i) => React.createElement("span", { key: i, style: { width: 26, height: 17, borderRadius: 3, background: c, display: "inline-block", opacity: .9, marginLeft: i ? -6 : 0 } })) ), ), React.createElement("div", { style: { marginTop: 14 } }, React.createElement(window.Field, { label: t("pay_card"), error: showErr("card") ? errs.card : null }, React.createElement("input", { className: "input" + (showErr("card") ? " has-error" : ""), value: d.card, onChange: (e) => set("card", formatCard(e.target.value)), onBlur: () => blur("card"), placeholder: "4242 4242 4242 4242", inputMode: "numeric" }))), React.createElement("div", { className: "row-2", style: { marginTop: 14 } }, React.createElement(window.Field, { label: t("pay_exp"), error: showErr("exp") ? errs.exp : null }, React.createElement("input", { className: "input" + (showErr("exp") ? " has-error" : ""), value: d.exp, onChange: (e) => set("exp", formatExp(e.target.value)), onBlur: () => blur("exp"), placeholder: "12/27", inputMode: "numeric" })), React.createElement(window.Field, { label: t("pay_cvc"), error: showErr("cvc") ? errs.cvc : null }, React.createElement("input", { className: "input" + (showErr("cvc") ? " has-error" : ""), value: d.cvc, onChange: (e) => set("cvc", e.target.value.replace(/\D/g, "").slice(0, 4)), onBlur: () => blur("cvc"), placeholder: "123", inputMode: "numeric" })), ), React.createElement("div", { style: { marginTop: 14 } }, React.createElement(window.Field, { label: t("pay_cardname") }, React.createElement("input", { className: "input", value: d.cardName, onChange: (e) => set("cardName", e.target.value), placeholder: "MARIE DURAND" }))), ) : null, ), // summary React.createElement("div", { className: "pay-summary" }, React.createElement("div", { className: "card", style: { padding: 22, position: "sticky", top: 76 } }, React.createElement("div", { style: { fontWeight: 600, fontSize: 17, marginBottom: 14 } }, t("pay_summary")), cart.items.map((it) => { const prod = window.PRODUCTS.find((p) => p.id === it.productId) || window.PRODUCTS[0]; const u0 = (it.units && it.units[0]) || {}; return React.createElement("div", { key: it.lineId, style: { display: "flex", gap: 11, alignItems: "center", padding: "8px 0" } }, React.createElement("div", { style: { borderRadius: 8, overflow: "hidden", flexShrink: 0, border: "1px solid var(--line)" } }, React.createElement(window.QRPreview, { text: window.buildQRPayload(u0), plate: window.findFilament(u0.plate).hex, code: window.findFilament(u0.code).hex, size: 44 })), React.createElement("div", { style: { flex: 1, minWidth: 0 } }, React.createElement("div", { style: { fontSize: 13.5, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, lang === "en" ? prod.name_en : prod.name_fr), React.createElement("div", { className: "faint", style: { fontSize: 12 } }, t("qty") + " " + it.qty)), React.createElement("div", { style: { fontSize: 13.5, fontWeight: 600 } }, window.fmtPrice(it.price * it.qty, lang)), ); }), React.createElement("hr", { className: "hr", style: { margin: "12px 0" } }), React.createElement(window.SummaryRow, { label: t("cart_shipping"), value: t("cart_free"), accent: true }), React.createElement(window.SummaryRow, { label: t("cart_total"), value: window.fmtPrice(total, lang), big: true }), React.createElement("button", { className: "btn btn--primary btn--block", style: { marginTop: 16 }, onClick: pay, disabled: processing }, processing ? (isDemo ? t("pay_processing") : t("pay_redirect")) : (isDemo ? (t("pay_btn") + " " + window.fmtPrice(total, lang)) : (t("pay_btn_stripe") + " " + window.fmtPrice(total, lang)))), React.createElement("div", { className: "secure-note" }, React.createElement("span", null, "🔒"), t("pay_secure")), ) ), ), ), ); } function ConfirmationScreen({ t, lang, order, onHome }) { if (!order) return null; const prod = window.PRODUCTS.find((p) => p.id === order.items[0].productId); return React.createElement( "div", { className: "screen container", style: { paddingTop: 50, paddingBottom: 120, maxWidth: 640, textAlign: "center" } }, React.createElement("div", { className: "conf-check" }, "✓"), React.createElement("h1", { className: "h2", style: { marginTop: 22 } }, t("conf_title")), React.createElement("p", { className: "lead", style: { margin: "12px auto 0", maxWidth: 460 } }, t("conf_body")), React.createElement("div", { className: "card", style: { padding: 24, marginTop: 30, textAlign: "left" } }, React.createElement("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 } }, React.createElement("span", { className: "muted", style: { fontSize: 13.5 } }, t("conf_order")), React.createElement("span", { style: { fontWeight: 700, fontSize: 16, letterSpacing: ".02em" } }, order.id)), React.createElement("hr", { className: "hr", style: { marginBottom: 16 } }), order.items.map((it) => { const p = window.PRODUCTS.find((x) => x.id === it.productId) || window.PRODUCTS[0]; const units = it.units || []; return React.createElement("div", { key: it.lineId, style: { display: "flex", gap: 13, alignItems: "center", marginBottom: 12 } }, React.createElement("div", { style: { display: "flex", gap: 6, flexShrink: 0 } }, units.map((u, i) => React.createElement(window.QRPreview, { key: i, text: window.buildQRPayload(u), plate: window.findFilament(u.plate).hex, code: window.findFilament(u.code).hex, size: 48 }))), React.createElement("div", { style: { flex: 1 } }, React.createElement("div", { style: { fontWeight: 600 } }, lang === "en" ? p.name_en : p.name_fr), React.createElement("div", { className: "muted", style: { fontSize: 13 } }, t("qty") + " " + it.qty)), React.createElement("div", { style: { fontWeight: 600 } }, window.fmtPrice(it.price * it.qty, lang)), ); }), React.createElement("hr", { className: "hr", style: { margin: "4px 0 14px" } }), React.createElement(window.SummaryRow, { label: t("cart_total"), value: window.fmtPrice(order.total, lang), big: true }), ), (order.customer && order.customer.email) ? React.createElement("p", { className: "muted", style: { fontSize: 13.5, marginTop: 18 } }, t("conf_email_note") + " ", React.createElement("b", { style: { color: "var(--ink)" } }, order.customer.email)) : null, React.createElement("button", { className: "btn btn--ghost btn--lg", style: { marginTop: 22 }, onClick: onHome }, t("conf_back")), ); } Object.assign(window, { PaymentScreen, ConfirmationScreen });