SendMoneyFlow
Payee, amount, note and a last look, keeping what was typed on back, with every limit stated and a receipt.
Demo
Step 1 of 4
Who are you sending to?
Or someone new
How much?
- Your balance$2,481.07
- Smallest transfer$1.00
- Most in one transfer$1,000.00
- Left of today's limit$750.00
- Left of this month's limit$4,000.00
- Fee for this transfer$1.50
Add a note?
0 of 140 characters
Check it, then send
- To
- Amount
- Fee
- $1.50
- Note
- None
- Total leaving your account
Sent
- To
- Amount
- Fee
- $1.50
- Total
Nothing sent
Get it
It needs theme.qwe, as every component does, and price.qwe and button.qwe; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/price.qwe$ curl -O https://quillolang.org/components/ui/button.qwe$ curl -O https://quillolang.org/components/ui/send_money_flow.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/send_money_flow.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/send_money_flow.qwe
let send_money_flow_demo_payees = [ { id: "ana", name: "Ana Reyes", account: "•• 4021" }, { id: "joe", name: "Joe Park", account: "•• 7730" }, { id: "mia", name: "Mia Lindqvist", account: "•• 1188" }, { id: "rent", name: "Harbour Lettings", account: "•• 9050" },]component SendMoneyFlowDemoLive() { state log = "Nothing sent" state refuse = true state keys = "" <div.demo-stack> <div.demo-phone> <SendMoneyFlow payees={send_money_flow_demo_payees} balance={2481.07m} fee={1.50m} now_at="2026-11-01T09:42:00Z" limits={{ minimum: 1m, per_transfer: 1000m, daily_left: 750m, monthly_left: 4000m }} on_send={(p) => { keys = keys + (keys == "" ? "" : ",") + slice(p.key, 0, 8) if refuse { refuse = false return { ok: false, message: "Your bank declined this." } } log = `Sent ${str(p.amount)}+${str(p.fee)}=${str(p.total)} ${p.currency} to ${p.payee.name} (${p.payee.account}): ${p.note}` js("Promise.resolve", { ref: "TX-20261101-0042", at: "2026-11-01T09:43:00Z" }) }} on_done={() => { log = "Done" }}/> </div> <p.demo-note id="sendflow-log">{log}</p> <p.demo-note id="sendflow-keys">{keys}</p> </div>}view SendMoneyFlowDemo() = <div.demo-stack><SendMoneyFlowDemoLive/></div>Source
ui/send_money_flow.qwe
// SendMoneyFlow: sending money, a screen at a time: who to, how much, a note, a last look, and a// receipt. Going back keeps everything typed, by the Back button or the phone's own, and from// the last look each line can be changed and returns there. Every limit is written out beside// the amount, not only the one that was broken. On a tablet what has been chosen stays in a// panel beside the step.//// import ui/send_money_flow.qwe use SendMoneyFlow//// component Send() {// <SendMoneyFlow balance={2481.07m} fee={1.50m} now_at="2026-11-01T09:42:00Z"// payees={[{ id: "ana", name: "Ana Reyes", account: "•• 4021" }]}// limits={{ minimum: 1m, per_transfer: 1000m, daily_left: 750m, monthly_left: 4000m }}// on_send={(p) => send_payment(p)} on_done={() => { navigate("/") }}/>// }//// payees: each { id, name, account }, the people already saved. balance: what can be spent. fee:// what a transfer costs, a decimal, added to the amount (none). limits: { minimum, per_transfer,// daily_left, monthly_left }, each left out if there is none. currency ("USD"), locale. now_at:// the time for the receipt, when on_send gives none. note_max: most characters in a note (140).// id: tells two flows on one page apart ("ui-sendflow"). new_payees: false for only those saved.// on_send(payment): { key, payee: { id, name, account }, amount, fee, total, currency, note },// the amounts decimals. It may wait for a server fn, and gives { ref, at }, or true. An error// value or false says it failed, or { ok: false, message }, and the person stays on the last// look with everything kept. `key` stays the same for each try of one payment, so a server can// use it with `idempotent`. on_done(): the person left the receipt.//// Steps are entries in the browser's history, so Android's back and a browser's go back a step;// the receipt cannot be gone back from, as there is nothing left to send.//// send_money_flow_parse(text, currency), send_money_flow_limits(...) and// send_money_flow_payee_problem(name, account) are the working.import ui/theme.qwe use ui_clsimport ui/price.qwe use price_textimport ui/button.qwe use Buttonlet send_money_flow_css = css`.ui-sendflow { box-sizing: border-box; display: grid; grid-template-rows: auto minmax(0, 1fr) auto; height: 100%; min-height: min(560px, 100%); min-width: 0; container-type: inline-size; padding-bottom: var(--ui-sendflow-kb, 0px); color: var(--ui-fg); background: var(--ui-bg); font: 15px/1.45 var(--ui-font);}.ui-sendflow-head { display: grid; grid-template-columns: 44px minmax(0, 1fr) 44px; align-items: center; padding: max(6px, env(safe-area-inset-top)) 8px 6px; border-bottom: 1px solid var(--ui-border); }.ui-sendflow-head p { display: grid; place-items: center; min-height: 44px; margin: 0; color: var(--ui-muted); font-size: 13px; }.ui-sendflow-back { display: grid; place-items: center; width: 44px; height: 44px; padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--ui-fg); font: 24px/1 var(--ui-font); cursor: pointer; touch-action: manipulation; }.ui-sendflow-back:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-sendflow-bar { height: 3px; background: var(--ui-subtle); grid-column: 1 / -1; margin-top: 6px; }.ui-sendflow-bar span { display: block; height: 100%; background: var(--ui-accent); transition: width .2s; }.ui-sendflow-mid { display: grid; min-height: 0; min-width: 0; }.ui-sendflow-scroll { min-height: 0; min-width: 0; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; padding: 14px 16px; }.ui-sendflow-step { display: grid; gap: 12px; min-width: 0; }.ui-sendflow-step[hidden] { display: none; }.ui-sendflow-step h2 { margin: 0; font-size: 21px; line-height: 1.25; outline: none; }.ui-sendflow-field { display: grid; gap: 4px; min-width: 0; }.ui-sendflow-field label { color: var(--ui-muted); font-size: 13px; font-weight: 600; }/* 16 px and up, or iOS zooms the page when one is focused. */.ui-sendflow-input { box-sizing: border-box; width: 100%; min-height: 48px; padding: 10px 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 16px/1.3 var(--ui-font); }.ui-sendflow-input:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-sendflow-input[aria-invalid="true"] { border-color: var(--ui-danger); }textarea.ui-sendflow-input { min-height: 96px; resize: vertical; }.ui-sendflow-big { font-size: 28px; font-weight: 700; font-variant-numeric: tabular-nums; }.ui-sendflow-hint { margin: 0; color: var(--ui-muted); font-size: 13px; }.ui-sendflow-err { margin: 0; color: var(--ui-danger); font-size: 14px; }.ui-sendflow-list { display: grid; gap: 4px; margin: 0; padding: 0; list-style: none; }.ui-sendflow-payee { box-sizing: border-box; display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px; width: 100%; min-height: 56px; padding: 6px 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: inherit; font: inherit; text-align: left; cursor: pointer; touch-action: manipulation; }.ui-sendflow-payee[aria-pressed="true"] { border-color: var(--ui-accent); background: var(--ui-accent-soft); }.ui-sendflow-payee:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-sendflow-payee small { color: var(--ui-muted); font-size: 13px; }.ui-sendflow-payee b { font-weight: 600; overflow-wrap: anywhere; }.ui-sendflow-mark { color: var(--ui-accent); font-weight: 700; }.ui-sendflow-new { display: grid; gap: 10px; padding-top: 4px; }.ui-sendflow-new h3 { margin: 0; font-size: 15px; }.ui-sendflow-limits { display: grid; gap: 0; margin: 0; padding: 0; list-style: none; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); }.ui-sendflow-limits li { display: flex; justify-content: space-between; gap: 12px; padding: 8px 12px; border-top: 1px solid var(--ui-border); font-size: 14px; font-variant-numeric: tabular-nums; }.ui-sendflow-limits li:first-child { border-top: 0; }.ui-sendflow-limits span { color: var(--ui-muted); }.ui-sendflow-over { background: var(--ui-danger-soft); }.ui-sendflow-over span, .ui-sendflow-over b { color: var(--ui-danger); }.ui-sendflow-sum { display: grid; gap: 0; margin: 0; }.ui-sendflow-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 0 8px; min-height: 52px; padding: 4px 0; border-top: 1px solid var(--ui-border); }.ui-sendflow-row:first-child { border-top: 0; }.ui-sendflow-row dt { color: var(--ui-muted); font-size: 13px; }.ui-sendflow-row dd { grid-column: 1; margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; white-space: pre-wrap; }.ui-sendflow-change { grid-column: 2; grid-row: 1 / span 2; min-width: 64px; min-height: 44px; padding: 0 8px; border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-accent); font: 600 14px/1 var(--ui-font); cursor: pointer; touch-action: manipulation; }.ui-sendflow-change:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-sendflow-total dd { font-size: 20px; }.ui-sendflow-done h2 { color: var(--ui-success); }.ui-sendflow-foot { padding: 8px 16px calc(8px + env(safe-area-inset-bottom)); border-top: 1px solid var(--ui-border); background: var(--ui-bg); }.ui-sendflow-foot .ui-btn { width: 100%; }.ui-sendflow-side { display: none; }@container (min-width: 640px) { .ui-sendflow-mid { grid-template-columns: minmax(0, 1fr) minmax(240px, 300px); } .ui-sendflow-scroll { padding: 22px 28px; } .ui-sendflow-side { display: block; min-height: 0; overflow-y: auto; padding: 22px 20px; border-left: 1px solid var(--ui-border); background: var(--ui-subtle); } .ui-sendflow-side h3 { margin: 0 0 8px; font-size: 13px; color: var(--ui-muted); } .ui-sendflow-foot { display: flex; justify-content: flex-end; padding-inline: 28px; } .ui-sendflow-foot .ui-btn { width: auto; min-width: 220px; }}@media (prefers-reduced-motion: reduce) { .ui-sendflow-bar span { transition: none; } }`// A number as a decimal, or null for one that is not.fn send_money_flow_dec(v) { let kind = type_of(v) if kind != "dec" && kind != "int" && kind != "float" && kind != "str" { return null } let d = dec(v) is_err(d) ? null : d}// The fee as a decimal, none being zero.fn send_money_flow_fee(fee) -> dec = send_money_flow_dec(fee) ?? 0m// What is wrong with a new payee, or "".fn send_money_flow_payee_problem(name: str, account: str) -> str { let a = trim(account) if len(trim(name)) < 2 { return "Enter the payee's name" } if len(a) < 4 { return "Enter the payee's account" } if len(a) > 34 || !re.test(a, "^[A-Za-z0-9@._ +-]+$") { return "An account has letters, digits and . _ @ + - only, 34 at most" } ""}// Decimal places a currency has, and 2 for a code that is not one.fn send_money_flow_places(currency: str) -> int { let p = money.minor_units(currency) is_err(p) ? 2 : p}// An amount as a person writes one: "12.50", "12,50", "1,234.50", "1.234,50" or "1,234". Gives// { amount } or { problem }. What cannot be told apart is refused and not guessed at, as a// wrong guess here moves a thousand times the money: "1.234" is three decimals, which USD does// not have, and it says so. A lone comma with three digits after it is thousands, unless the// currency has three decimals.fn send_money_flow_parse(text: str, currency: str) -> map { let t = re.replace(trim(text), "[\\s\\u00a0]", "") if t == "" { return { problem: "Enter an amount" } } if len(t) > 16 || !re.test(t, "^[0-9]{1,3}([.,][0-9]{3})*([.,][0-9]{1,3})?$") && !re.test(t, "^[0-9]{1,12}([.,][0-9]{1,3})?$") { return { problem: "That is not an amount" } } let places = send_money_flow_places(currency) let seps = re.find_all(t, "[.,]") | map((m) => m.text) let hit = re.find(t, "[.,][0-9]{1,3}$") let tail = hit == null ? "" : hit.text let tail_digits = max([0, len(tail) - 1]) let same = len(unique(seps)) == 1 let grouped = same && tail_digits == 3 && (len(seps) > 1 || (seps[0] == "," && places != 3)) let whole = grouped ? t : slice(t, 0, len(t) - len(tail)) let cents = grouped || tail == "" ? "" : slice(tail, 1, len(tail)) if len(cents) > places { return { problem: places == 0 ? `${currency} has no decimals` : `${currency} has ${places} decimals at most` } } let digits = re.replace(whole, "[.,]", "") let d = dec(cents == "" ? digits : `${digits}.${cents}`) if is_err(d) { return { problem: "That is not an amount" } } if d <= 0m { return { problem: "Enter an amount above zero" } } { amount: d }}// Every limit that applies, each as { key_of, label, text, over }. `over` says by how much the amount// and fee go past it, which is how it is put when it is broken.fn send_money_flow_limits(amount, balance, fee, limits, currency: str, locale: str?) -> list { let cash = (v) => price_text(v, currency, locale) let lim = limits ?? {} let cost = send_money_flow_fee(fee) let total = amount == null ? null : amount + cost let rows = [ { key_of: "balance", label: "Your balance", top: send_money_flow_dec(balance), used: total }, { key_of: "minimum", label: "Smallest transfer", bottom: send_money_flow_dec(lim.minimum), used: amount }, { key_of: "per_transfer", label: "Most in one transfer", top: send_money_flow_dec(lim.per_transfer), used: amount }, { key_of: "daily_left", label: "Left of today's limit", top: send_money_flow_dec(lim.daily_left), used: amount }, { key_of: "monthly_left", label: "Left of this month's limit", top: send_money_flow_dec(lim.monthly_left), used: amount }, ] let limited = rows | filter((r) => r.top != null || r.bottom != null) | map((r) => { let over = r.used == null ? null : r.top != null && r.used > r.top ? r.used - r.top : r.bottom != null && r.used < r.bottom ? r.bottom - r.used : null { key_of: r.key_of, label: r.label, text: cash(r.top != null ? r.top : r.bottom), over: over, short: r.bottom != null } }) cost > 0m ? limited + [{ key_of: "fee", label: "Fee for this transfer", text: cash(cost), over: null, short: false }] : limited}// What a broken limit says, the first one first: "" when none is.fn send_money_flow_broken(rows: list, currency: str, locale: str?) -> str { let bad = rows | filter((r) => r.over != null) if len(bad) == 0 { return "" } let r = bad[0] let by = price_text(r.over, currency, locale) if r.short { return `That is ${by} below the smallest transfer of ${r.text}` } if r.key_of == "balance" { return `With the fee that is ${by} more than your balance of ${r.text}` } `That is ${by} over: ${lower(r.label)} is ${r.text}`}// A note's trouble, or "": `len` counts characters as a person does, emoji as one.fn send_money_flow_note_problem(note: str, note_max: int) -> str = len(note) > note_max ? `A note can be ${note_max} characters: this is ${len(note) - note_max} too long` : ""// The answer to on_send: { ok, ref, at, message }.fn send_money_flow_answer(r) -> map { let no = "The money was not sent. Try again, nothing has left your account." if r == false || r == null || is_err(r) { return { ok: false, message: no } } if type_of(r) == "map" && get(r, "ok", true) == false { return { ok: false, message: str(get(r, "message", no)) } } if type_of(r) == "map" { return { ok: true, ref: str(get(r, "ref", "")), at: str(get(r, "at", "")) } } { ok: true, ref: "", at: "" }}// Two lines of a summary, with a way back to the step that set them.view SendMoneyFlowLine(label: str, text: str, step_of: int = -1, on_change = null, class: str? = null) = <div class={ui_cls(["ui-sendflow-row", class])}> <dt>{label}</dt> <dd>{text}</dd> {step_of >= 0 && <button.ui-sendflow-change type="button" aria-label={`Change ${lower(label)}`} @click={() => { on_change(step_of) }}>Change</button>} </div>component SendMoneyFlow(payees: list = [], balance = null, fee = null, limits = null, currency: str = "USD", locale: str? = null, now_at: str? = null, note_max: int = 140, new_payees: bool = true, id: str = "ui-sendflow", on_send = null, on_done = null) { style global send_money_flow_css state step = 0 // 0 who, 1 how much, 2 note, 3 last look, 4 receipt state back_to = -1 // the last look, when a line was changed from there state pushed = 0 // history entries this flow has added state pops = 0 // goings back the Back button has asked for and already shown state query = "" state payee_id = null state new_name = "" state new_account = "" state amount_text = "" state note = "" state tried = false // Continue was pressed on this step, so its trouble may show state busy = false state failed = "" state key_of = "" // one payment's key, whatever number of tries state done_ref = "" state done_at = "" state root_el = null state scroll_el = null let saved = payees | find((p) => p.id == payee_id) let typed_payee = new_payees && payee_id == null && (trim(new_name) != "" || trim(new_account) != "") let payee = saved != null ? saved : typed_payee ? { id: null, name: trim(new_name), account: trim(new_account) } : null let payee_problem = saved != null ? "" : typed_payee ? send_money_flow_payee_problem(new_name, new_account) : "Choose who to send to" let parsed = send_money_flow_parse(amount_text, currency) let amount = parsed.amount let cost = send_money_flow_fee(fee) let total = amount == null ? null : amount + cost let rows = send_money_flow_limits(amount, balance, fee, limits, currency, locale) let broken = send_money_flow_broken(rows, currency, locale) // A limit shows as it is typed; text that is not an amount yet waits for Continue, as it may be half typed. let amount_problem = amount == null ? (tried ? parsed.problem : "") : broken let note_problem = send_money_flow_note_problem(note, note_max) let found = payees | filter((p) => query == "" || contains(lower(`${p.name} ${p.account}`), lower(trim(query)))) let cash = (v) => v == null ? "" : price_text(v, currency, locale) let show_step = (n: int) => { step = n tried = false js("Promise.resolve", 1) if scroll_el != null { scroll_el.scrollTop = 0 } if root_el != null { let h = root_el.querySelector(`[data-step="${n}"] h2`) if h != null { h.focus() } } } let forward = (n: int) => { js("history.pushState", { ui_sendflow: id, step: n }, "") pushed = pushed + 1 show_step(n) } let next = () => { let problem = match step { 0 => payee_problem 1 => amount == null ? parsed.problem : broken 2 => note_problem _ => "" } if problem != "" { tried = true } else if back_to >= 0 { back_to = -1 forward(3) } else { forward(step + 1) } } let back = () => { if step > 0 && step < 4 { failed = "" back_to = -1 // The step changes at once: the browser's own going back can take a moment on a slow phone. let going = pushed > 0 if going { pops = pops + 1 } show_step(step - 1) if going { js("history.back") } } } let change = (n: int) => { back_to = n forward(n) } let send = () => { if !busy && step == 3 { busy = true failed = "" if key_of == "" { key_of = uuid() } let r = on_send == null ? true : js("Promise.resolve", on_send({ key: key_of, payee: { id: payee.id, name: payee.name, account: payee.account }, amount: amount, fee: cost, total: total, currency: currency, note: trim(note) })) let a = send_money_flow_answer(r) busy = false if a.ok { done_ref = a.ref done_at = a.at != "" ? a.at : now_at != null ? now_at : "" show_step(4) } else { failed = a.message } } } let finish = () => { step = 0 query = "" payee_id = null new_name = "" new_account = "" amount_text = "" note = "" key_of = "" done_ref = "" done_at = "" on_done && on_done() } let side_lines = [ payee != null && payee_problem == "" ? { label: "To", text: `${payee.name}\n${payee.account}` } : null, amount != null ? { label: "Amount", text: cash(amount) } : null, amount != null && cost > 0m ? { label: "Fee", text: cash(cost) } : null, trim(note) != "" ? { label: "Note", text: trim(note) } : null, ] | filter((x) => x != null) <section.ui-sendflow id={id} aria-label="Send money" @mount={(el) => { let w = js("window") let vv = w.visualViewport // On iOS the keyboard covers the page without changing its height, so the part it // takes is worked out; on Android the page may shrink, which makes this nothing. let fit_keyboard = (_e = null) => { if vv != null { let covered = w.innerHeight - vv.height - vv.offsetTop el.style.setProperty("--ui-sendflow-kb", `${max([0, round(covered)])}px`) } } let popped = (e) => { let s = e.state pushed = max([0, pushed - 1]) if pops > 0 { pops = pops - 1 } else if step < 4 { failed = "" back_to = -1 show_step(s != null && s.ui_sendflow == id ? s.step : 0) } } w.addEventListener("popstate", popped) if vv != null { vv.addEventListener("resize", fit_keyboard) vv.addEventListener("scroll", fit_keyboard) } root_el = el () => { w.removeEventListener("popstate", popped) if vv != null { vv.removeEventListener("resize", fit_keyboard) vv.removeEventListener("scroll", fit_keyboard) } } }}> <div.ui-sendflow-head> {step > 0 && step < 4 ? <button.ui-sendflow-back type="button" aria-label="Back" @click={back}>‹</button> : <span></span>} <p>{step < 4 ? `Step ${step + 1} of 4` : "Receipt"}</p> <span></span> <div.ui-sendflow-bar aria-hidden="true"><span style={`width: ${(step + 1) * 20}%`}></span></div> </div> <div.ui-sendflow-mid> <div.ui-sendflow-scroll @mount={(el) => { scroll_el = el }}> <div.ui-sendflow-step data-step="0" hidden={step != 0}> <h2 tabindex="-1">Who are you sending to?</h2> {len(payees) > 3 && <div.ui-sendflow-field> <label for={`${id}-query`}>Find someone saved</label> <input.ui-sendflow-input id={`${id}-query`} type="search" autocomplete="off" enterkeyhint="search" bind:value={query}/> </div>} {len(payees) > 0 && <ul.ui-sendflow-list> {for p in found => <li key={p.id}> <button.ui-sendflow-payee type="button" aria-pressed={payee_id == p.id ? "true" : "false"} @click={() => { payee_id = payee_id == p.id ? null : p.id tried = false }}> <span><b>{p.name}</b><br/><small>{p.account}</small></span> <span.ui-sendflow-mark aria-hidden="true">{payee_id == p.id ? "✓" : ""}</span> </button> </li>} {len(found) == 0 && <li.ui-sendflow-hint>No one saved goes by that.</li>} </ul>} {new_payees && <div.ui-sendflow-new> <h3>{len(payees) > 0 ? "Or someone new" : "Someone new"}</h3> <div.ui-sendflow-field> <label for={`${id}-name`}>Name</label> <input.ui-sendflow-input id={`${id}-name`} type="text" autocomplete="off" autocapitalize="words" enterkeyhint="next" bind:value={new_name} @input={() => { payee_id = null }}/> </div> <div.ui-sendflow-field> <label for={`${id}-account`}>Account</label> <input.ui-sendflow-input id={`${id}-account`} type="text" autocomplete="off" autocapitalize="none" spellcheck="false" enterkeyhint="next" bind:value={new_account} @input={() => { payee_id = null }}/> </div> </div>} {tried && step == 0 && payee_problem != "" && <p.ui-sendflow-err role="alert">{payee_problem}</p>} </div> <div.ui-sendflow-step data-step="1" hidden={step != 1}> <h2 tabindex="-1">How much?</h2> <div.ui-sendflow-field> <label for={`${id}-amount`}>Amount in {currency}{payee != null ? ` to ${payee.name}` : ""}</label> <input class="ui-sendflow-input ui-sendflow-big" id={`${id}-amount`} type="text" inputmode="decimal" autocomplete="off" enterkeyhint="next" aria-invalid={amount_problem != "" ? "true" : "false"} aria-describedby={`${id}-amount-says`} bind:value={amount_text} @keydown={(e) => { if e.key == "Enter" { e.preventDefault() next() } }}/> </div> <p class={ui_cls(["ui-sendflow-hint", amount_problem != "" && "ui-sendflow-err"])} id={`${id}-amount-says`} role={amount_problem != "" ? "alert" : null}> {amount_problem != "" ? amount_problem : total != null && cost > 0m ? `${cash(total)} will leave your account, with the fee` : ""} </p> {len(rows) > 0 && <ul.ui-sendflow-limits aria-label="Limits"> {for r in rows => <li key={r.key_of} class={r.over != null ? "ui-sendflow-over" : null}><span>{r.label}</span><b>{r.text}</b></li>} </ul>} </div> <div.ui-sendflow-step data-step="2" hidden={step != 2}> <h2 tabindex="-1">Add a note?</h2> <div.ui-sendflow-field> <label for={`${id}-note`}>A note for {payee != null ? payee.name : "them"} (you can leave this empty)</label> <textarea.ui-sendflow-input id={`${id}-note`} rows="3" enterkeyhint="done" aria-invalid={note_problem != "" ? "true" : "false"} bind:value={note}></textarea> </div> <p class={ui_cls(["ui-sendflow-hint", note_problem != "" && "ui-sendflow-err"])} role={note_problem != "" ? "alert" : null}>{note_problem != "" ? note_problem : `${len(note)} of ${note_max} characters`}</p> </div> <div.ui-sendflow-step data-step="3" hidden={step != 3}> <h2 tabindex="-1">Check it, then send</h2> <dl.ui-sendflow-sum> <SendMoneyFlowLine label="To" text={payee != null ? `${payee.name}\n${payee.account}` : ""} step_of={0} on_change={change}/> <SendMoneyFlowLine label="Amount" text={cash(amount)} step_of={1} on_change={change}/> {cost > 0m && <SendMoneyFlowLine label="Fee" text={cash(cost)}/>} <SendMoneyFlowLine label="Note" text={trim(note) != "" ? trim(note) : "None"} step_of={2} on_change={change}/> <SendMoneyFlowLine label="Total leaving your account" text={cash(total)} class="ui-sendflow-total"/> </dl> {failed != "" && <p.ui-sendflow-err role="alert">{failed}</p>} </div> <div class="ui-sendflow-step ui-sendflow-done" data-step="4" hidden={step != 4}> <h2 tabindex="-1">Sent</h2> <dl.ui-sendflow-sum> <SendMoneyFlowLine label="To" text={payee != null ? `${payee.name}\n${payee.account}` : ""}/> <SendMoneyFlowLine label="Amount" text={cash(amount)}/> {cost > 0m && <SendMoneyFlowLine label="Fee" text={cash(cost)}/>} <SendMoneyFlowLine label="Total" text={cash(total)} class="ui-sendflow-total"/> {trim(note) != "" && <SendMoneyFlowLine label="Note" text={trim(note)}/>} {done_ref != "" && <SendMoneyFlowLine label="Reference" text={done_ref}/>} {done_at != "" && date.valid(done_at) && <SendMoneyFlowLine label="When" text={date.format(done_at, date.valid(done_at, "date-time") ? "MMM D, YYYY HH:mm" : "MMM D, YYYY")}/>} </dl> </div> </div> <aside.ui-sendflow-side aria-label="So far"> <h3>So far</h3> {len(side_lines) == 0 && <p.ui-sendflow-hint>Nothing chosen yet.</p>} <dl.ui-sendflow-sum> {for s in side_lines => <SendMoneyFlowLine key={s.label} label={s.label} text={s.text}/>} </dl> </aside> </div> <div.ui-sendflow-foot> {step < 3 && <Button size="lg" on_click={next}>{back_to >= 0 ? "Back to the last look" : "Continue"}</Button>} {step == 3 && <Button size="lg" loading={busy} on_click={send}>{`Send ${cash(total)}`}</Button>} {step == 4 && <Button size="lg" on_click={finish}>Done</Button>} </div> </section>}Related
QuickTransferRecent people in a row, a keypad amount checked against every limit, and a slide to send.2 views · 7 downloadsBalanceCardBalances that hide with a tap and show for a hold, swiped between accounts, in decimals, with when they were read.3 views · 6 downloadsBillRemindersUpcoming bills in order, marked when close, to pay or snooze, with the ends of months handled.3 views · 6 downloadsCardStackCards in a fan: a tap lifts one, a switch freezes it, and its details are hidden, shown and copied a field at a time.2 views · 8 downloadsConfirmWithPinA step-up before something risky: what is about to happen, a PIN pad, tries counted, and a clock.1 view · 5 downloadsInspectionFormA checklist of pass, fail and N/A with big targets, a note and stamped photo needed on a fail, a score, and a draft kept.2 views · 5 downloads
Thread
Thread 0 posts
On SendMoneyFlow: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.