diff --git a/cdn/src/paywall.ts b/cdn/src/paywall.ts index fd2bed03..30e50e89 100644 --- a/cdn/src/paywall.ts +++ b/cdn/src/paywall.ts @@ -4,7 +4,6 @@ import { sleep } from '@shared/utils' import { Paywall } from '@tools/components' import { fetchProfile, - fetchQuote, getScriptParams, getWallet, initiatePayment, @@ -35,10 +34,6 @@ function drawPaywall() { // TODO: create and call API }, getWallet: (walletAddressUrl) => getWallet(API_URL, walletAddressUrl), - fetchQuote({ sender, receiver, amount }) { - const receiveAmount = Number(amount) - return fetchQuote(API_URL, { sender, receiver, receiveAmount }) - }, async initiatePayment({ sender, receiver, amount, note }) { const receiveAmount = Number(amount) const redirectUrl = window.location.href diff --git a/components/src/paywall/components/common.css b/components/src/paywall/components/common.css new file mode 100644 index 00000000..0c917b11 --- /dev/null +++ b/components/src/paywall/components/common.css @@ -0,0 +1,35 @@ +.title { + font-size: 1.75em; +} + +.description { + font-size: 1em; +} + +button { + border: none; + display: flex; + justify-content: center; + align-items: center; + padding: 1em; + font-size: 1.25em; + border-radius: var(--border-radius); + background-color: var(--theme); + /* contrast-color() is unreliable in Chrome shadow DOM; approximate with OKLCH: + https://css-tricks.com/approximating-contrast-color-with-other-css-features/ */ + color: white; + color: oklch(from var(--theme) round(1.21 - L) 0 0); + font-weight: 600; + cursor: pointer; +} + +button:disabled { + opacity: 0.75; + cursor: not-allowed; +} + +.footer { + font-size: 0.6em; + margin-top: -1.25em; + text-align: center; +} diff --git a/components/src/paywall/components/form.css b/components/src/paywall/components/form.css new file mode 100644 index 00000000..5ff09a1a --- /dev/null +++ b/components/src/paywall/components/form.css @@ -0,0 +1,47 @@ +:host { + display: flex; + flex-direction: column; + gap: 1em; + width: 100%; + height: 100%; +} + +form { + display: flex; + flex-direction: column; + gap: 1em; + margin-top: auto; +} + +form label { + display: block; + margin-bottom: 0.2em; + font-size: 1em; + font-weight: 600; +} + +form input { + font-family: inherit; + border: 2px solid rgba(0, 0, 0, 0.1); + display: flex; + width: 100%; + justify-content: center; + align-items: center; + padding: 1em; + font-size: 1.25em; + border-radius: var(--border-radius); + background-color: #fff; +} + +form wm-dots-loader { + --primary-color: var(--theme); +} + +form input[aria-invalid='true'] { + border-color: var(--color-error); +} +form div p.error { + margin-top: 0.2em; + font-size: 0.9em; + color: var(--color-error); +} diff --git a/components/src/paywall/components/form.ts b/components/src/paywall/components/form.ts new file mode 100644 index 00000000..9e03ec69 --- /dev/null +++ b/components/src/paywall/components/form.ts @@ -0,0 +1,90 @@ +import { html, LitElement, unsafeCSS } from 'lit' +import { property, state } from 'lit/decorators.js' +import { DotsLoader } from '@c/shared/dots-loader' +import { registerComponents } from '@c/utils' +import stylesCommon from './common.css?raw' +import styles from './form.css?raw' +import { DEFAULTS } from '../utils' +import styleTokens from '../vars.css?raw' + +export type FormSubmitEventDetail = { + walletAddress: string + onComplete(error?: string): void +} + +export class PaywallWalletAddressForm extends LitElement { + static styles = [ + unsafeCSS(styleTokens), + unsafeCSS(stylesCommon), + unsafeCSS(styles), + ] + + @property({ type: String }) title = DEFAULTS.title.text + @property({ type: String }) description = DEFAULTS.description.text + @property({ type: String }) ctaText = DEFAULTS.ctaButton.text + + @state() private _error = '' + @state() private _loading = false + + connectedCallback() { + super.connectedCallback() + registerComponents({ + 'wm-dots-loader': DotsLoader, + }) + } + + render() { + return html` +

${this.title}

+

${this.description}

+ +
+
+ + +

+ ${this._error}  +

+
+ + +
+ ` + } + + private async handleSubmit(ev: SubmitEvent) { + ev.preventDefault() + this._error = '' + const formData = new FormData(ev.target as HTMLFormElement) + const walletAddress = formData.get('walletAddress') as string + + if (!walletAddress) { + this._error = 'Please enter a valid wallet address' + return + } + + this._loading = true + const detail: FormSubmitEventDetail = { + walletAddress, + onComplete: (error) => { + this._error = error || '' + this._loading = false + }, + } + this.dispatchEvent(new CustomEvent('submit', { detail })) + } +} diff --git a/components/src/paywall/components/home.css b/components/src/paywall/components/home.css index 5b09b676..e30842e6 100644 --- a/components/src/paywall/components/home.css +++ b/components/src/paywall/components/home.css @@ -6,14 +6,6 @@ height: 100%; } -.title { - font-size: 1.75em; -} - -.description { - font-size: 1em; -} - .price { background-color: rgba(0, 0, 0, 0.05); border: rgba(0, 0, 0, 0.1); @@ -33,26 +25,3 @@ font-weight: 600; } } - -button { - border: none; - display: flex; - justify-content: center; - align-items: center; - padding: 1em; - font-size: 1.25em; - border-radius: var(--border-radius); - background-color: var(--theme); - /* contrast-color() is unreliable in Chrome shadow DOM; approximate with OKLCH: - https://css-tricks.com/approximating-contrast-color-with-other-css-features/ */ - color: white; - color: oklch(from var(--theme) round(1.21 - L) 0 0); - font-weight: 600; - cursor: pointer; -} - -.footer { - font-size: 0.6em; - margin-top: -1.25em; - text-align: center; -} diff --git a/components/src/paywall/components/home.ts b/components/src/paywall/components/home.ts index 808094f9..312c9777 100644 --- a/components/src/paywall/components/home.ts +++ b/components/src/paywall/components/home.ts @@ -1,14 +1,17 @@ import { html, LitElement, unsafeCSS } from 'lit' import { property } from 'lit/decorators.js' -import { createDefaultPaywallProfile } from '@shared/default-data' import { formatCurrency } from '@shared/utils' +import stylesCommon from './common.css?raw' import styles from './home.css?raw' +import { DEFAULTS } from '../utils' import styleTokens from '../vars.css?raw' -const DEFAULTS = createDefaultPaywallProfile('') - export class PaywallHome extends LitElement { - static styles = [unsafeCSS(styleTokens), unsafeCSS(styles)] + static styles = [ + unsafeCSS(styleTokens), + unsafeCSS(stylesCommon), + unsafeCSS(styles), + ] @property({ type: Object, attribute: false }) price: PaymentCurrencyAmount = DEFAULTS.price @@ -25,8 +28,12 @@ export class PaywallHome extends LitElement {
Unlock ${formatCurrency(this.price)}
- + ` } + + private onClick() { + this.dispatchEvent(new CustomEvent('payStart')) + } } diff --git a/components/src/paywall/controller.ts b/components/src/paywall/controller.ts index c4d9f505..db6c3128 100644 --- a/components/src/paywall/controller.ts +++ b/components/src/paywall/controller.ts @@ -7,15 +7,6 @@ type WalletAddressUrl = string /** The amount sender wants to send (like "1.05"), does not include fees */ type UserAmount = number | PaymentCurrencyAmount['value'] -interface QuoteInput { - sender: WalletAddressInfo - receiver: WalletAddressInfo - amount: UserAmount -} -type QuoteResult = - | { debitAmount: PaymentCurrencyAmount; receiveAmount: PaymentCurrencyAmount } - | { error: string; minSendAmount?: PaymentCurrencyAmount } - interface InitiatePaymentInput { sender: WalletAddressInfo receiver: WalletAddressInfo @@ -29,6 +20,8 @@ interface InitiatePaymentResult { type Entitlement = 'no-access' | 'auth-required' | 'has-access' +export type Screens = 'home' | 'form' + export interface Controller { receiverWalletAddressUrl: string cdnUrl: string @@ -48,7 +41,6 @@ export interface Controller { ): Promise getWallet(walletAddressUrl: WalletAddressUrl): Promise - fetchQuote(request: QuoteInput): Promise initiatePayment(request: InitiatePaymentInput): Promise getStatus( paymentId: string, @@ -75,12 +67,6 @@ export const NO_OP_CONTROLLER: Controller = { publicName: 'Wallet (Preview)', }) }, - fetchQuote({ amount, sender, receiver }) { - amount = String(amount) - const debitAmount = { value: amount, currency: sender.assetCode } - const receiveAmount = { value: amount, currency: receiver.assetCode } - return Promise.resolve({ debitAmount, receiveAmount }) - }, initiatePayment() { return Promise.resolve({ paymentId: 'payment-id', diff --git a/components/src/paywall/index.ts b/components/src/paywall/index.ts index e28e89e4..8ba89489 100644 --- a/components/src/paywall/index.ts +++ b/components/src/paywall/index.ts @@ -1,6 +1,10 @@ import { html, LitElement, nothing, unsafeCSS } from 'lit' import { property, state } from 'lit/decorators.js' -import { type Controller, NO_OP_CONTROLLER } from '@c/paywall/controller' +import { + NO_OP_CONTROLLER, + type Controller, + type Screens, +} from '@c/paywall/controller' import { applyFontFamily, registerComponents } from '@c/utils.js' import { BORDER_RADIUS, @@ -8,6 +12,10 @@ import { type PaywallProfile, } from '@shared/types' import { sleep } from '@shared/utils' +import { + PaywallWalletAddressForm, + type FormSubmitEventDetail, +} from './components/form.js' import { PaywallHome } from './components/home.js' import styles from './styles.css?raw' import styleTokens from './vars.css?raw' @@ -20,6 +28,7 @@ export class Paywall extends LitElement { @property({ type: Boolean, reflect: true }) hidden = true @state() _ready = false + @state() _screen: Screens = 'home' connectedCallback(): void { super.connectedCallback() @@ -30,6 +39,7 @@ export class Paywall extends LitElement { registerComponents({ 'wmt-paywall-home': PaywallHome, + 'wmt-paywall-form': PaywallWalletAddressForm, }) void this.#init() @@ -78,14 +88,73 @@ export class Paywall extends LitElement { const { title, description, ctaButton, price } = this.#config + if (this._screen === 'form') { + return html`` + } + return html`` } + async #onPayStart() { + this.#setScreen('form') + void this.#getReceiver() // pre-fetch + } + + async #onSubmit(ev: CustomEvent) { + const { walletAddress, onComplete } = ev.detail + try { + await this.#handleSubmit(walletAddress) + onComplete() + } catch (err) { + const error = err as Error + onComplete(error.message) + } + } + + async #handleSubmit(walletAddress: string) { + const sender = await this.#controller.getWallet(walletAddress) + const receiver = await this.#getReceiver() + + const result = await this.#controller.initiatePayment({ + sender, + receiver, + amount: this.#price, + note: this.defaultNote, + }) + + if (!this.#controller.isPreviewMode) { + window.location.href = result.grantRedirectUrl + } + } + + #receiver_!: ReturnType + #getReceiver() { + if (!this.#receiver_) { + const walletAddress = this.#controller.receiverWalletAddressUrl + this.#receiver_ = this.#controller.getWallet(walletAddress) + } + return this.#receiver_ + } + + get defaultNote() { + return `Pay Per Article service` + } + + #setScreen(screen: Screens) { + this._screen = screen + } + @state() _delayComplete = false private async showAfterDelay(connectedAt: number) { const delay = this.#config.behavior.delay.value * 1000 diff --git a/components/src/paywall/utils.ts b/components/src/paywall/utils.ts new file mode 100644 index 00000000..9ccf9cc5 --- /dev/null +++ b/components/src/paywall/utils.ts @@ -0,0 +1,3 @@ +import { createDefaultPaywallProfile } from '@shared/default-data' + +export const DEFAULTS = createDefaultPaywallProfile('') diff --git a/components/src/paywall/vars.css b/components/src/paywall/vars.css index 2ebcc6a3..b0e689ff 100644 --- a/components/src/paywall/vars.css +++ b/components/src/paywall/vars.css @@ -5,6 +5,8 @@ --color: var(--wmt-color, #6b6e76); --theme: var(--wmt-theme, #56b7b5); --border-radius: var(--wmt-border-radius, 1rem); + + --color-error: #e51d25; } * {