@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 +21 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +781 -0
- package/dist/styles.css +1 -0
- package/package.json +18 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/styles.css
ADDED
|
@@ -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
|
+
}
|