Checkout Flow
blockcommerceThree-step checkout (cart → payment → confirm) with animated step transitions, an inline PaymentForm, a success screen that fires a confetti burst with an animated checkmark, and an error screen with retry. Async submit is forwarded to the consumer-provided `onSubmit`, which returns an `orderId` that ends up on the success screen.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/checkout-flow.json$npx shadcn@latest add https://uipkge.dev/r/lit/checkout-flow.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/checkout-flow.json$bunx shadcn@latest add https://uipkge.dev/r/lit/checkout-flow.jsonManifest:
https://uipkge.dev/r/lit/checkout-flow.jsonInstalls to:src/uip/blocks/checkout-flow/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
currency | string | USD | optional |
shipping-fee | number | 0 | optional |
tax-rate | number | 0 | optional |
simulate-error | boolean | false | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
PaymentPayloadinterface PaymentPayload {
number: string
name: string
expiry: string
cvc: string
brand: CardBrand
}CheckoutCartIteminterface CheckoutCartItem {
id: string
name: string
image?: string
qty: number
price: number
}Particleinterface Particle {
id: number
x: number
y: number
vx: number
vy: number
rot: number
vr: number
color: string
size: number
}HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-checkout-flow': UipCheckoutFlow
}
}Files installed (1)
src/uip/blocks/checkout-flow/checkout-flow.ts25.6 kB
import { LitElement, css, html, nothing } from 'lit' import { ArrowLeft, Minus, Plus, Trash2 } from 'lucide' import { tailwind } from '../../lib/styles' import { ThemeController } from '../../lib/theme' import { icon } from '../../lib/icon' // Side-effect imports: register the composed elements for standalone use // (consumers who install only this block still get working primitives). import '../../components/button/button' import '../../components/stepper/stepper' import '../../components/separator/separator' import '../../components/payment-card/payment-card' import '../../components/input/input' import type { UipInput } from '../../components/input/input' import '../../components/label/label' type CardBrand = 'visa' | 'mastercard' | 'amex' | 'discover' | 'unknown' export interface PaymentPayload { number: string name: string expiry: string cvc: string brand: CardBrand } export interface CheckoutCartItem { id: string name: string image?: string qty: number price: number } type Step = 'cart' | 'payment' | 'confirm' | 'success' | 'error' interface Particle { id: number x: number y: number vx: number vy: number rot: number vr: number color: string size: number } function detectBrand(raw: string): CardBrand { const n = raw.replace(/\D/g, '') if (!n) return 'unknown' if (/^4/.test(n)) return 'visa' if (/^(5[1-5]|2[2-7])/.test(n)) return 'mastercard' if (/^3[47]/.test(n)) return 'amex' if (/^(6011|65|64[4-9])/.test(n)) return 'discover' return 'unknown' } function luhn(digits: string): boolean { let sum = 0 let alt = false for (let i = digits.length - 1; i >= 0; i--) { let d = Number(digits[i]) if (alt) { d *= 2 if (d > 9) d -= 9 } sum += d alt = !alt } return sum % 10 === 0 && digits.length >= 13 } const stepperSteps = [ { id: 'cart', title: 'Cart' }, { id: 'payment', title: 'Payment' }, { id: 'confirm', title: 'Confirm' }, ] /** * <uip-checkout-flow> — the registry CheckoutFlow block as a web component. * * Class strings, step keyframes, check-draw animation, and the confetti * physics are React's verbatim (keyframes are the one allowed extra-CSS * exception). React state becomes { state: true } fields; `items` is a * property (attribute: false — set it from JS). `onSubmit` (payload → orderId) * / `onComplete` ({ orderId }) / `onCancel` become `submit` / `complete` / * `cancel` CustomEvents (bubbles, composed; payloads in `detail`). Like * login-01, the block simulates the processor round-trip itself (1s) and then * notifies; `simulate-error` forces the rejection path so the demo can show it * without a callback prop. * * One deliberate departure: React embeds the PaymentForm *block* in the * payment step, but blocks never import sibling blocks (one block = one * install unit), so the payment step is an inline card form built from * uip-input + uip-payment-card with the same validation as uip-payment-form. */ export class UipCheckoutFlow extends LitElement { static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true } // :host display can't be a utility class; step + check-draw keyframes are // React's injected <style> copied verbatim (allowed keyframes exception). static styles = [ tailwind, css` :host { display: block; } .checkout-flow-anim-forward, .checkout-flow-anim-back { animation-duration: 280ms; animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); animation-fill-mode: both; } .checkout-flow-anim-forward { animation-name: checkout-flow-forward; } .checkout-flow-anim-back { animation-name: checkout-flow-back; } @keyframes checkout-flow-forward { from { transform: translateX(24px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes checkout-flow-back { from { transform: translateX(-24px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } .checkout-flow-check-draw { stroke-dasharray: 100; stroke-dashoffset: 100; animation: checkout-flow-check-draw 700ms cubic-bezier(0.4, 0, 0.2, 1) 120ms forwards; } @keyframes checkout-flow-check-draw { to { stroke-dashoffset: 0; } } @media (prefers-reduced-motion: reduce) { .checkout-flow-anim-forward, .checkout-flow-anim-back { animation-duration: 0ms; } .checkout-flow-check-draw { animation: none; stroke-dashoffset: 0; } } `, ] static properties = { items: { attribute: false }, currency: {}, shippingFee: { type: Number, attribute: 'shipping-fee' }, taxRate: { type: Number, attribute: 'tax-rate' }, simulateError: { type: Boolean, attribute: 'simulate-error' }, step: { state: true }, direction: { state: true }, cart: { state: true }, paymentPayload: { state: true }, orderId: { state: true }, lastError: { state: true }, placing: { state: true }, particles: { state: true }, cardNumber: { state: true }, cardName: { state: true }, cardExpiry: { state: true }, cardCvc: { state: true }, cardErrors: { state: true }, } items: CheckoutCartItem[] = [] currency = 'USD' shippingFee = 0 taxRate = 0 simulateError = false private step: Step = 'cart' private direction: 'forward' | 'back' = 'forward' private cart: CheckoutCartItem[] = [] private paymentPayload: PaymentPayload | null = null private orderId: string | null = null private lastError: Error | null = null private placing = false private particles: Particle[] = [] private cardNumber = '' private cardName = '' private cardExpiry = '' private cardCvc = '' private cardErrors: Record<string, string | null> = { number: null, name: null, expiry: null, cvc: null } private confettiRaf: number | null = null private timer?: number constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'checkout-flow') } disconnectedCallback() { super.disconnectedCallback() if (this.confettiRaf) cancelAnimationFrame(this.confettiRaf) if (this.timer) window.clearTimeout(this.timer) } willUpdate(changed: Map<string, unknown>) { if (changed.has('items')) { this.cart = (this.items ?? []).map((i) => ({ ...i })) } } updated(changed: Map<string, unknown>) { if (changed.has('step') && this.step === 'success') this.fireConfetti() } private go(next: Step) { const order: Record<Step, number> = { cart: 0, payment: 1, confirm: 2, success: 3, error: 3 } this.direction = order[next] >= order[this.step] ? 'forward' : 'back' this.step = next } private get subtotal() { return this.cart.reduce((s, i) => s + i.qty * i.price, 0) } private get tax() { return this.subtotal * this.taxRate } private get total() { return this.subtotal + this.shippingFee + this.tax } private fmt(n: number) { try { return new Intl.NumberFormat(undefined, { style: 'currency', currency: this.currency }).format(n) } catch { return `${this.currency} ${n.toFixed(2)}` } } private get stepIndex() { return this.step === 'cart' ? 1 : this.step === 'payment' ? 2 : 3 } private dec(item: CheckoutCartItem) { if (item.qty > 1) this.cart = this.cart.map((i) => (i.id === item.id ? { ...i, qty: i.qty - 1 } : i)) } private inc(item: CheckoutCartItem) { this.cart = this.cart.map((i) => (i.id === item.id ? { ...i, qty: i.qty + 1 } : i)) } private removeItem(item: CheckoutCartItem) { this.cart = this.cart.filter((i) => i.id !== item.id) } private get cardBrand(): CardBrand { return detectBrand(this.cardNumber) } private formatCardNumber(raw: string, b: CardBrand): string { const digits = raw.replace(/\D/g, '').slice(0, b === 'amex' ? 15 : 16) if (b === 'amex') { return digits.replace(/^(\d{0,4})(\d{0,6})(\d{0,5}).*$/, (_m, a, bb, c) => [a, bb, c].filter(Boolean).join(' ')) } return digits.replace(/(.{4})/g, '$1 ').trim() } private formatCardExpiry(raw: string): string { const d = raw.replace(/\D/g, '').slice(0, 4) if (d.length <= 2) return d return `${d.slice(0, 2)}/${d.slice(2)}` } private validateCard(): boolean { const digits = this.cardNumber.replace(/\D/g, '') const expectedLen = this.cardBrand === 'amex' ? 15 : 16 const next: Record<string, string | null> = { number: null, name: null, expiry: null, cvc: null } next.number = !digits ? 'Card number required' : digits.length !== expectedLen ? `Card number must be ${expectedLen} digits` : !luhn(digits) ? 'Card number is invalid' : null next.name = !this.cardName.trim() ? 'Name required' : this.cardName.trim().length < 2 ? 'Name too short' : null const expOk = /^(0[1-9]|1[0-2])\/\d{2}$/.test(this.cardExpiry) if (!expOk) { next.expiry = 'Use MM/YY' } else { const [m, y] = this.cardExpiry.split('/').map(Number) const now = new Date() const expDate = new Date(2000 + (y ?? 0), (m ?? 1) - 1, 1) expDate.setMonth(expDate.getMonth() + 1) next.expiry = expDate <= now ? 'Card has expired' : null } const cvcLen = this.cardBrand === 'amex' ? 4 : 3 next.cvc = this.cardCvc.length !== cvcLen ? `CVC must be ${cvcLen} digits` : null this.cardErrors = next return Object.values(next).every((e) => !e) } private submitCardForm(e: Event) { e.preventDefault() if (!this.validateCard()) return this.paymentPayload = { number: this.cardNumber.replace(/\D/g, ''), name: this.cardName.trim(), expiry: this.cardExpiry, cvc: this.cardCvc, brand: this.cardBrand, } this.go('confirm') } private placeOrder() { if (!this.paymentPayload || this.placing) return this.placing = true this.timer = window.setTimeout(() => { this.placing = false if (this.simulateError) { this.lastError = new Error('Issuer rejected the charge. Please contact your bank.') this.go('error') return } const nextOrderId = Math.random().toString(36).slice(2, 10).toUpperCase() this.orderId = nextOrderId this.dispatchEvent( new CustomEvent('submit', { detail: { ...this.paymentPayload }, bubbles: true, composed: true }), ) this.dispatchEvent(new CustomEvent('complete', { detail: { orderId: nextOrderId }, bubbles: true, composed: true })) this.go('success') }, 1000) } private fireConfetti() { if (typeof window === 'undefined' || !window.matchMedia) return if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return // Pull theme tokens at runtime so confetti follows the active palette // (light/dark mode + any consumer theme override). const cs = getComputedStyle(document.documentElement) const tok = (name: string, fallback: string) => cs?.getPropertyValue(name).trim() || fallback const colors = [ tok('--chart-1', 'oklch(0.65 0.18 145)'), tok('--chart-2', 'oklch(0.6 0.22 260)'), tok('--chart-3', 'oklch(0.7 0.16 75)'), tok('--chart-4', 'oklch(0.65 0.2 330)'), tok('--chart-5', 'oklch(0.6 0.22 30)'), tok('--primary', 'oklch(0.55 0.22 260)'), ] this.particles = Array.from({ length: 60 }, (_, id) => ({ id, x: 50, y: 40, vx: (Math.random() - 0.5) * 12, vy: -Math.random() * 14 - 4, rot: Math.random() * 360, vr: (Math.random() - 0.5) * 20, color: colors[Math.floor(Math.random() * colors.length)] ?? colors[0]!, size: 4 + Math.random() * 6, })) const start = performance.now() const tick = (now: number) => { const elapsed = now - start if (elapsed > 1800) { this.particles = [] this.confettiRaf = null return } this.particles = this.particles.map((p) => ({ ...p, x: p.x + p.vx * 0.25, y: p.y + p.vy * 0.25, vy: p.vy + 0.45, rot: p.rot + p.vr, })) this.confettiRaf = requestAnimationFrame(tick) } this.confettiRaf = requestAnimationFrame(tick) } render() { const animClass = this.direction === 'forward' ? 'checkout-flow-anim-forward' : 'checkout-flow-anim-back' return html` <div data-slot="checkout-flow" class="bg-card text-card-foreground mx-auto w-full max-w-3xl overflow-hidden rounded-xl border shadow-sm"> ${this.step !== 'success' && this.step !== 'error' ? html`<div class="bg-muted/30 border-b px-6 py-4"> <uip-stepper .steps=${stepperSteps} value=${this.stepIndex}></uip-stepper> </div>` : nothing} <div class="relative min-h-[420px]"> ${this.step === 'cart' ? this.renderCart(animClass) : nothing} ${this.step === 'payment' ? this.renderPayment(animClass) : nothing} ${this.step === 'confirm' ? this.renderConfirm(animClass) : nothing} ${this.step === 'success' ? this.renderSuccess(animClass) : nothing} ${this.step === 'error' ? this.renderError(animClass) : nothing} </div> </div> ` } private renderCart(animClass: string) { return html`<div class=${`p-6 ${animClass}`}> <h3 class="mb-4 text-lg font-semibold">Your cart</h3> ${this.cart.length === 0 ? html`<div class="text-muted-foreground py-12 text-center text-sm">Your cart is empty.</div>` : html`<ul class="divide-border divide-y"> ${this.cart.map( (item) => html`<li class="flex items-center gap-3 py-3"> <div class="bg-muted size-14 shrink-0 overflow-hidden rounded-md"> ${item.image ? html`<img src=${item.image} alt=${item.name} class="size-full object-cover" />` : nothing} </div> <div class="min-w-0 flex-1"> <p class="truncate text-sm font-medium">${item.name}</p> <p class="text-muted-foreground text-xs">${this.fmt(item.price)} each</p> </div> <div class="flex items-center gap-1"> <uip-button variant="outline" size="icon-sm" aria-label="Decrease" @click=${() => this.dec(item)}> ${icon(Minus, 'minus', 'size-3.5')} </uip-button> <span class="w-8 text-center text-sm tabular-nums">${item.qty}</span> <uip-button variant="outline" size="icon-sm" aria-label="Increase" @click=${() => this.inc(item)}> ${icon(Plus, 'plus', 'size-3.5')} </uip-button> </div> <div class="w-20 text-right text-sm font-medium tabular-nums">${this.fmt(item.qty * item.price)}</div> <uip-button variant="ghost" size="icon-sm" aria-label="Remove" @click=${() => this.removeItem(item)}> ${icon(Trash2, 'trash-2', 'text-muted-foreground size-4')} </uip-button> </li>`, )} </ul>`} <uip-separator class="my-4"></uip-separator> <dl class="space-y-1.5 text-sm"> <div class="flex justify-between"> <dt class="text-muted-foreground">Subtotal</dt> <dd>${this.fmt(this.subtotal)}</dd> </div> ${this.shippingFee > 0 ? html`<div class="flex justify-between"> <dt class="text-muted-foreground">Shipping</dt> <dd>${this.fmt(this.shippingFee)}</dd> </div>` : nothing} ${this.taxRate > 0 ? html`<div class="flex justify-between"> <dt class="text-muted-foreground">Tax</dt> <dd>${this.fmt(this.tax)}</dd> </div>` : nothing} <div class="flex justify-between border-t pt-2 text-base font-semibold"> <dt>Total</dt> <dd>${this.fmt(this.total)}</dd> </div> </dl> <div class="mt-5 flex justify-between"> <uip-button variant="ghost" @click=${() => this.dispatchEvent(new CustomEvent('cancel', { bubbles: true, composed: true }))} > Cancel </uip-button> <uip-button ?disabled=${this.cart.length === 0} @click=${() => this.go('payment')}>Continue to payment →</uip-button> </div> </div>` } private renderPayment(animClass: string) { return html`<div class=${`p-6 ${animClass}`}> <button type="button" class="text-muted-foreground hover:text-foreground mb-3 inline-flex items-center gap-1 text-sm" @click=${() => this.go('cart')} > ${icon(ArrowLeft, 'arrow-left', 'size-3.5')} Back to cart </button> <div class="mb-4 flex justify-center"> <uip-payment-card number=${this.cardNumber} name=${this.cardName} expiry=${this.cardExpiry} cvc=${this.cardCvc} size="md" ></uip-payment-card> </div> <form class="space-y-3" @submit=${(e: Event) => this.submitCardForm(e)}> <div class="grid gap-1.5"> <uip-label for="co-cc-number">Card number</uip-label> <uip-input id="co-cc-number" type="text" autocomplete="cc-number" placeholder="1234 5678 9012 3456" .value=${this.cardNumber} status=${this.cardErrors.number ? 'error' : nothing} @input=${(e: Event) => { const el = e.target as UipInput const b = detectBrand(el.value ?? '') this.cardNumber = this.formatCardNumber(el.value ?? '', b) this.cardErrors = { ...this.cardErrors, number: null } }} ></uip-input> ${this.cardErrors.number ? html`<p class="text-destructive text-xs" role="alert">${this.cardErrors.number}</p>` : nothing} </div> <div class="grid gap-1.5"> <uip-label for="co-cc-name">Name on card</uip-label> <uip-input id="co-cc-name" type="text" autocomplete="cc-name" placeholder="Jane Doe" .value=${this.cardName} status=${this.cardErrors.name ? 'error' : nothing} @input=${(e: Event) => { this.cardName = (e.target as UipInput).value ?? '' }} ></uip-input> ${this.cardErrors.name ? html`<p class="text-destructive text-xs" role="alert">${this.cardErrors.name}</p>` : nothing} </div> <div class="grid grid-cols-2 gap-3"> <div class="grid gap-1.5"> <uip-label for="co-cc-exp">Expiry</uip-label> <uip-input id="co-cc-exp" type="text" autocomplete="cc-exp" placeholder="MM/YY" .value=${this.cardExpiry} status=${this.cardErrors.expiry ? 'error' : nothing} @input=${(e: Event) => { this.cardExpiry = this.formatCardExpiry((e.target as UipInput).value ?? '') this.cardErrors = { ...this.cardErrors, expiry: null } }} ></uip-input> ${this.cardErrors.expiry ? html`<p class="text-destructive text-xs" role="alert">${this.cardErrors.expiry}</p>` : nothing} </div> <div class="grid gap-1.5"> <uip-label for="co-cc-cvc">CVC</uip-label> <uip-input id="co-cc-cvc" type="text" autocomplete="cc-csc" placeholder=${this.cardBrand === 'amex' ? '1234' : '123'} .value=${this.cardCvc} status=${this.cardErrors.cvc ? 'error' : nothing} @input=${(e: Event) => { const max = this.cardBrand === 'amex' ? 4 : 3 this.cardCvc = ((e.target as UipInput).value ?? '').replace(/\D/g, '').slice(0, max) this.cardErrors = { ...this.cardErrors, cvc: null } }} ></uip-input> ${this.cardErrors.cvc ? html`<p class="text-destructive text-xs" role="alert">${this.cardErrors.cvc}</p>` : nothing} </div> </div> <uip-button type="submit" class="h-11 w-full text-base">Pay ${this.fmt(this.total)}</uip-button> </form> </div>` } private renderConfirm(animClass: string) { const p = this.paymentPayload return html`<div class=${`p-6 ${animClass}`}> <h3 class="mb-4 text-lg font-semibold">Review your order</h3> <div class="mb-4 flex items-start gap-4 rounded-lg border p-4"> ${p ? html`<uip-payment-card number=${p.number} name=${p.name} expiry=${p.expiry} brand=${p.brand} variant="compact" flip="false" ></uip-payment-card>` : nothing} <div class="min-w-0 flex-1 text-sm"> <p class="font-medium">Paying with card ending ${p?.number.slice(-4)}</p> <p class="text-muted-foreground">${p?.name} · expires ${p?.expiry}</p> <button class="text-primary mt-2 text-xs underline" @click=${() => this.go('payment')}> Change payment method </button> </div> </div> <ul class="divide-border divide-y border-y"> ${this.cart.map( (item) => html`<li class="flex justify-between py-2 text-sm"> <span class="truncate">${item.qty} × ${item.name}</span> <span class="tabular-nums">${this.fmt(item.qty * item.price)}</span> </li>`, )} </ul> <dl class="mt-3 space-y-1 text-sm"> <div class="flex justify-between"> <dt class="text-muted-foreground">Subtotal</dt> <dd>${this.fmt(this.subtotal)}</dd> </div> ${this.shippingFee > 0 ? html`<div class="flex justify-between"> <dt class="text-muted-foreground">Shipping</dt> <dd>${this.fmt(this.shippingFee)}</dd> </div>` : nothing} ${this.taxRate > 0 ? html`<div class="flex justify-between"> <dt class="text-muted-foreground">Tax</dt> <dd>${this.fmt(this.tax)}</dd> </div>` : nothing} <div class="flex justify-between border-t pt-2 text-base font-semibold"> <dt>Total</dt> <dd>${this.fmt(this.total)}</dd> </div> </dl> <div class="mt-5 flex justify-between"> <uip-button variant="outline" @click=${() => this.go('payment')}> ${icon(ArrowLeft, 'arrow-left', 'mr-1.5 size-4')} Back </uip-button> <uip-button ?disabled=${this.placing} @click=${() => this.placeOrder()}> ${this.placing ? 'Placing order…' : `Place order · ${this.fmt(this.total)}`} </uip-button> </div> </div>` } private renderSuccess(animClass: string) { return html`<div class=${`relative flex flex-col items-center px-6 py-14 text-center ${animClass}`}> <div class="pointer-events-none absolute inset-0 overflow-hidden"> ${this.particles.map( (pt) => html`<div class="absolute" style="left:${pt.x}%;top:${pt.y}%;width:${pt.size}px;height:${pt.size}px;background:${pt.color};transform:rotate(${pt.rot}deg);border-radius:2px;" ></div>`, )} </div> <div class="bg-primary/10 ring-primary/30 relative grid size-20 place-items-center rounded-full ring-2"> <svg viewBox="0 0 52 52" class="text-primary size-12" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" > <path d="M14 27 L23 36 L40 18" pathLength="100" class="checkout-flow-check-draw" /> </svg> </div> <h3 class="mt-5 text-xl font-semibold">Order placed</h3> <p class="text-muted-foreground mt-1 text-sm">Thanks — a confirmation will be sent shortly.</p> <div class="bg-muted mt-4 inline-flex rounded-md px-3 py-1.5 font-mono text-sm">#${this.orderId}</div> <uip-button variant="outline" class="mt-6" @click=${() => this.dispatchEvent(new CustomEvent('cancel', { bubbles: true, composed: true }))} > Back to home </uip-button> </div>` } private renderError(animClass: string) { return html`<div class=${`flex flex-col items-center px-6 py-14 text-center ${animClass}`}> <div class="bg-destructive/10 ring-destructive/30 relative grid size-20 place-items-center rounded-full ring-2"> <svg viewBox="0 0 52 52" class="text-destructive size-12" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" aria-hidden="true" > <path d="M16 16 L36 36 M36 16 L16 36" pathLength="100" class="checkout-flow-check-draw" /> </svg> </div> <h3 class="mt-5 text-xl font-semibold">Payment failed</h3> <p class="text-muted-foreground mt-1 max-w-xs text-sm"> ${this.lastError?.message || 'Something went wrong while charging your card.'} </p> <div class="mt-6 flex gap-2"> <uip-button variant="outline" @click=${() => this.dispatchEvent(new CustomEvent('cancel', { bubbles: true, composed: true }))} > Cancel </uip-button> <uip-button @click=${() => this.go('payment')}>Try again</uip-button> </div> </div>` } } customElements.get('uip-checkout-flow') || customElements.define('uip-checkout-flow', UipCheckoutFlow) declare global { interface HTMLElementTagNameMap { 'uip-checkout-flow': UipCheckoutFlow } }
Raw manifest:https://uipkge.dev/r/lit/checkout-flow.json