Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions components/src/widget/views/amount/amount.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
:host {
font-family: var(--wm-font-family, system-ui, sans-serif);
display: block;
flex: 0;

--primary-color: var(--wm-primary-color, #56b7b5);
--background-color: var(--wm-background-color, #ffffff);
--text-color: var(--wm-text-color, #363636);
}

fieldset {
border: 0;
padding: 0;
margin: 0;
width: 100%;
}

.preset-buttons {
display: flex;
justify-content: center;
gap: 12px;
margin: var(--Spacings-lg, 24px) 0 var(--Spacings-lg, 24px) 0;
}

.preset-buttons button {
display: flex;
width: calc(var(--Font-Size-text-base) + 2.75rem);
height: calc(var(--Font-Size-text-base) + 2.75rem);
cursor: pointer;
padding: 16px 12px;
justify-content: center;
align-items: center;
gap: 10px;
border-radius: 100px;
border: 1px solid var(--text-color, #c9c9c9);

background: var(--background-color, #f2fbf9);
color: var(--Text-paragraph-standard, #7b7b7b);
font-family: var(--Font-Family-Inter, Inter);
font-size: var(--Font-Size-text-base, 16px);
font-style: normal;
font-weight: var(--Font-Weight-Bold, 700);
line-height: var(--Font-Line-Height-md, 24px);
}

.preset-buttons button:hover {
background: #0000002f;
}

.preset-buttons [aria-checked='true'] {
background: color-mix(
in srgb,
var(--primary-color, #f2fbf9) 45%,
transparent
);
border: 1px solid var(--primary-color, #5b5380);
}

.currency-symbol {
position: absolute;
left: var(--Spacings-md, 12px);
top: 50%;
transform: translateY(-50%);
color: var(--Text-paragraph-standard, #363636);
font-family: var(--Font-Family-Inter, Inter);
font-size: var(--Font-Size-text-base, 16px);
font-weight: var(--Font-Weight-Regular, 400);
pointer-events: none;
z-index: 1;
}

.form-input {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
align-self: stretch;
}

.form-input[aria-invalid='true'] {
border-color: var(--Colors-red-500, #ef4444);
}

.form-label {
display: flex;
justify-content: flex-start;
align-items: center;
gap: 2px;
color: var(--Text-paragraph-standard, #363636);
font-family: var(--Font-Family-Inter, Inter);
font-size: var(--Font-Size-text-xs, 12px);
font-style: normal;
font-weight: var(--Font-Weight-Regular, 400);
line-height: var(--Font-Line-Height-xs, 16px);
}

.amount-input-wrapper {
position: relative;
width: 100%;
margin-top: 4px;
}

.form-input.with-currency {
padding-left: calc(var(--Font-Size-text-base) + 2rem);
border-width: 2px;
}

.amount-error {
color: var(--Colors-red-500, #ef4444);
font-family: var(--Font-Family-Inter, Inter);
font-size: var(--Font-Size-text-sm, 14px);
font-style: normal;
font-weight: var(--Font-Weight-Regular, 400);
line-height: var(--Font-Line-Height-sm, 20px);
}
148 changes: 148 additions & 0 deletions components/src/widget/views/amount/amount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { LitElement, html, nothing, unsafeCSS } from 'lit'
import { property, query, state } from 'lit/decorators.js'
import { getCurrencySymbol } from '@c/utils'
import styles from './amount.css?raw'
import stylesBase from '../confirmation/confirmation.css?raw'

export interface AmountChangeEventDetail {
amount: number
onComplete: (error?: string | null) => void
}

export class PaymentAmount extends LitElement {
#debounceTimer: ReturnType<typeof setTimeout> | null = null

@property({ type: String }) currency = 'USD'
@property({ type: Array }) presets: number[] = [1, 5, 10]
@property({ type: Number }) value = 0

@query('input') private _inputEl!: HTMLInputElement

@state() private _errorMsg: string | null = null

static styles = [unsafeCSS(stylesBase), unsafeCSS(styles)]

connectedCallback(): void {
super.connectedCallback()
}

protected firstUpdated() {
this._inputEl.focus()
}

private _handleInput(e: Event) {
const el = e.target as HTMLInputElement
const rawValue = el.value

// assumes assetScale=2 here
const validFormat = /^\d*\.?\d{0,2}$/.test(rawValue)
if (!validFormat) {
el.value = this.value === 0 ? '' : this.value.toString()
return
}

const newValue = Number.parseFloat(rawValue) || 0
if (newValue === this.value) return
if (Number.isNaN(newValue)) return

this.value = newValue

this._errorMsg = null
this._processUpdate()
}

private _handlePresetClick(presetAmount: number) {
this._errorMsg = null
this.value = presetAmount
this._processUpdate(true)
}

private _processUpdate(immediate = false) {
if (this.#debounceTimer) clearTimeout(this.#debounceTimer)
this.#debounceTimer = setTimeout(
() => this.onChange(this.value),
immediate ? 0 : 750,
)
}

private onChange(amount: number) {
this.dispatchEvent(
new CustomEvent<AmountChangeEventDetail>('change', {
detail: {
amount,
onComplete: (error) => (this._errorMsg = error || null),
},
}),
)
}

render() {
const currencySymbol = getCurrencySymbol(this.currency)
const hasError = !!this._errorMsg

return html`
<fieldset>
<legend id="amount-label" class="form-label">Amount</legend>

<div class="amount-input-wrapper">
<span class="currency-symbol">${currencySymbol}</span>
<input
id="amount-input"
aria-labelledby="amount-label"
class="form-input with-currency"
type="text"
inputmode="decimal"
placeholder="1.5"
.value=${this.value === 0 ? '' : this.value.toString()}
@input=${this._handleInput}
@keydown=${allowOnlyNumericInput}
aria-invalid=${hasError}
aria-describedby=${hasError ? 'amount-error' : nothing}
@paste=${(ev: Event) => ev.preventDefault()}
autocomplete="off"
spellcheck="false"
/>
</div>
<p
id="amount-error"
class="amount-error"
role="alert"
>
${hasError ? this._errorMsg : nothing}
</p>
</div>

<div class="preset-buttons" role="radiogroup" aria-label="Preset amounts">
${this.presets.map(
(amount) => html`
<button
type="button"
role="radio"
aria-checked="${this.value === amount}"
@click=${() => this._handlePresetClick(amount)}
>
${currencySymbol}${amount}
</button>
`,
)}
</fieldset>
`
}
}

function allowOnlyNumericInput(
ev: KeyboardEvent & { currentTarget: HTMLInputElement },
) {
if (ev.key.length > 1 || ev.ctrlKey || ev.metaKey) return
if (ev.key === 'Tab') return
if (
!charIsNumber(ev.key) ||
(ev.key === '.' && ev.currentTarget.value.includes('.'))
) {
ev.preventDefault()
}
}

function charIsNumber(char?: string) {
return !!(char || '').match(/\d|\./)
}
64 changes: 0 additions & 64 deletions components/src/widget/views/confirmation/confirmation.css
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,6 @@
flex: 1 1 auto;
}

.preset-buttons {
display: flex;
justify-content: center;
gap: 12px;
margin: var(--Spacings-lg, 24px) 0 var(--Spacings-lg, 24px) 0;
}

.enter-amount-description {
color: var(--Text-paragraph-standard, #363636);
font-family: var(--Font-Family-Inter, Inter);
Expand All @@ -85,40 +78,6 @@
line-height: var(--Font-Line-Height-sm, 20px);
}

.preset-btn {
display: flex;
width: calc(var(--Font-Size-text-base) + 2.75rem);
height: calc(var(--Font-Size-text-base) + 2.75rem);
cursor: pointer;
padding: 16px 12px;
justify-content: center;
align-items: center;
gap: 10px;
border-radius: 100px;
border: 1px solid var(--text-color, #c9c9c9);

background: var(--background-color, #f2fbf9);
color: var(--Text-paragraph-standard, #7b7b7b);
font-family: var(--Font-Family-Inter, Inter);
font-size: var(--Font-Size-text-base, 16px);
font-style: normal;
font-weight: var(--Font-Weight-Bold, 700);
line-height: var(--Font-Line-Height-md, 24px);
}

.preset-btn:hover {
background: #0000002f;
}

.preset-btn.selected {
background: color-mix(
in srgb,
var(--primary-color, #f2fbf9) 45%,
transparent
);
border: 1px solid var(--primary-color, #5b5380);
}

.widget-body p,
.widget-body label:not(.form-label) {
color: var(--Text-paragraph-standard, #363636);
Expand All @@ -136,19 +95,6 @@
height: 100%;
}

.currency-symbol {
position: absolute;
left: var(--Spacings-md, 12px);
top: 50%;
transform: translateY(-50%);
color: var(--Text-paragraph-standard, #363636);
font-family: var(--Font-Family-Inter, Inter);
font-size: var(--Font-Size-text-base, 16px);
font-weight: var(--Font-Weight-Regular, 400);
pointer-events: none;
z-index: 1;
}

.payment-details {
display: flex;
padding: var(--Spacings-md, 12px);
Expand Down Expand Up @@ -352,16 +298,6 @@
line-height: var(--Font-Line-Height-xs, 16px);
}

.amount-input-wrapper {
position: relative;
width: 100%;
}

.form-input.with-currency {
padding-left: calc(var(--Font-Size-text-base) + 2rem);
border: 2px solid var(--Colors-silver-300, #c9c9c9);
}

.form-input,
.payment-note-input {
width: 100%;
Expand Down
Loading
Loading