@openreceive/elements 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenReceive contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,42 @@
1
+ import { type OpenReceiveBrowserLogger, type OpenReceivePaymentMethod, type OpenReceiveQrEncoder, type OpenReceiveRegionId, formatOpenReceiveMsats, type Status } from "@openreceive/browser/internal";
2
+ export interface CheckoutView {
3
+ readonly invoice_id?: string;
4
+ readonly invoice: string;
5
+ readonly payment_hash?: string;
6
+ readonly amount_msats?: number;
7
+ readonly fiat_quote?: {
8
+ readonly fiat?: {
9
+ readonly currency?: string;
10
+ readonly value?: string;
11
+ };
12
+ } | null;
13
+ readonly status?: Status;
14
+ readonly expires_at?: number;
15
+ readonly theme?: "light" | "dark";
16
+ readonly payment_wizard?: boolean;
17
+ readonly wizard?: OpenReceiveElementsWizardView;
18
+ }
19
+ export interface OpenReceiveElementsWizardView {
20
+ readonly selectedMethod?: OpenReceivePaymentMethod | null;
21
+ readonly selectedCountryCode?: string;
22
+ readonly selectedBitcoinRoute?: string | null;
23
+ readonly selectedCryptoRoute?: string | null;
24
+ readonly selectedRegion?: OpenReceiveRegionId;
25
+ readonly countryPickerOpen?: boolean;
26
+ readonly activeTutorialProviderId?: string | null;
27
+ readonly activeTutorialIndex?: number;
28
+ readonly activeTutorialCopied?: boolean;
29
+ }
30
+ export interface DefineOpenReceiveElementsOptions {
31
+ readonly tagName?: string;
32
+ readonly themeToggleTagName?: string;
33
+ readonly registry?: CustomElementRegistry;
34
+ readonly qrEncoder?: OpenReceiveQrEncoder;
35
+ readonly logger?: OpenReceiveBrowserLogger;
36
+ }
37
+ export { OPENRECEIVE_THEME_TOGGLE_ELEMENT_TAG_NAME } from "@openreceive/browser/internal";
38
+ export declare function renderCheckoutHtml(view: CheckoutView): string;
39
+ export declare function renderOpenReceiveThemeToggleHtml(label: string, resolvedTheme?: "light" | "dark"): string;
40
+ export declare function renderOpenReceivePaymentWizardHtml(view?: OpenReceiveElementsWizardView): string;
41
+ export declare function defineOpenReceiveElements(options?: DefineOpenReceiveElementsOptions): void;
42
+ export declare const formatMsats: typeof formatOpenReceiveMsats;
package/dist/index.js ADDED
@@ -0,0 +1,781 @@
1
+ import { OPENRECEIVE_CHECKOUT_ELEMENT_EVENTS, OPENRECEIVE_CHECKOUT_DATA_ATTRIBUTES, OPENRECEIVE_CHECKOUT_DATA_SELECTORS, OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES, OPENRECEIVE_COUNTRY_STORAGE_KEY, OPENRECEIVE_CHECKOUT_ELEMENT_PARTS, OPENRECEIVE_CHECKOUT_ELEMENT_PART_SELECTORS, OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES, OPENRECEIVE_PAYMENT_WIZARD_SELECTORS, OPENRECEIVE_THEME_TOGGLE_ELEMENT_ATTRIBUTES, OPENRECEIVE_THEME_TOGGLE_ELEMENT_PARTS, OPENRECEIVE_THEME_TOGGLE_ELEMENT_PART_SELECTORS, OPENRECEIVE_THEME_TOGGLE_ELEMENT_TAG_NAME, copyInvoice, createCheckoutActionEvent, createCheckoutDisplayModel, createCheckoutController, createCheckoutErrorEvent, createCheckoutStatusModel, createCheckoutSnapshotFromDisplayData, createCheckoutStateFromDisplayData, createCheckoutStateEvent, createOpenReceivePaymentWizardModel, createOpenReceivePaymentWizardSelection, createCheckoutProviderCopyEvent, createOpenReceiveThemeChangeEvent, createOpenReceiveTransientFeedbackController, createOpenReceiveWizardRouteAssetDisplays, createOpenReceiveWizardRouteDisplays, createQrSvg, escapeOpenReceiveHtml, formatOpenReceiveMsats, getOpenReceiveDefaultCountryCode, getOpenReceivePaymentMethodIcon, getOpenReceiveRegionForCountry, getOpenReceiveWizardEmptyMessage, openReceiveCheckoutLabels, openReceiveCheckoutElementStyles, openWallet, openReceivePaymentMethods, openReceiveThemeToggleElementStyles, parseOpenReceiveBooleanAttribute, parseOpenReceiveOptionalInteger, parseOpenReceivePaymentMethod, parseOpenReceiveRegion, parseOpenReceiveResolvedTheme, parseOpenReceiveThemePreference, readOpenReceiveStoredCountryCode, status as deriveStatus, syncOpenReceiveStoredThemeControls, toggleOpenReceiveStoredThemeControls, updateOpenReceivePaymentWizardSelection, writeOpenReceiveStoredCountryCode } from "@openreceive/browser/internal";
2
+ const DEFAULT_TAG_NAME = "openreceive-checkout";
3
+ export { OPENRECEIVE_THEME_TOGGLE_ELEMENT_TAG_NAME } from "@openreceive/browser/internal";
4
+ export function renderCheckoutHtml(view) {
5
+ const display = createCheckoutDisplayModel(view);
6
+ const checkoutState = createElementCheckoutState(view);
7
+ const amountLabel = display.amountLabel === undefined
8
+ ? ""
9
+ : `<span part="amount">${escapeHtml(display.amountLabel)}</span>`;
10
+ const fiatLabel = display.fiatLabel === undefined
11
+ ? ""
12
+ : `<span part="amount">${escapeHtml(display.fiatLabel)}</span>`;
13
+ const statusLabel = view.status ?? (checkoutState === undefined
14
+ ? deriveStatus(view)
15
+ : deriveStatus(checkoutState));
16
+ const stateLabel = `<span part="state" data-state="${escapeHtml(statusLabel)}">${escapeHtml(statusLabel)}</span>`;
17
+ const status = checkoutState === undefined
18
+ ? ""
19
+ : renderElementPaymentStatusHtml(checkoutState);
20
+ const statusModel = checkoutState === undefined
21
+ ? undefined
22
+ : createCheckoutStatusModel(checkoutState);
23
+ const expired = statusModel?.phase === "expired";
24
+ const wizard = expired || view.payment_wizard === false
25
+ ? ""
26
+ : renderOpenReceivePaymentWizardHtml(view.wizard);
27
+ return `
28
+ <style>${openReceiveCheckoutElementStyles}</style>
29
+ <section part="root"${view.theme === undefined ? "" : ` data-theme="${escapeHtml(view.theme)}"`}>
30
+ ${expired ? "" : `<div part="qr" ${OPENRECEIVE_CHECKOUT_DATA_ATTRIBUTES.qr}></div>`}
31
+ ${status}
32
+ <div part="meta">${amountLabel}${fiatLabel}${stateLabel}</div>
33
+ <div part="actions">
34
+ ${expired
35
+ ? `<button part="${OPENRECEIVE_CHECKOUT_ELEMENT_PARTS.startOver}" type="button">${escapeHtml(openReceiveCheckoutLabels.startOver)}</button>`
36
+ : `<button part="${OPENRECEIVE_CHECKOUT_ELEMENT_PARTS.copy}" type="button">${escapeHtml(openReceiveCheckoutLabels.copyInvoice)}</button>`}
37
+ </div>
38
+ ${wizard}
39
+ </section>
40
+ `;
41
+ }
42
+ export function renderOpenReceiveThemeToggleHtml(label, resolvedTheme = label.toLowerCase().includes("dark") ? "dark" : "light") {
43
+ return `
44
+ <style>${openReceiveThemeToggleElementStyles}</style>
45
+ <button
46
+ aria-label="${escapeHtml(label)}"
47
+ class="or-theme-toggle-${escapeHtml(resolvedTheme)}"
48
+ part="${OPENRECEIVE_THEME_TOGGLE_ELEMENT_PARTS.button}"
49
+ title="${escapeHtml(label)}"
50
+ type="button"
51
+ ${OPENRECEIVE_CHECKOUT_DATA_ATTRIBUTES.themeToggle}
52
+ >
53
+ <span class="or-theme-toggle-track" aria-hidden="true">
54
+ <span class="or-theme-toggle-icon or-theme-toggle-icon-light"></span>
55
+ </span>
56
+ <span class="or-theme-toggle-label">${escapeHtml(label)}</span>
57
+ </button>
58
+ `;
59
+ }
60
+ export function renderOpenReceivePaymentWizardHtml(view = {}) {
61
+ const selectedCountryCode = view.selectedCountryCode ?? getOpenReceiveDefaultCountryCode();
62
+ const selection = {
63
+ selectedMethod: view.selectedMethod ?? null,
64
+ selectedCountryCode,
65
+ selectedBitcoinRoute: view.selectedBitcoinRoute ?? null,
66
+ selectedCryptoRoute: view.selectedCryptoRoute ?? null,
67
+ selectedRegion: view.selectedRegion ?? getOpenReceiveRegionForCountry(selectedCountryCode),
68
+ countryPickerOpen: view.countryPickerOpen ?? false
69
+ };
70
+ const model = createOpenReceivePaymentWizardModel(selection);
71
+ const { wizard } = model;
72
+ const routeAssetDisplays = createOpenReceiveWizardRouteAssetDisplays(model.routeAssets, {
73
+ selectedRoute: model.selectedRoute
74
+ });
75
+ const routeDisplays = createOpenReceiveWizardRouteDisplays(wizard.routes);
76
+ const showRoutePicker = routeAssetDisplays.length > 0 &&
77
+ (model.selectedRoute === null || routeDisplays.length === 0);
78
+ const breadcrumbs = selection.selectedMethod === null
79
+ ? ""
80
+ : renderWizardBreadcrumbsHtml({
81
+ method: selection.selectedMethod,
82
+ selectedRoute: model.selectedRoute,
83
+ routeAssets: routeAssetDisplays
84
+ });
85
+ const methodPicker = selection.selectedMethod === null
86
+ ? `
87
+ <div>
88
+ <h2>${escapeHtml(openReceiveCheckoutLabels.wizardTitle)}</h2>
89
+ <p>${escapeHtml(openReceiveCheckoutLabels.wizardSubtitle)}</p>
90
+ </div>
91
+ <div part="method-grid">
92
+ ${openReceivePaymentMethods.map((method) => `
93
+ <button
94
+ part="method"
95
+ ${OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.method}="${escapeHtml(method.id)}"
96
+ type="button"
97
+ >
98
+ <img alt="" src="${escapeHtml(getOpenReceivePaymentMethodIcon(method.id))}">
99
+ <strong>${escapeHtml(method.title)}</strong>
100
+ <small>${escapeHtml(method.detail)}</small>
101
+ </button>
102
+ `).join("")}
103
+ </div>
104
+ `
105
+ : "";
106
+ return `
107
+ <section part="wizard" ${OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.root}>
108
+ ${methodPicker}
109
+ ${breadcrumbs}
110
+ ${showRoutePicker ? `
111
+ <div part="route-picker">
112
+ ${routeAssetDisplays.map((asset) => `
113
+ <button
114
+ part="route${asset.selected ? " selected" : ""}"
115
+ ${OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.route}="${escapeHtml(asset.id)}"
116
+ type="button"
117
+ >
118
+ <img alt="" src="${escapeHtml(asset.icon)}">
119
+ <strong>${escapeHtml(asset.label)}</strong>
120
+ <small>${escapeHtml(asset.subtitle)}</small>
121
+ </button>
122
+ `).join("")}
123
+ </div>
124
+ ` : ""}
125
+ ${selection.selectedMethod === null ? "" : `
126
+ <div part="wizard-results">
127
+ ${routeDisplays.length === 0 ? `
128
+ <p part="wizard-empty">${escapeHtml(getOpenReceiveWizardEmptyMessage(selection.selectedMethod))}</p>
129
+ ` : routeDisplays.map((route) => `
130
+ <section part="wizard-route">
131
+ <h3>
132
+ ${escapeHtml(route.title)}
133
+ ${wizard.selectedRail === null ? "" : renderCountrySelectHtml({
134
+ countries: model.countryDisplays,
135
+ selectedCountryCode: selection.selectedCountryCode
136
+ })}
137
+ </h3>
138
+ <div part="provider-grid">
139
+ ${route.providers.map((provider) => `
140
+ <article part="provider${provider.recommended ? " selected" : ""}">
141
+ <div part="provider-heading">
142
+ <img alt="" src="${escapeHtml(provider.icon)}">
143
+ <h4>${escapeHtml(provider.name)}</h4>
144
+ ${provider.recommendedLabel === null ? "" : `<span part="recommended">${escapeHtml(provider.recommendedLabel)}</span>`}
145
+ </div>
146
+ <p part="provider-kind">${escapeHtml(provider.kind)}</p>
147
+ <div part="provider-actions">
148
+ ${renderProviderOpenActionHtml(provider)}
149
+ </div>
150
+ </article>
151
+ `).join("")}
152
+ </div>
153
+ </section>
154
+ `).join("")}
155
+ </div>
156
+ `}
157
+ ${renderTutorialModalHtml(routeDisplays, view.activeTutorialProviderId ?? null, view.activeTutorialIndex ?? 0, view.activeTutorialCopied ?? false)}
158
+ </section>
159
+ `;
160
+ }
161
+ function renderWizardBreadcrumbsHtml(options) {
162
+ const method = openReceivePaymentMethods.find((candidate) => candidate.id === options.method);
163
+ const methodLabel = method?.title ?? openReceiveCheckoutLabels.paymentMethod;
164
+ const routeLabel = options.selectedRoute === null || options.routeAssets.length <= 1
165
+ ? null
166
+ : options.routeAssets.find((asset) => asset.id === options.selectedRoute)?.label ?? options.selectedRoute;
167
+ return `
168
+ <nav part="wizard-breadcrumbs" aria-label="Payment path">
169
+ <button
170
+ part="wizard-breadcrumb"
171
+ ${OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.breadcrumb}="method"
172
+ type="button"
173
+ >
174
+ <span>${escapeHtml(openReceiveCheckoutLabels.paymentMethod)}</span>
175
+ </button>
176
+ <span part="wizard-breadcrumb-separator" aria-hidden="true">/</span>
177
+ ${routeLabel === null
178
+ ? `<span part="wizard-breadcrumb-current">${escapeHtml(methodLabel)}</span>`
179
+ : `
180
+ <button
181
+ part="wizard-breadcrumb"
182
+ ${OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.breadcrumb}="route"
183
+ type="button"
184
+ >
185
+ <span>${escapeHtml(methodLabel)}</span>
186
+ </button>
187
+ <span part="wizard-breadcrumb-separator" aria-hidden="true">/</span>
188
+ <span part="wizard-breadcrumb-current">${escapeHtml(routeLabel)}</span>
189
+ `}
190
+ </nav>
191
+ `;
192
+ }
193
+ function renderProviderOpenActionHtml(provider) {
194
+ if (provider.tutorials.length === 0) {
195
+ return `<a href="${escapeHtml(provider.url)}" rel="noreferrer" target="_blank">${escapeHtml(provider.openLabel)}</a>`;
196
+ }
197
+ return `
198
+ <button
199
+ part="provider-open"
200
+ ${OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.providerTutorial}="${escapeHtml(provider.id)}"
201
+ ${OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.providerTutorialIndex}="0"
202
+ type="button"
203
+ >${escapeHtml(provider.openLabel)}</button>
204
+ `;
205
+ }
206
+ function renderTutorialModalHtml(routes, activeProviderId, activeTutorialIndex, copied) {
207
+ if (activeProviderId === null)
208
+ return "";
209
+ const provider = routes
210
+ .flatMap((route) => route.providers)
211
+ .find((candidate) => candidate.id === activeProviderId);
212
+ if (provider === undefined || provider.tutorials.length === 0)
213
+ return "";
214
+ const totalSteps = provider.tutorials.length + 1;
215
+ const stepIndex = Math.max(0, Math.min(provider.tutorials.length, activeTutorialIndex));
216
+ const tutorial = stepIndex === 0 ? undefined : provider.tutorials[stepIndex - 1];
217
+ const previousIndex = Math.max(0, stepIndex - 1);
218
+ const nextIndex = Math.min(provider.tutorials.length, stepIndex + 1);
219
+ const isFinalStep = stepIndex === provider.tutorials.length;
220
+ const body = stepIndex === 0
221
+ ? `
222
+ <div part="tutorial-intro">
223
+ <img part="tutorial-provider-logo" alt="" src="${escapeHtml(provider.icon)}">
224
+ <p>${escapeHtml(openReceiveCheckoutLabels.tutorialIntroPrefix)} ${escapeHtml(provider.name)}.</p>
225
+ <p>${escapeHtml(openReceiveCheckoutLabels.tutorialIntroCopy)}</p>
226
+ <button part="tutorial-copy" type="button">${escapeHtml(openReceiveCheckoutLabels.copyInvoice)}</button>
227
+ ${copied
228
+ ? `<p part="tutorial-copy-message">${escapeHtml(openReceiveCheckoutLabels.tutorialCopiedContinue)}</p>`
229
+ : ""}
230
+ </div>
231
+ `
232
+ : `
233
+ <div part="tutorial-frame">
234
+ <img part="tutorial-image" alt="${escapeHtml(tutorial?.caption ?? "")}" src="${escapeHtml(tutorial?.image ?? "")}">
235
+ </div>
236
+ <p part="tutorial-caption">${escapeHtml(tutorial?.caption ?? "")}</p>
237
+ `;
238
+ return `
239
+ <div part="tutorial" role="dialog" aria-modal="true" aria-label="${escapeHtml(openReceiveCheckoutLabels.tutorialTitlePrefix)} ${escapeHtml(provider.name)}" tabindex="-1">
240
+ <div part="tutorial-dialog">
241
+ <div part="tutorial-header">
242
+ <div part="tutorial-title">
243
+ <img part="tutorial-header-logo" alt="" src="${escapeHtml(provider.icon)}">
244
+ <h3>${escapeHtml(openReceiveCheckoutLabels.tutorialTitlePrefix)} ${escapeHtml(provider.name)}</h3>
245
+ </div>
246
+ <button
247
+ part="tutorial-close"
248
+ ${OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.providerTutorial}=""
249
+ type="button"
250
+ aria-label="Close"
251
+ >X</button>
252
+ </div>
253
+ ${body}
254
+ <div part="tutorial-steps" aria-hidden="true">
255
+ ${Array.from({ length: totalSteps }, (_, index) => `
256
+ <span part="${index === stepIndex ? "tutorial-step-active" : "tutorial-step"}"></span>
257
+ `).join("")}
258
+ </div>
259
+ <p part="tutorial-progress">Step ${stepIndex + 1} of ${totalSteps}</p>
260
+ <div part="tutorial-controls">
261
+ <button
262
+ part="tutorial-nav"
263
+ ${OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.providerTutorial}="${escapeHtml(provider.id)}"
264
+ ${OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.providerTutorialIndex}="${previousIndex}"
265
+ type="button"
266
+ ${stepIndex === 0 ? "disabled" : ""}
267
+ >Back</button>
268
+ <button
269
+ part="tutorial-nav"
270
+ ${OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.providerTutorial}="${isFinalStep ? "" : escapeHtml(provider.id)}"
271
+ ${OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.providerTutorialIndex}="${nextIndex}"
272
+ type="button"
273
+ >${escapeHtml(isFinalStep ? openReceiveCheckoutLabels.tutorialExit : "Next")}</button>
274
+ </div>
275
+ </div>
276
+ </div>
277
+ `;
278
+ }
279
+ function renderCountrySelectHtml(options) {
280
+ return `
281
+ <label part="country-select">
282
+ <span>${escapeHtml(openReceiveCheckoutLabels.chooseCountry)}</span>
283
+ <select ${OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.country}="${escapeHtml(options.selectedCountryCode)}">
284
+ ${options.countries.map((country) => `
285
+ <option
286
+ value="${escapeHtml(country.code)}"
287
+ ${country.code === options.selectedCountryCode ? "selected" : ""}
288
+ >${escapeHtml(country.label)}</option>
289
+ `).join("")}
290
+ </select>
291
+ </label>
292
+ `;
293
+ }
294
+ function createElementCheckoutState(view) {
295
+ if (view.invoice_id === undefined)
296
+ return undefined;
297
+ return createCheckoutStateFromDisplayData({
298
+ ...view,
299
+ ...(view.status === undefined
300
+ ? {}
301
+ : { transaction_state: transactionStateFromStatus(view.status) })
302
+ });
303
+ }
304
+ function transactionStateFromStatus(status) {
305
+ if (status === "paid")
306
+ return "settled";
307
+ if (status === "expired")
308
+ return "expired";
309
+ if (status === "failed")
310
+ return "failed";
311
+ return "pending";
312
+ }
313
+ function parseElementStatus(value) {
314
+ if (value === "pending" ||
315
+ value === "paid" ||
316
+ value === "expired" ||
317
+ value === "failed") {
318
+ return value;
319
+ }
320
+ return undefined;
321
+ }
322
+ function renderElementPaymentStatusHtml(state) {
323
+ const status = createCheckoutStatusModel(state);
324
+ const countdown = status.countdownLabel === undefined
325
+ ? ""
326
+ : `<small part="countdown">${escapeHtml(status.countdownPrefix)} <strong>${escapeHtml(status.countdownLabel)}</strong></small>`;
327
+ return `
328
+ <div part="status">
329
+ ${status.waiting ? `<span part="spinner" aria-hidden="true"></span>` : ""}
330
+ <div>
331
+ <strong>${escapeHtml(status.title)}</strong>
332
+ <p>${escapeHtml(status.detail)}</p>
333
+ ${countdown}
334
+ </div>
335
+ </div>
336
+ `;
337
+ }
338
+ export function defineOpenReceiveElements(options = {}) {
339
+ const registry = options.registry ?? globalThis.customElements;
340
+ const HTMLElementCtor = globalThis.HTMLElement;
341
+ const tagName = options.tagName ?? DEFAULT_TAG_NAME;
342
+ const themeToggleTagName = options.themeToggleTagName ?? OPENRECEIVE_THEME_TOGGLE_ELEMENT_TAG_NAME;
343
+ if (registry === undefined || HTMLElementCtor === undefined) {
344
+ throw new Error("Custom elements are unavailable in this environment.");
345
+ }
346
+ class CheckoutElement extends HTMLElementCtor {
347
+ selection = createOpenReceivePaymentWizardSelection({
348
+ storedCountryCode: readOpenReceiveStoredCountryCode(),
349
+ defaultCountryCode: getOpenReceiveDefaultCountryCode()
350
+ });
351
+ activeTutorialProviderId = null;
352
+ activeTutorialIndex = 0;
353
+ activeTutorialCopied = false;
354
+ controller;
355
+ announcedSettledPaymentHash;
356
+ static get observedAttributes() {
357
+ return [
358
+ OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.invoiceId,
359
+ OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.invoice,
360
+ OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.paymentHash,
361
+ OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.amountMsats,
362
+ OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.fiatCurrency,
363
+ OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.fiatValue,
364
+ OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.status,
365
+ OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.expiresAt,
366
+ OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.lookupUrl,
367
+ OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.theme,
368
+ OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.paymentWizard
369
+ ];
370
+ }
371
+ connectedCallback() {
372
+ this.render();
373
+ this.startCheckoutController();
374
+ }
375
+ attributeChangedCallback() {
376
+ if (!this.isConnected)
377
+ return;
378
+ this.render();
379
+ this.startCheckoutController();
380
+ }
381
+ disconnectedCallback() {
382
+ this.stopCheckoutController();
383
+ }
384
+ render() {
385
+ const invoice = this.getAttribute(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.invoice);
386
+ if (invoice === null || invoice === "") {
387
+ this.replaceChildren();
388
+ return;
389
+ }
390
+ const root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
391
+ root.innerHTML = renderCheckoutHtml({
392
+ invoice_id: this.getAttribute(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.invoiceId) ?? undefined,
393
+ invoice,
394
+ payment_hash: this.getAttribute(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.paymentHash) ?? undefined,
395
+ amount_msats: parseOpenReceiveOptionalInteger(this.getAttribute(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.amountMsats), { label: "amount-msats" }),
396
+ fiat_quote: readElementFiatQuote(this),
397
+ status: parseElementStatus(this.getAttribute(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.status)),
398
+ expires_at: parseOpenReceiveOptionalInteger(this.getAttribute(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.expiresAt), { label: "expires-at" }),
399
+ theme: parseOpenReceiveResolvedTheme(this.getAttribute(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.theme)),
400
+ payment_wizard: parseOpenReceiveBooleanAttribute(this.getAttribute(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.paymentWizard)),
401
+ wizard: {
402
+ selectedMethod: this.selection.selectedMethod,
403
+ selectedCountryCode: this.selection.selectedCountryCode,
404
+ selectedBitcoinRoute: this.selection.selectedBitcoinRoute,
405
+ selectedCryptoRoute: this.selection.selectedCryptoRoute,
406
+ selectedRegion: this.selection.selectedRegion,
407
+ countryPickerOpen: this.selection.countryPickerOpen,
408
+ activeTutorialProviderId: this.activeTutorialProviderId,
409
+ activeTutorialIndex: this.activeTutorialIndex,
410
+ activeTutorialCopied: this.activeTutorialCopied
411
+ }
412
+ });
413
+ const copyButton = root.querySelector(OPENRECEIVE_CHECKOUT_ELEMENT_PART_SELECTORS.copy);
414
+ copyButton?.addEventListener("click", () => {
415
+ void (this.controller?.copyInvoice() ?? copyInvoice({ invoice, logger: options.logger }))
416
+ .then(() => {
417
+ showElementCopyFeedback(copyButton);
418
+ this.dispatchEvent(createCheckoutActionEvent(OPENRECEIVE_CHECKOUT_ELEMENT_EVENTS.copy));
419
+ })
420
+ .catch((error) => this.dispatchError(error));
421
+ });
422
+ root.querySelector(OPENRECEIVE_CHECKOUT_ELEMENT_PART_SELECTORS.open)?.addEventListener("click", (event) => {
423
+ event.preventDefault();
424
+ try {
425
+ this.controller?.openWallet() ?? openWallet({ invoice, logger: options.logger });
426
+ this.dispatchEvent(createCheckoutActionEvent(OPENRECEIVE_CHECKOUT_ELEMENT_EVENTS.openWallet));
427
+ }
428
+ catch (error) {
429
+ this.dispatchError(error);
430
+ }
431
+ });
432
+ root.querySelector(OPENRECEIVE_CHECKOUT_ELEMENT_PART_SELECTORS.startOver)?.addEventListener("click", () => {
433
+ this.dispatchEvent(createCheckoutActionEvent(OPENRECEIVE_CHECKOUT_ELEMENT_EVENTS.startOver));
434
+ });
435
+ const qrTarget = root.querySelector(OPENRECEIVE_CHECKOUT_DATA_SELECTORS.qr);
436
+ if (qrTarget !== null) {
437
+ void createQrSvg(invoice, {
438
+ encoder: options.qrEncoder,
439
+ width: 256
440
+ })
441
+ .then((svg) => {
442
+ const qr = root.querySelector(OPENRECEIVE_CHECKOUT_DATA_SELECTORS.qr);
443
+ if (qr !== null)
444
+ qr.innerHTML = svg;
445
+ })
446
+ .catch((error) => this.dispatchError(error));
447
+ }
448
+ this.bindWizard(root, invoice, options.logger);
449
+ }
450
+ startCheckoutController() {
451
+ const snapshot = this.currentCheckoutSnapshot();
452
+ const lookupUrl = this.getAttribute(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.lookupUrl);
453
+ if (snapshot === undefined) {
454
+ this.stopCheckoutController();
455
+ return;
456
+ }
457
+ this.stopCheckoutController();
458
+ this.controller = createCheckoutController({
459
+ snapshot,
460
+ ...(lookupUrl === null
461
+ ? {}
462
+ : { lookupUrl }),
463
+ logger: options.logger,
464
+ onError: (error) => this.dispatchError(error),
465
+ onState: (nextState) => this.applyCheckoutState(nextState)
466
+ });
467
+ this.controller.start();
468
+ }
469
+ stopCheckoutController() {
470
+ this.controller?.stop();
471
+ this.controller = undefined;
472
+ }
473
+ currentCheckoutSnapshot() {
474
+ const invoiceId = this.getAttribute(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.invoiceId);
475
+ const invoice = this.getAttribute(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.invoice);
476
+ if (invoiceId === null || invoice === null)
477
+ return undefined;
478
+ const amountMsats = parseOpenReceiveOptionalInteger(this.getAttribute(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.amountMsats), { label: "amount-msats" });
479
+ const expiresAt = parseOpenReceiveOptionalInteger(this.getAttribute(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.expiresAt), { label: "expires-at" });
480
+ return createCheckoutSnapshotFromDisplayData({
481
+ invoice_id: invoiceId,
482
+ invoice,
483
+ ...(this.getAttribute(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.paymentHash) === null
484
+ ? {}
485
+ : { payment_hash: this.getAttribute(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.paymentHash) ?? undefined }),
486
+ ...(amountMsats === undefined ? {} : { amount_msats: amountMsats }),
487
+ ...(readElementFiatQuote(this) === undefined ? {} : { fiat_quote: readElementFiatQuote(this) }),
488
+ transaction_state: transactionStateFromStatus(parseElementStatus(this.getAttribute(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.status)) ?? "pending"),
489
+ ...(expiresAt === undefined ? {} : { expires_at: expiresAt }),
490
+ });
491
+ }
492
+ applyCheckoutState(state) {
493
+ this.setAttributeIfChanged(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.status, deriveStatus(state));
494
+ if (state.expires_at !== undefined) {
495
+ this.setAttributeIfChanged(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.expiresAt, String(state.expires_at));
496
+ }
497
+ this.render();
498
+ this.dispatchEvent(createCheckoutStateEvent(OPENRECEIVE_CHECKOUT_ELEMENT_EVENTS.state, state));
499
+ if (state.settled && state.payment_hash !== this.announcedSettledPaymentHash) {
500
+ this.announcedSettledPaymentHash = state.payment_hash;
501
+ this.dispatchEvent(createCheckoutStateEvent(OPENRECEIVE_CHECKOUT_ELEMENT_EVENTS.settled, state));
502
+ }
503
+ }
504
+ setAttributeIfChanged(name, value) {
505
+ if (this.getAttribute(name) !== value)
506
+ this.setAttribute(name, value);
507
+ }
508
+ bindWizard(root, invoice, logger) {
509
+ root.querySelectorAll(OPENRECEIVE_PAYMENT_WIZARD_SELECTORS.method).forEach((button) => {
510
+ button.addEventListener("click", () => {
511
+ const method = parseOpenReceivePaymentMethod(button.getAttribute(OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.method));
512
+ if (method === null)
513
+ return;
514
+ this.selection = updateOpenReceivePaymentWizardSelection(this.selection, {
515
+ type: "select_method",
516
+ method,
517
+ storedCountryCode: method === "card" || method === "bank"
518
+ ? readOpenReceiveStoredCountryCode()
519
+ : undefined
520
+ });
521
+ this.render();
522
+ });
523
+ });
524
+ root.querySelectorAll(OPENRECEIVE_PAYMENT_WIZARD_SELECTORS.breadcrumb).forEach((button) => {
525
+ button.addEventListener("click", () => {
526
+ const target = button.getAttribute(OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.breadcrumb);
527
+ if (target === "method") {
528
+ this.selection = updateOpenReceivePaymentWizardSelection(this.selection, {
529
+ type: "change_method"
530
+ });
531
+ this.render();
532
+ return;
533
+ }
534
+ if (target === "route") {
535
+ this.selection = updateOpenReceivePaymentWizardSelection(this.selection, {
536
+ type: "change_route"
537
+ });
538
+ this.render();
539
+ }
540
+ });
541
+ });
542
+ root.querySelectorAll(OPENRECEIVE_PAYMENT_WIZARD_SELECTORS.region).forEach((button) => {
543
+ button.addEventListener("click", () => {
544
+ const region = parseOpenReceiveRegion(button.getAttribute(OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.region));
545
+ if (region === null)
546
+ return;
547
+ this.selection = updateOpenReceivePaymentWizardSelection(this.selection, {
548
+ type: "select_region",
549
+ region
550
+ });
551
+ this.render();
552
+ });
553
+ });
554
+ root.querySelectorAll(OPENRECEIVE_PAYMENT_WIZARD_SELECTORS.country).forEach((button) => {
555
+ const selectCountry = () => {
556
+ const countryCode = button instanceof HTMLSelectElement
557
+ ? button.value
558
+ : button.getAttribute(OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.country);
559
+ if (countryCode === null)
560
+ return;
561
+ this.selection = updateOpenReceivePaymentWizardSelection(this.selection, {
562
+ type: "select_country",
563
+ countryCode
564
+ });
565
+ writeOpenReceiveStoredCountryCode(countryCode, {
566
+ storageKey: OPENRECEIVE_COUNTRY_STORAGE_KEY
567
+ });
568
+ this.render();
569
+ };
570
+ if (button instanceof HTMLSelectElement) {
571
+ button.addEventListener("change", selectCountry);
572
+ return;
573
+ }
574
+ button.addEventListener("click", selectCountry);
575
+ button.addEventListener("keydown", (event) => {
576
+ if (!(event instanceof KeyboardEvent))
577
+ return;
578
+ if (event.key !== "Enter" && event.key !== " ")
579
+ return;
580
+ event.preventDefault();
581
+ selectCountry();
582
+ });
583
+ });
584
+ root.querySelector(OPENRECEIVE_PAYMENT_WIZARD_SELECTORS.switchCountry)?.addEventListener("click", () => {
585
+ this.selection = updateOpenReceivePaymentWizardSelection(this.selection, {
586
+ type: "open_country_picker"
587
+ });
588
+ this.render();
589
+ });
590
+ root.querySelectorAll(OPENRECEIVE_PAYMENT_WIZARD_SELECTORS.route).forEach((button) => {
591
+ button.addEventListener("click", () => {
592
+ const route = button.getAttribute(OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.route);
593
+ if (route === null)
594
+ return;
595
+ this.selection = updateOpenReceivePaymentWizardSelection(this.selection, {
596
+ type: "select_route",
597
+ route
598
+ });
599
+ this.render();
600
+ });
601
+ });
602
+ root.querySelectorAll(OPENRECEIVE_PAYMENT_WIZARD_SELECTORS.providerTutorial).forEach((button) => {
603
+ button.addEventListener("click", () => {
604
+ const providerId = button.getAttribute(OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.providerTutorial);
605
+ if (providerId === null)
606
+ return;
607
+ if (providerId === "") {
608
+ this.activeTutorialProviderId = null;
609
+ this.activeTutorialIndex = 0;
610
+ this.activeTutorialCopied = false;
611
+ this.render();
612
+ return;
613
+ }
614
+ const index = Number(button.getAttribute(OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES.providerTutorialIndex) ?? "0");
615
+ if (this.activeTutorialProviderId !== providerId) {
616
+ this.activeTutorialCopied = false;
617
+ }
618
+ this.activeTutorialProviderId = providerId;
619
+ this.activeTutorialIndex = Number.isSafeInteger(index) && index >= 0 ? index : 0;
620
+ this.render();
621
+ });
622
+ });
623
+ root.querySelector('[part="tutorial-copy"]')?.addEventListener("click", () => {
624
+ void copyInvoice({ invoice, logger })
625
+ .then(() => {
626
+ this.activeTutorialCopied = true;
627
+ if (this.activeTutorialProviderId !== null) {
628
+ this.dispatchEvent(createCheckoutProviderCopyEvent(this.activeTutorialProviderId));
629
+ }
630
+ this.dispatchEvent(createCheckoutActionEvent(OPENRECEIVE_CHECKOUT_ELEMENT_EVENTS.copy));
631
+ this.render();
632
+ })
633
+ .catch((error) => this.dispatchError(error));
634
+ });
635
+ const tutorial = root.querySelector('[part="tutorial"]');
636
+ tutorial?.addEventListener("click", (event) => {
637
+ if (event.target !== event.currentTarget)
638
+ return;
639
+ this.activeTutorialProviderId = null;
640
+ this.activeTutorialIndex = 0;
641
+ this.activeTutorialCopied = false;
642
+ this.render();
643
+ });
644
+ tutorial?.addEventListener("keydown", (event) => {
645
+ if (!(event instanceof KeyboardEvent))
646
+ return;
647
+ if (event.key !== "Escape" || this.activeTutorialProviderId === null)
648
+ return;
649
+ this.activeTutorialProviderId = null;
650
+ this.activeTutorialIndex = 0;
651
+ this.activeTutorialCopied = false;
652
+ this.render();
653
+ });
654
+ if (tutorial instanceof HTMLElement)
655
+ tutorial.focus();
656
+ }
657
+ dispatchError(error) {
658
+ this.dispatchEvent(createCheckoutErrorEvent(error));
659
+ }
660
+ }
661
+ class OpenReceiveThemeToggleElement extends HTMLElementCtor {
662
+ observer;
663
+ static get observedAttributes() {
664
+ return [
665
+ OPENRECEIVE_THEME_TOGGLE_ELEMENT_ATTRIBUTES.rootSelector,
666
+ OPENRECEIVE_THEME_TOGGLE_ELEMENT_ATTRIBUTES.checkoutSelector,
667
+ OPENRECEIVE_THEME_TOGGLE_ELEMENT_ATTRIBUTES.defaultTheme,
668
+ OPENRECEIVE_THEME_TOGGLE_ELEMENT_ATTRIBUTES.storageKey
669
+ ];
670
+ }
671
+ connectedCallback() {
672
+ this.render();
673
+ this.startObserver();
674
+ this.syncTheme();
675
+ }
676
+ attributeChangedCallback() {
677
+ if (!this.isConnected)
678
+ return;
679
+ this.render();
680
+ this.syncTheme();
681
+ }
682
+ disconnectedCallback() {
683
+ this.observer?.disconnect();
684
+ this.observer = undefined;
685
+ }
686
+ render() {
687
+ const root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
688
+ const theme = this.syncTheme();
689
+ root.innerHTML = renderOpenReceiveThemeToggleHtml(theme.toggleLabel, theme.resolvedTheme);
690
+ root.querySelector(OPENRECEIVE_THEME_TOGGLE_ELEMENT_PART_SELECTORS.button)?.addEventListener("click", () => {
691
+ const nextTheme = toggleOpenReceiveStoredThemeControls(this.themeTargets(), this.themeOptions());
692
+ this.setAttributeIfChanged(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.theme, nextTheme.resolvedTheme);
693
+ this.dispatchEvent(createOpenReceiveThemeChangeEvent(nextTheme));
694
+ this.render();
695
+ });
696
+ }
697
+ syncTheme() {
698
+ const theme = syncOpenReceiveStoredThemeControls(this.themeTargets(), this.themeOptions());
699
+ this.setAttributeIfChanged(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.theme, theme.resolvedTheme);
700
+ return theme;
701
+ }
702
+ startObserver() {
703
+ if (this.observer !== undefined)
704
+ return;
705
+ const MutationObserverCtor = globalThis.MutationObserver;
706
+ if (MutationObserverCtor === undefined || this.ownerDocument.body === null)
707
+ return;
708
+ this.observer = new MutationObserverCtor(() => {
709
+ this.syncTheme();
710
+ });
711
+ this.observer.observe(this.ownerDocument.body, {
712
+ childList: true,
713
+ subtree: true
714
+ });
715
+ }
716
+ themeTargets() {
717
+ const rootSelector = this.getAttribute(OPENRECEIVE_THEME_TOGGLE_ELEMENT_ATTRIBUTES.rootSelector);
718
+ const checkoutSelector = this.getAttribute(OPENRECEIVE_THEME_TOGGLE_ELEMENT_ATTRIBUTES.checkoutSelector);
719
+ const button = this.shadowRoot?.querySelector(OPENRECEIVE_THEME_TOGGLE_ELEMENT_PART_SELECTORS.button) ??
720
+ null;
721
+ return {
722
+ root: rootSelector === null ? undefined : this.ownerDocument.querySelector(rootSelector),
723
+ checkout: checkoutSelector === null ? undefined : this.ownerDocument.querySelector(checkoutSelector),
724
+ toggle: button
725
+ };
726
+ }
727
+ themeOptions() {
728
+ const defaultTheme = parseOpenReceiveThemePreference(this.getAttribute(OPENRECEIVE_THEME_TOGGLE_ELEMENT_ATTRIBUTES.defaultTheme));
729
+ return {
730
+ ...(defaultTheme === undefined ? {} : { defaultTheme }),
731
+ ...(this.getAttribute(OPENRECEIVE_THEME_TOGGLE_ELEMENT_ATTRIBUTES.storageKey) === null
732
+ ? {}
733
+ : {
734
+ storageKey: this.getAttribute(OPENRECEIVE_THEME_TOGGLE_ELEMENT_ATTRIBUTES.storageKey) ??
735
+ undefined
736
+ })
737
+ };
738
+ }
739
+ setAttributeIfChanged(name, value) {
740
+ if (this.getAttribute(name) !== value)
741
+ this.setAttribute(name, value);
742
+ }
743
+ }
744
+ if (registry.get(tagName) === undefined) {
745
+ registry.define(tagName, CheckoutElement);
746
+ }
747
+ if (registry.get(themeToggleTagName) === undefined) {
748
+ registry.define(themeToggleTagName, OpenReceiveThemeToggleElement);
749
+ }
750
+ }
751
+ export const formatMsats = formatOpenReceiveMsats;
752
+ const elementCopyFeedbackControllers = new WeakMap();
753
+ const escapeHtml = escapeOpenReceiveHtml;
754
+ function readElementFiatQuote(element) {
755
+ const currency = element.getAttribute(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.fiatCurrency);
756
+ const value = element.getAttribute(OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES.fiatValue);
757
+ if (currency === null || value === null)
758
+ return undefined;
759
+ return {
760
+ fiat: {
761
+ currency,
762
+ value
763
+ }
764
+ };
765
+ }
766
+ function showElementCopyFeedback(button) {
767
+ if (button === null)
768
+ return;
769
+ let controller = elementCopyFeedbackControllers.get(button);
770
+ if (controller === undefined) {
771
+ controller = createOpenReceiveTransientFeedbackController({
772
+ resetValue: openReceiveCheckoutLabels.copyInvoice,
773
+ onValue: (label) => {
774
+ if (button.isConnected)
775
+ button.textContent = label;
776
+ }
777
+ });
778
+ elementCopyFeedbackControllers.set(button, controller);
779
+ }
780
+ controller.show(openReceiveCheckoutLabels.copied);
781
+ }
@@ -0,0 +1 @@
1
+ @import "@openreceive/browser/styles.css";
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@openreceive/elements",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "dependencies": {
6
+ "@openreceive/browser": "0.1.0"
7
+ },
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./styles.css": "./dist/styles.css"
11
+ },
12
+ "types": "./dist/index.d.ts",
13
+ "files": [
14
+ "dist",
15
+ "README.md",
16
+ "LICENSE"
17
+ ]
18
+ }