@openreceive/react 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 +208 -0
- package/dist/index.js +919 -0
- package/dist/styles.css +1 -0
- package/package.json +21 -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,208 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type OpenReceiveQrEncoder, type OpenReceiveBrowserLogContext, type OpenReceiveBrowserLogger, type CheckoutPhase, type CheckoutSnapshot, type CheckoutState, type CheckoutStatusModel, type OpenReceiveRefreshInvoiceResult, type OpenReceiveResolvedTheme, type OpenReceiveThemeModel, type OpenReceiveThemePreference, type Status } from "@openreceive/browser/internal";
|
|
3
|
+
export interface CheckoutData {
|
|
4
|
+
readonly invoice: CheckoutSnapshot;
|
|
5
|
+
}
|
|
6
|
+
export interface CheckoutViewModel {
|
|
7
|
+
readonly invoice_id?: string;
|
|
8
|
+
readonly invoice: string;
|
|
9
|
+
readonly payment_hash?: string;
|
|
10
|
+
readonly amount_msats?: number;
|
|
11
|
+
readonly fiat_quote?: CheckoutSnapshot["fiat_quote"];
|
|
12
|
+
readonly expires_at?: number;
|
|
13
|
+
readonly settled_at?: number;
|
|
14
|
+
readonly lightningUri: string;
|
|
15
|
+
readonly amountLabel?: string;
|
|
16
|
+
readonly fiatLabel?: string;
|
|
17
|
+
readonly paymentHashLabel?: string;
|
|
18
|
+
readonly status: Status;
|
|
19
|
+
}
|
|
20
|
+
export interface UseCheckoutOptions extends CheckoutData {
|
|
21
|
+
readonly clipboard?: Pick<Clipboard, "writeText">;
|
|
22
|
+
readonly open?: (uri: string) => void;
|
|
23
|
+
readonly logger?: OpenReceiveBrowserLogger;
|
|
24
|
+
readonly onError?: (error: unknown) => void;
|
|
25
|
+
readonly lookupInvoice?: (state: CheckoutState) => Promise<Partial<CheckoutSnapshot>>;
|
|
26
|
+
readonly lookupUrl?: string | false;
|
|
27
|
+
readonly refreshInvoice?: (state: CheckoutState) => Promise<CheckoutSnapshot | OpenReceiveRefreshInvoiceResult>;
|
|
28
|
+
readonly refreshUrl?: string | ((state: CheckoutState) => string);
|
|
29
|
+
readonly refreshHeaders?: Readonly<Record<string, string>>;
|
|
30
|
+
readonly refreshIdempotencyKey?: string | ((state: CheckoutState) => string);
|
|
31
|
+
readonly refreshReason?: string | ((state: CheckoutState) => string);
|
|
32
|
+
readonly onState?: (state: CheckoutState) => void;
|
|
33
|
+
readonly onPaid?: () => void;
|
|
34
|
+
readonly polling?: boolean;
|
|
35
|
+
readonly pollIntervalMs?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface UseCheckoutResult extends CheckoutViewModel {
|
|
38
|
+
readonly copied: boolean;
|
|
39
|
+
readonly expiresInSeconds?: number;
|
|
40
|
+
readonly countdownLabel?: string;
|
|
41
|
+
readonly countdownPrefix?: string;
|
|
42
|
+
readonly statusTitle: string;
|
|
43
|
+
readonly statusDetail: string;
|
|
44
|
+
readonly waiting: boolean;
|
|
45
|
+
reloadState(): Promise<void>;
|
|
46
|
+
retry(): Promise<void>;
|
|
47
|
+
refreshExpiredInvoice(): Promise<void>;
|
|
48
|
+
cancel(): void;
|
|
49
|
+
copyInvoice(): Promise<void>;
|
|
50
|
+
openWallet(): string;
|
|
51
|
+
}
|
|
52
|
+
export interface CheckoutProviderProps extends UseCheckoutOptions {
|
|
53
|
+
readonly children?: React.ReactNode | ((checkout: UseCheckoutResult) => React.ReactNode);
|
|
54
|
+
}
|
|
55
|
+
export interface QRCodeProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
|
|
56
|
+
readonly invoice: string;
|
|
57
|
+
readonly encoder?: OpenReceiveQrEncoder;
|
|
58
|
+
readonly width?: number;
|
|
59
|
+
readonly onError?: (error: unknown) => void;
|
|
60
|
+
}
|
|
61
|
+
export type ButtonComponent = React.ElementType<React.ButtonHTMLAttributes<HTMLButtonElement>>;
|
|
62
|
+
export interface CopyInvoiceButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
63
|
+
readonly invoice: string;
|
|
64
|
+
readonly copyInvoice?: () => Promise<void>;
|
|
65
|
+
readonly clipboard?: Pick<Clipboard, "writeText">;
|
|
66
|
+
readonly logger?: OpenReceiveBrowserLogger;
|
|
67
|
+
readonly onCopied?: () => void;
|
|
68
|
+
readonly onError?: (error: unknown) => void;
|
|
69
|
+
readonly copiedLabel?: React.ReactNode;
|
|
70
|
+
readonly ButtonComponent?: ButtonComponent;
|
|
71
|
+
}
|
|
72
|
+
export interface OpenWalletButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
73
|
+
readonly invoice: string;
|
|
74
|
+
readonly openWallet?: () => string;
|
|
75
|
+
readonly open?: (uri: string) => void;
|
|
76
|
+
readonly logger?: OpenReceiveBrowserLogger;
|
|
77
|
+
readonly onOpened?: (uri: string) => void;
|
|
78
|
+
readonly onError?: (error: unknown) => void;
|
|
79
|
+
readonly ButtonComponent?: ButtonComponent;
|
|
80
|
+
}
|
|
81
|
+
export interface PaymentStateProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
82
|
+
readonly state?: string;
|
|
83
|
+
}
|
|
84
|
+
export interface InvoiceSummaryClassNames {
|
|
85
|
+
readonly amount?: string;
|
|
86
|
+
readonly fiat?: string;
|
|
87
|
+
readonly paymentHash?: string;
|
|
88
|
+
readonly paymentState?: string;
|
|
89
|
+
}
|
|
90
|
+
export interface InvoiceSummaryProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
91
|
+
readonly amountLabel?: string;
|
|
92
|
+
readonly fiatLabel?: string;
|
|
93
|
+
readonly paymentHashLabel?: string;
|
|
94
|
+
readonly status?: Status;
|
|
95
|
+
readonly PaymentStateComponent?: React.ComponentType<PaymentStateProps>;
|
|
96
|
+
readonly classNames?: InvoiceSummaryClassNames;
|
|
97
|
+
}
|
|
98
|
+
export interface CheckoutClassNames extends InvoiceSummaryClassNames {
|
|
99
|
+
readonly root?: string;
|
|
100
|
+
readonly qr?: string;
|
|
101
|
+
readonly details?: string;
|
|
102
|
+
readonly waiting?: string;
|
|
103
|
+
readonly countdown?: string;
|
|
104
|
+
readonly summary?: string;
|
|
105
|
+
readonly invoice?: string;
|
|
106
|
+
readonly actions?: string;
|
|
107
|
+
readonly copyButton?: string;
|
|
108
|
+
readonly openWalletButton?: string;
|
|
109
|
+
readonly wizard?: string;
|
|
110
|
+
readonly themeToggle?: string;
|
|
111
|
+
}
|
|
112
|
+
export interface CheckoutComponents {
|
|
113
|
+
readonly Button?: ButtonComponent;
|
|
114
|
+
readonly QRCode?: React.ComponentType<QRCodeProps>;
|
|
115
|
+
readonly InvoiceSummary?: React.ComponentType<InvoiceSummaryProps>;
|
|
116
|
+
readonly CopyButton?: React.ComponentType<CopyInvoiceButtonProps>;
|
|
117
|
+
readonly OpenWalletButton?: React.ComponentType<OpenWalletButtonProps>;
|
|
118
|
+
readonly PaymentState?: React.ComponentType<PaymentStateProps>;
|
|
119
|
+
}
|
|
120
|
+
export type CheckoutChildren = React.ReactNode | ((model: UseCheckoutResult) => React.ReactNode);
|
|
121
|
+
export interface CheckoutProps extends CheckoutData, Omit<React.HTMLAttributes<HTMLElement>, "children"> {
|
|
122
|
+
readonly qrEncoder?: OpenReceiveQrEncoder;
|
|
123
|
+
readonly logger?: OpenReceiveBrowserLogger;
|
|
124
|
+
readonly onError?: (error: unknown) => void;
|
|
125
|
+
readonly lookupInvoice?: (state: CheckoutState) => Promise<Partial<CheckoutSnapshot>>;
|
|
126
|
+
readonly lookupUrl?: string | false;
|
|
127
|
+
readonly refreshInvoice?: (state: CheckoutState) => Promise<CheckoutSnapshot | OpenReceiveRefreshInvoiceResult>;
|
|
128
|
+
readonly refreshUrl?: string | ((state: CheckoutState) => string);
|
|
129
|
+
readonly refreshHeaders?: Readonly<Record<string, string>>;
|
|
130
|
+
readonly refreshIdempotencyKey?: string | ((state: CheckoutState) => string);
|
|
131
|
+
readonly refreshReason?: string | ((state: CheckoutState) => string);
|
|
132
|
+
readonly onState?: (state: CheckoutState) => void;
|
|
133
|
+
readonly onPaid?: () => void;
|
|
134
|
+
readonly onStartOver?: () => void;
|
|
135
|
+
readonly polling?: boolean;
|
|
136
|
+
readonly paymentWizard?: boolean;
|
|
137
|
+
readonly themeSwitcher?: boolean;
|
|
138
|
+
readonly defaultTheme?: OpenReceiveThemePreference;
|
|
139
|
+
readonly themeStorageKey?: string;
|
|
140
|
+
readonly countryStorageKey?: string;
|
|
141
|
+
readonly components?: CheckoutComponents;
|
|
142
|
+
readonly classNames?: CheckoutClassNames;
|
|
143
|
+
readonly children?: CheckoutChildren;
|
|
144
|
+
}
|
|
145
|
+
export interface UseThemeOptions {
|
|
146
|
+
readonly defaultTheme?: OpenReceiveThemePreference;
|
|
147
|
+
readonly storageKey?: string;
|
|
148
|
+
readonly storage?: Storage;
|
|
149
|
+
}
|
|
150
|
+
export interface UseThemeResult {
|
|
151
|
+
readonly theme: OpenReceiveThemePreference;
|
|
152
|
+
readonly resolvedTheme: OpenReceiveResolvedTheme;
|
|
153
|
+
readonly model: OpenReceiveThemeModel;
|
|
154
|
+
readonly nextTheme: OpenReceiveThemePreference;
|
|
155
|
+
readonly toggleLabel: string;
|
|
156
|
+
readonly attributes: OpenReceiveThemeModel["attributes"];
|
|
157
|
+
readonly checkoutElementAttributes: OpenReceiveThemeModel["checkoutElementAttributes"];
|
|
158
|
+
setTheme(theme: OpenReceiveThemePreference): void;
|
|
159
|
+
toggleTheme(): void;
|
|
160
|
+
}
|
|
161
|
+
export interface ThemeToggleProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
162
|
+
readonly theme?: OpenReceiveThemePreference;
|
|
163
|
+
readonly resolvedTheme?: OpenReceiveResolvedTheme;
|
|
164
|
+
readonly onThemeChange?: (theme: OpenReceiveThemePreference) => void;
|
|
165
|
+
readonly ButtonComponent?: ButtonComponent;
|
|
166
|
+
}
|
|
167
|
+
export type ThemeScopeChildren = React.ReactNode | ((theme: UseThemeResult) => React.ReactNode);
|
|
168
|
+
export interface ThemeScopeProps extends Omit<React.HTMLAttributes<HTMLElement>, "children"> {
|
|
169
|
+
readonly as?: keyof React.JSX.IntrinsicElements;
|
|
170
|
+
readonly defaultTheme?: OpenReceiveThemePreference;
|
|
171
|
+
readonly themeStorageKey?: string;
|
|
172
|
+
readonly storage?: Storage;
|
|
173
|
+
readonly themeToggle?: boolean;
|
|
174
|
+
readonly topbarClassName?: string;
|
|
175
|
+
readonly themeToggleClassName?: string;
|
|
176
|
+
readonly ButtonComponent?: ButtonComponent;
|
|
177
|
+
readonly children?: ThemeScopeChildren;
|
|
178
|
+
}
|
|
179
|
+
export interface PaymentWizardProps {
|
|
180
|
+
readonly invoice: string;
|
|
181
|
+
readonly className?: string;
|
|
182
|
+
readonly logger?: OpenReceiveBrowserLogger;
|
|
183
|
+
readonly logContext?: OpenReceiveBrowserLogContext;
|
|
184
|
+
readonly countryStorageKey?: string;
|
|
185
|
+
readonly onError?: (error: unknown) => void;
|
|
186
|
+
}
|
|
187
|
+
export declare function createCheckoutViewModel(data: CheckoutData): CheckoutViewModel;
|
|
188
|
+
export declare function useCheckout(options: UseCheckoutOptions): UseCheckoutResult;
|
|
189
|
+
export declare function useCheckoutContext(): UseCheckoutResult;
|
|
190
|
+
export declare function CheckoutProvider(props: CheckoutProviderProps): React.ReactElement;
|
|
191
|
+
export declare function QRCode(props: QRCodeProps): React.ReactElement;
|
|
192
|
+
export declare function CopyInvoiceButton(props: CopyInvoiceButtonProps): React.ReactElement;
|
|
193
|
+
export declare function OpenWalletButton(props: OpenWalletButtonProps): React.ReactElement;
|
|
194
|
+
export declare function PaymentState(props: PaymentStateProps): React.ReactElement;
|
|
195
|
+
export declare function useTheme(options?: UseThemeOptions): UseThemeResult;
|
|
196
|
+
export declare function ThemeToggle(props: ThemeToggleProps): React.ReactElement;
|
|
197
|
+
export declare function ThemeScope(props: ThemeScopeProps): React.ReactElement;
|
|
198
|
+
export declare function WaitingState(props: {
|
|
199
|
+
readonly waiting?: boolean;
|
|
200
|
+
readonly phase?: CheckoutPhase;
|
|
201
|
+
readonly status?: CheckoutStatusModel;
|
|
202
|
+
readonly statusTitle?: string;
|
|
203
|
+
readonly statusDetail?: string;
|
|
204
|
+
readonly className?: string;
|
|
205
|
+
}): React.ReactElement;
|
|
206
|
+
export declare function PaymentWizard(props: PaymentWizardProps): React.ReactElement;
|
|
207
|
+
export declare function InvoiceSummary(props: InvoiceSummaryProps): React.ReactElement;
|
|
208
|
+
export declare function Checkout(props: CheckoutProps): React.ReactElement;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { OPENRECEIVE_COPY_FEEDBACK_MS, OPENRECEIVE_COUNTRY_STORAGE_KEY, OPENRECEIVE_CHECKOUT_DATA_ATTRIBUTES, OPENRECEIVE_THEME_STORAGE_KEY, copyInvoice as copyInvoiceHelper, createCheckoutController, createCheckoutDisplayModel, createCheckoutSnapshotFromDisplayData, createCheckoutStatusModel, createCheckoutStateFromDisplayData, createOpenReceivePaymentWizardModel, createOpenReceivePaymentWizardController, createCheckoutProviderCopyEvent, createOpenReceiveWizardRouteAssetDisplays, createOpenReceiveWizardRouteDisplays, createQrSvg, createOpenReceiveThemeModel, createOpenReceiveTransientFeedbackController, getOpenReceiveDefaultCountryCode, getOpenReceivePaymentMethodIcon, getOpenReceiveWizardEmptyMessage, openReceiveCheckoutLabels, openReceivePaymentMethods, openWallet as openWalletHelper, readOpenReceiveThemePreference, status as deriveStatus, writeOpenReceiveThemePreference } from "@openreceive/browser/internal";
|
|
3
|
+
const DEFAULT_LOOKUP_URL = "/openreceive/v1/invoices/lookup";
|
|
4
|
+
function useOpenReceiveTransientValue(resetValue, delayMs = OPENRECEIVE_COPY_FEEDBACK_MS) {
|
|
5
|
+
const [value, setValue] = React.useState(resetValue);
|
|
6
|
+
const controller = React.useRef(null);
|
|
7
|
+
React.useEffect(() => {
|
|
8
|
+
controller.current?.clear();
|
|
9
|
+
controller.current = createOpenReceiveTransientFeedbackController({
|
|
10
|
+
resetValue,
|
|
11
|
+
delayMs,
|
|
12
|
+
onValue: setValue
|
|
13
|
+
});
|
|
14
|
+
return () => controller.current?.clear();
|
|
15
|
+
}, [resetValue, delayMs]);
|
|
16
|
+
const showValue = React.useCallback((nextValue) => {
|
|
17
|
+
if (controller.current === null) {
|
|
18
|
+
setValue(nextValue);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
controller.current.show(nextValue);
|
|
22
|
+
}, []);
|
|
23
|
+
return [value, showValue];
|
|
24
|
+
}
|
|
25
|
+
function toCheckoutDisplayData(invoice) {
|
|
26
|
+
return {
|
|
27
|
+
invoice_id: invoice.invoice_id,
|
|
28
|
+
invoice: invoice.invoice,
|
|
29
|
+
...(invoice.payment_hash === undefined ? {} : { payment_hash: invoice.payment_hash }),
|
|
30
|
+
...(invoice.amount_msats === undefined ? {} : { amount_msats: invoice.amount_msats }),
|
|
31
|
+
...(invoice.fiat_quote === undefined ? {} : { fiat_quote: invoice.fiat_quote }),
|
|
32
|
+
...(invoice.transaction_state === undefined
|
|
33
|
+
? {}
|
|
34
|
+
: { transaction_state: invoice.transaction_state }),
|
|
35
|
+
...(invoice.workflow_state === undefined ? {} : { workflow_state: invoice.workflow_state }),
|
|
36
|
+
...(invoice.expires_at === undefined ? {} : { expires_at: invoice.expires_at }),
|
|
37
|
+
...(invoice.settled_at === undefined ? {} : { settled_at: invoice.settled_at })
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function toCheckoutViewModel(display, currentStatus) {
|
|
41
|
+
return {
|
|
42
|
+
invoice_id: display.invoice_id,
|
|
43
|
+
invoice: display.invoice,
|
|
44
|
+
...(display.payment_hash === undefined ? {} : { payment_hash: display.payment_hash }),
|
|
45
|
+
...(display.amount_msats === undefined ? {} : { amount_msats: display.amount_msats }),
|
|
46
|
+
...(display.fiat_quote === undefined ? {} : { fiat_quote: display.fiat_quote }),
|
|
47
|
+
...(display.expires_at === undefined ? {} : { expires_at: display.expires_at }),
|
|
48
|
+
...(display.settled_at === undefined ? {} : { settled_at: display.settled_at }),
|
|
49
|
+
lightningUri: display.lightningUri,
|
|
50
|
+
...(display.amountLabel === undefined ? {} : { amountLabel: display.amountLabel }),
|
|
51
|
+
...(display.fiatLabel === undefined ? {} : { fiatLabel: display.fiatLabel }),
|
|
52
|
+
...(display.paymentHashLabel === undefined ? {} : { paymentHashLabel: display.paymentHashLabel }),
|
|
53
|
+
status: currentStatus
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function createCheckoutViewModel(data) {
|
|
57
|
+
return toCheckoutViewModel(createCheckoutDisplayModel(toCheckoutDisplayData(data.invoice)), deriveStatus(data.invoice));
|
|
58
|
+
}
|
|
59
|
+
function resolveCheckoutLookupUrl(options) {
|
|
60
|
+
if (options.polling === false || options.lookupUrl === false)
|
|
61
|
+
return undefined;
|
|
62
|
+
return options.lookupUrl ?? DEFAULT_LOOKUP_URL;
|
|
63
|
+
}
|
|
64
|
+
export function useCheckout(options) {
|
|
65
|
+
const [copied, showCopied] = useOpenReceiveTransientValue(false);
|
|
66
|
+
const displayData = React.useMemo(() => toCheckoutDisplayData(options.invoice), [options.invoice]);
|
|
67
|
+
const model = React.useMemo(() => toCheckoutViewModel(createCheckoutDisplayModel(displayData), deriveStatus(options.invoice)), [
|
|
68
|
+
displayData,
|
|
69
|
+
options.invoice
|
|
70
|
+
]);
|
|
71
|
+
const snapshot = React.useMemo(() => createCheckoutSnapshotFromDisplayData(displayData), [
|
|
72
|
+
displayData
|
|
73
|
+
]);
|
|
74
|
+
const [state, setState] = React.useState(() => createCheckoutStateFromDisplayData(displayData, {
|
|
75
|
+
logger: options.logger
|
|
76
|
+
}));
|
|
77
|
+
const controllerRef = React.useRef(null);
|
|
78
|
+
const onStateRef = React.useRef(options.onState);
|
|
79
|
+
onStateRef.current = options.onState;
|
|
80
|
+
const onPaidRef = React.useRef(options.onPaid);
|
|
81
|
+
onPaidRef.current = options.onPaid;
|
|
82
|
+
const paidAnnouncementRef = React.useRef({
|
|
83
|
+
invoiceId: snapshot.invoice_id,
|
|
84
|
+
fired: false
|
|
85
|
+
});
|
|
86
|
+
const logContext = React.useMemo(() => getCheckoutLogContext(displayData), [
|
|
87
|
+
displayData
|
|
88
|
+
]);
|
|
89
|
+
React.useEffect(() => {
|
|
90
|
+
const lookupInvoice = options.polling === false ? undefined : options.lookupInvoice;
|
|
91
|
+
const lookupUrl = resolveCheckoutLookupUrl(options);
|
|
92
|
+
const controller = createCheckoutController({
|
|
93
|
+
snapshot,
|
|
94
|
+
...(lookupInvoice === undefined ? {} : { lookupInvoice }),
|
|
95
|
+
...(lookupUrl === undefined ? {} : { lookupUrl }),
|
|
96
|
+
...(options.refreshInvoice === undefined ? {} : { refreshInvoice: options.refreshInvoice }),
|
|
97
|
+
...(options.refreshUrl === undefined ? {} : { refreshUrl: options.refreshUrl }),
|
|
98
|
+
...(options.refreshHeaders === undefined ? {} : { refreshHeaders: options.refreshHeaders }),
|
|
99
|
+
...(options.refreshIdempotencyKey === undefined
|
|
100
|
+
? {}
|
|
101
|
+
: { refreshIdempotencyKey: options.refreshIdempotencyKey }),
|
|
102
|
+
...(options.refreshReason === undefined ? {} : { refreshReason: options.refreshReason }),
|
|
103
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
104
|
+
logger: options.logger,
|
|
105
|
+
onError: options.onError,
|
|
106
|
+
clipboard: options.clipboard,
|
|
107
|
+
open: options.open,
|
|
108
|
+
onState: (nextState) => {
|
|
109
|
+
setState(nextState);
|
|
110
|
+
onStateRef.current?.(nextState);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
controllerRef.current = controller;
|
|
114
|
+
controller.start();
|
|
115
|
+
return () => {
|
|
116
|
+
controller.stop();
|
|
117
|
+
if (controllerRef.current === controller)
|
|
118
|
+
controllerRef.current = null;
|
|
119
|
+
};
|
|
120
|
+
}, [
|
|
121
|
+
snapshot,
|
|
122
|
+
options.lookupInvoice,
|
|
123
|
+
options.lookupUrl,
|
|
124
|
+
options.polling,
|
|
125
|
+
options.refreshInvoice,
|
|
126
|
+
options.refreshUrl,
|
|
127
|
+
options.refreshHeaders,
|
|
128
|
+
options.refreshIdempotencyKey,
|
|
129
|
+
options.refreshReason,
|
|
130
|
+
options.pollIntervalMs,
|
|
131
|
+
options.logger,
|
|
132
|
+
options.onError,
|
|
133
|
+
options.clipboard,
|
|
134
|
+
options.open
|
|
135
|
+
]);
|
|
136
|
+
const publicStatus = deriveStatus(state);
|
|
137
|
+
const richStatus = createCheckoutStatusModel(state);
|
|
138
|
+
React.useEffect(() => {
|
|
139
|
+
const announced = paidAnnouncementRef.current;
|
|
140
|
+
if (announced.invoiceId !== snapshot.invoice_id) {
|
|
141
|
+
paidAnnouncementRef.current = {
|
|
142
|
+
invoiceId: snapshot.invoice_id,
|
|
143
|
+
fired: false
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}, [snapshot.invoice_id]);
|
|
147
|
+
React.useEffect(() => {
|
|
148
|
+
const announced = paidAnnouncementRef.current;
|
|
149
|
+
if (publicStatus !== "paid" || announced.fired)
|
|
150
|
+
return;
|
|
151
|
+
paidAnnouncementRef.current = {
|
|
152
|
+
invoiceId: snapshot.invoice_id,
|
|
153
|
+
fired: true
|
|
154
|
+
};
|
|
155
|
+
// UI hint only; server-side fulfillment must use the backend onPaid hook.
|
|
156
|
+
onPaidRef.current?.();
|
|
157
|
+
}, [publicStatus, snapshot.invoice_id]);
|
|
158
|
+
const copyInvoice = React.useCallback(async () => {
|
|
159
|
+
try {
|
|
160
|
+
const controller = controllerRef.current;
|
|
161
|
+
if (controller === null) {
|
|
162
|
+
await copyInvoiceHelper({
|
|
163
|
+
invoice: displayData.invoice,
|
|
164
|
+
clipboard: options.clipboard,
|
|
165
|
+
logger: options.logger,
|
|
166
|
+
logContext
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
await controller.copyInvoice();
|
|
171
|
+
}
|
|
172
|
+
showCopied(true);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
options.onError?.(error);
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}, [logContext, displayData.invoice, options.clipboard, options.logger, options.onError, showCopied]);
|
|
179
|
+
const openWallet = React.useCallback(() => {
|
|
180
|
+
try {
|
|
181
|
+
const controller = controllerRef.current;
|
|
182
|
+
return controller === null
|
|
183
|
+
? openWalletHelper({
|
|
184
|
+
invoice: displayData.invoice,
|
|
185
|
+
open: options.open,
|
|
186
|
+
logger: options.logger,
|
|
187
|
+
logContext
|
|
188
|
+
})
|
|
189
|
+
: controller.openWallet();
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
options.onError?.(error);
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}, [logContext, displayData.invoice, options.open, options.logger, options.onError]);
|
|
196
|
+
const reloadState = React.useCallback(async () => {
|
|
197
|
+
try {
|
|
198
|
+
const next = await controllerRef.current?.reloadState();
|
|
199
|
+
if (next !== undefined)
|
|
200
|
+
setState(next);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
options.onError?.(error);
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}, [options.onError]);
|
|
207
|
+
const retry = React.useCallback(async () => {
|
|
208
|
+
try {
|
|
209
|
+
const next = await controllerRef.current?.retry();
|
|
210
|
+
if (next !== undefined)
|
|
211
|
+
setState(next);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
options.onError?.(error);
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}, [options.onError]);
|
|
218
|
+
const refreshExpiredInvoice = React.useCallback(async () => {
|
|
219
|
+
try {
|
|
220
|
+
const next = await controllerRef.current?.refreshExpiredInvoice();
|
|
221
|
+
if (next !== undefined)
|
|
222
|
+
setState(next);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
options.onError?.(error);
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
}, [options.onError]);
|
|
229
|
+
const cancel = React.useCallback(() => {
|
|
230
|
+
const next = controllerRef.current?.cancel();
|
|
231
|
+
if (next !== undefined)
|
|
232
|
+
setState(next);
|
|
233
|
+
}, []);
|
|
234
|
+
return {
|
|
235
|
+
...model,
|
|
236
|
+
copied,
|
|
237
|
+
status: publicStatus,
|
|
238
|
+
expiresInSeconds: richStatus.expiresInSeconds,
|
|
239
|
+
countdownLabel: richStatus.countdownLabel,
|
|
240
|
+
countdownPrefix: richStatus.countdownPrefix,
|
|
241
|
+
statusTitle: richStatus.title,
|
|
242
|
+
statusDetail: richStatus.detail,
|
|
243
|
+
waiting: richStatus.waiting,
|
|
244
|
+
reloadState,
|
|
245
|
+
retry,
|
|
246
|
+
refreshExpiredInvoice,
|
|
247
|
+
cancel,
|
|
248
|
+
copyInvoice,
|
|
249
|
+
openWallet
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const CheckoutContext = React.createContext(null);
|
|
253
|
+
export function useCheckoutContext() {
|
|
254
|
+
const checkout = React.useContext(CheckoutContext);
|
|
255
|
+
if (checkout === null) {
|
|
256
|
+
throw new Error("useCheckoutContext must be used within CheckoutProvider.");
|
|
257
|
+
}
|
|
258
|
+
return checkout;
|
|
259
|
+
}
|
|
260
|
+
export function CheckoutProvider(props) {
|
|
261
|
+
const { children, ...options } = props;
|
|
262
|
+
const checkout = useCheckout(options);
|
|
263
|
+
const content = typeof children === "function" ? children(checkout) : children;
|
|
264
|
+
return React.createElement(CheckoutContext.Provider, { value: checkout }, content);
|
|
265
|
+
}
|
|
266
|
+
export function QRCode(props) {
|
|
267
|
+
const { invoice, encoder, width = 256, onError, ...divProps } = props;
|
|
268
|
+
const [svg, setSvg] = React.useState("");
|
|
269
|
+
React.useEffect(() => {
|
|
270
|
+
let cancelled = false;
|
|
271
|
+
createQrSvg(invoice, { encoder, width })
|
|
272
|
+
.then((nextSvg) => {
|
|
273
|
+
if (!cancelled)
|
|
274
|
+
setSvg(nextSvg);
|
|
275
|
+
})
|
|
276
|
+
.catch((error) => {
|
|
277
|
+
if (!cancelled)
|
|
278
|
+
onError?.(error);
|
|
279
|
+
});
|
|
280
|
+
return () => {
|
|
281
|
+
cancelled = true;
|
|
282
|
+
};
|
|
283
|
+
}, [invoice, encoder, width, onError]);
|
|
284
|
+
return React.createElement("div", {
|
|
285
|
+
...divProps,
|
|
286
|
+
[OPENRECEIVE_CHECKOUT_DATA_ATTRIBUTES.qr]: "",
|
|
287
|
+
dangerouslySetInnerHTML: {
|
|
288
|
+
__html: svg
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
export function CopyInvoiceButton(props) {
|
|
293
|
+
const { invoice, copyInvoice, clipboard, logger, onCopied, onError, onClick, copiedLabel = openReceiveCheckoutLabels.copied, ButtonComponent = "button", children, type = "button", ...buttonProps } = props;
|
|
294
|
+
const [copied, showCopied] = useOpenReceiveTransientValue(false);
|
|
295
|
+
return React.createElement(ButtonComponent, {
|
|
296
|
+
...buttonProps,
|
|
297
|
+
type,
|
|
298
|
+
onClick: async (event) => {
|
|
299
|
+
onClick?.(event);
|
|
300
|
+
if (event.defaultPrevented)
|
|
301
|
+
return;
|
|
302
|
+
try {
|
|
303
|
+
if (copyInvoice === undefined) {
|
|
304
|
+
await copyInvoiceHelper({ invoice, clipboard, logger });
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
await copyInvoice();
|
|
308
|
+
}
|
|
309
|
+
showCopied(true);
|
|
310
|
+
onCopied?.();
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
onError?.(error);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}, children ?? (copied ? copiedLabel : openReceiveCheckoutLabels.copyInvoice));
|
|
317
|
+
}
|
|
318
|
+
export function OpenWalletButton(props) {
|
|
319
|
+
const { invoice, openWallet, open, logger, onOpened, onError, onClick, ButtonComponent = "button", children = openReceiveCheckoutLabels.openWallet, type = "button", ...buttonProps } = props;
|
|
320
|
+
return React.createElement(ButtonComponent, {
|
|
321
|
+
...buttonProps,
|
|
322
|
+
type,
|
|
323
|
+
onClick: (event) => {
|
|
324
|
+
onClick?.(event);
|
|
325
|
+
if (event.defaultPrevented)
|
|
326
|
+
return;
|
|
327
|
+
try {
|
|
328
|
+
const uri = openWallet === undefined
|
|
329
|
+
? openWalletHelper({ invoice, open, logger })
|
|
330
|
+
: openWallet();
|
|
331
|
+
onOpened?.(uri);
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
onError?.(error);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}, children);
|
|
338
|
+
}
|
|
339
|
+
export function PaymentState(props) {
|
|
340
|
+
const { state = "pending", ...spanProps } = props;
|
|
341
|
+
return React.createElement("span", {
|
|
342
|
+
...spanProps,
|
|
343
|
+
[OPENRECEIVE_CHECKOUT_DATA_ATTRIBUTES.state]: state
|
|
344
|
+
}, state);
|
|
345
|
+
}
|
|
346
|
+
export function useTheme(options = {}) {
|
|
347
|
+
const storageKey = options.storageKey ?? OPENRECEIVE_THEME_STORAGE_KEY;
|
|
348
|
+
const [theme, setThemeState] = React.useState(() => readOpenReceiveThemePreference({
|
|
349
|
+
storage: options.storage,
|
|
350
|
+
storageKey,
|
|
351
|
+
defaultTheme: options.defaultTheme
|
|
352
|
+
}));
|
|
353
|
+
const [systemDark, setSystemDark] = React.useState(() => globalThis.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false);
|
|
354
|
+
React.useEffect(() => {
|
|
355
|
+
const media = globalThis.matchMedia?.("(prefers-color-scheme: dark)");
|
|
356
|
+
if (media === undefined)
|
|
357
|
+
return undefined;
|
|
358
|
+
const update = () => setSystemDark(media.matches);
|
|
359
|
+
media.addEventListener?.("change", update);
|
|
360
|
+
return () => media.removeEventListener?.("change", update);
|
|
361
|
+
}, []);
|
|
362
|
+
const themeModel = createOpenReceiveThemeModel(theme, { systemDark });
|
|
363
|
+
const setTheme = React.useCallback((nextTheme) => {
|
|
364
|
+
setThemeState(nextTheme);
|
|
365
|
+
writeOpenReceiveThemePreference(nextTheme, {
|
|
366
|
+
storage: options.storage,
|
|
367
|
+
storageKey
|
|
368
|
+
});
|
|
369
|
+
}, [options.storage, storageKey]);
|
|
370
|
+
const toggleTheme = React.useCallback(() => {
|
|
371
|
+
setTheme(themeModel.nextTheme);
|
|
372
|
+
}, [setTheme, themeModel.nextTheme]);
|
|
373
|
+
return {
|
|
374
|
+
theme,
|
|
375
|
+
resolvedTheme: themeModel.resolvedTheme,
|
|
376
|
+
model: themeModel,
|
|
377
|
+
nextTheme: themeModel.nextTheme,
|
|
378
|
+
toggleLabel: themeModel.toggleLabel,
|
|
379
|
+
attributes: themeModel.attributes,
|
|
380
|
+
checkoutElementAttributes: themeModel.checkoutElementAttributes,
|
|
381
|
+
setTheme,
|
|
382
|
+
toggleTheme
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
export function ThemeToggle(props) {
|
|
386
|
+
const { theme, resolvedTheme, onThemeChange, ButtonComponent = "button", children, type = "button", onClick, ...buttonProps } = props;
|
|
387
|
+
const fallback = useTheme({
|
|
388
|
+
defaultTheme: theme
|
|
389
|
+
});
|
|
390
|
+
const activeTheme = resolvedTheme ?? fallback.resolvedTheme;
|
|
391
|
+
const themeModel = createOpenReceiveThemeModel(activeTheme);
|
|
392
|
+
const componentProps = {
|
|
393
|
+
...buttonProps,
|
|
394
|
+
"aria-label": themeModel.toggleLabel,
|
|
395
|
+
className: joinClassNames("or-theme-toggle-button", `or-theme-toggle-${themeModel.resolvedTheme}`, buttonProps.className),
|
|
396
|
+
title: themeModel.toggleLabel,
|
|
397
|
+
type,
|
|
398
|
+
[OPENRECEIVE_CHECKOUT_DATA_ATTRIBUTES.themeToggle]: "",
|
|
399
|
+
onClick: (event) => {
|
|
400
|
+
onClick?.(event);
|
|
401
|
+
if (event.defaultPrevented)
|
|
402
|
+
return;
|
|
403
|
+
onThemeChange?.(themeModel.nextTheme);
|
|
404
|
+
if (onThemeChange === undefined)
|
|
405
|
+
fallback.setTheme(themeModel.nextTheme);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
const defaultChildren = React.createElement(React.Fragment, null, React.createElement("span", {
|
|
409
|
+
"aria-hidden": true,
|
|
410
|
+
className: "or-theme-toggle-track"
|
|
411
|
+
}, React.createElement("span", {
|
|
412
|
+
className: "or-theme-toggle-icon or-theme-toggle-icon-light"
|
|
413
|
+
})), React.createElement("span", {
|
|
414
|
+
className: "or-theme-toggle-label"
|
|
415
|
+
}, themeModel.toggleLabel));
|
|
416
|
+
return React.createElement(ButtonComponent, componentProps, children ?? defaultChildren);
|
|
417
|
+
}
|
|
418
|
+
export function ThemeScope(props) {
|
|
419
|
+
const { as: Element = "div", defaultTheme, themeStorageKey, storage, themeToggle = false, topbarClassName, themeToggleClassName, ButtonComponent, children, ...elementProps } = props;
|
|
420
|
+
const theme = useTheme({
|
|
421
|
+
defaultTheme,
|
|
422
|
+
storageKey: themeStorageKey,
|
|
423
|
+
storage
|
|
424
|
+
});
|
|
425
|
+
const scopedChildren = typeof children === "function" ? children(theme) : children;
|
|
426
|
+
return React.createElement(Element, {
|
|
427
|
+
...elementProps,
|
|
428
|
+
...theme.attributes
|
|
429
|
+
}, [
|
|
430
|
+
themeToggle
|
|
431
|
+
? React.createElement("div", {
|
|
432
|
+
className: topbarClassName,
|
|
433
|
+
key: "openreceive-theme-scope-toggle"
|
|
434
|
+
}, React.createElement(ThemeToggle, {
|
|
435
|
+
className: themeToggleClassName,
|
|
436
|
+
theme: theme.theme,
|
|
437
|
+
resolvedTheme: theme.resolvedTheme,
|
|
438
|
+
onThemeChange: theme.setTheme,
|
|
439
|
+
ButtonComponent
|
|
440
|
+
}))
|
|
441
|
+
: null,
|
|
442
|
+
scopedChildren
|
|
443
|
+
]);
|
|
444
|
+
}
|
|
445
|
+
export function WaitingState(props) {
|
|
446
|
+
const status = props.status ??
|
|
447
|
+
createCheckoutStatusModel({
|
|
448
|
+
phase: props.phase,
|
|
449
|
+
waiting: props.waiting ?? false
|
|
450
|
+
});
|
|
451
|
+
return React.createElement("div", {
|
|
452
|
+
className: joinClassNames("or-payment-status", props.className)
|
|
453
|
+
}, status.waiting
|
|
454
|
+
? React.createElement("span", {
|
|
455
|
+
className: "or-spinner",
|
|
456
|
+
"aria-hidden": "true"
|
|
457
|
+
})
|
|
458
|
+
: null, React.createElement("div", null, React.createElement("strong", null, props.statusTitle ?? status.title), React.createElement("span", null, props.statusDetail ?? status.detail)));
|
|
459
|
+
}
|
|
460
|
+
export function PaymentWizard(props) {
|
|
461
|
+
const countryStorageKey = props.countryStorageKey ?? OPENRECEIVE_COUNTRY_STORAGE_KEY;
|
|
462
|
+
const [selection, setSelection] = React.useState(() => createOpenReceivePaymentWizardController({
|
|
463
|
+
storageKey: countryStorageKey,
|
|
464
|
+
defaultCountryCode: getOpenReceiveDefaultCountryCode()
|
|
465
|
+
}).getSelection());
|
|
466
|
+
const [activeTutorial, setActiveTutorial] = React.useState(null);
|
|
467
|
+
const updateWizardSelection = React.useCallback((apply) => {
|
|
468
|
+
setSelection((current) => apply(createOpenReceivePaymentWizardController({
|
|
469
|
+
selection: current,
|
|
470
|
+
storageKey: countryStorageKey
|
|
471
|
+
})));
|
|
472
|
+
}, [countryStorageKey]);
|
|
473
|
+
const model = createOpenReceivePaymentWizardModel(selection);
|
|
474
|
+
const { wizard } = model;
|
|
475
|
+
const routeAssetDisplays = createOpenReceiveWizardRouteAssetDisplays(model.routeAssets, {
|
|
476
|
+
selectedRoute: model.selectedRoute
|
|
477
|
+
});
|
|
478
|
+
const routeDisplays = createOpenReceiveWizardRouteDisplays(wizard.routes);
|
|
479
|
+
const showRoutePicker = routeAssetDisplays.length > 0 &&
|
|
480
|
+
(model.selectedRoute === null || routeDisplays.length === 0);
|
|
481
|
+
const activeTutorialProvider = activeTutorial === null
|
|
482
|
+
? undefined
|
|
483
|
+
: routeDisplays
|
|
484
|
+
.flatMap((route) => route.providers)
|
|
485
|
+
.find((provider) => provider.id === activeTutorial.providerId);
|
|
486
|
+
return React.createElement("div", {
|
|
487
|
+
className: joinClassNames("or-wizard", props.className)
|
|
488
|
+
}, selection.selectedMethod === null
|
|
489
|
+
? React.createElement(React.Fragment, null, React.createElement("div", {
|
|
490
|
+
className: "or-wizard-header"
|
|
491
|
+
}, React.createElement("div", null, React.createElement("h2", null, openReceiveCheckoutLabels.wizardTitle), React.createElement("p", null, openReceiveCheckoutLabels.wizardSubtitle))), React.createElement("div", {
|
|
492
|
+
className: "or-method-grid"
|
|
493
|
+
}, openReceivePaymentMethods.map((method) => React.createElement("button", {
|
|
494
|
+
key: method.id,
|
|
495
|
+
onClick: () => {
|
|
496
|
+
updateWizardSelection((controller) => controller.selectMethod(method.id));
|
|
497
|
+
},
|
|
498
|
+
type: "button"
|
|
499
|
+
}, React.createElement("img", { alt: "", src: getOpenReceivePaymentMethodIcon(method.id) }), React.createElement("span", null, method.title), React.createElement("small", null, method.detail)))))
|
|
500
|
+
: null, selection.selectedMethod === null
|
|
501
|
+
? null
|
|
502
|
+
: renderWizardBreadcrumbs({
|
|
503
|
+
method: selection.selectedMethod,
|
|
504
|
+
selectedRoute: model.selectedRoute,
|
|
505
|
+
routeAssets: routeAssetDisplays,
|
|
506
|
+
onChangeMethod: () => {
|
|
507
|
+
updateWizardSelection((controller) => controller.changeMethod());
|
|
508
|
+
},
|
|
509
|
+
onChangeRoute: () => {
|
|
510
|
+
updateWizardSelection((controller) => controller.update({ type: "change_route" }));
|
|
511
|
+
}
|
|
512
|
+
}), showRoutePicker && selection.selectedMethod === "bitcoin"
|
|
513
|
+
? renderRoutePicker({
|
|
514
|
+
assets: routeAssetDisplays,
|
|
515
|
+
method: "bitcoin",
|
|
516
|
+
onSelectRoute: (route) => {
|
|
517
|
+
updateWizardSelection((controller) => controller.selectRoute(route));
|
|
518
|
+
}
|
|
519
|
+
})
|
|
520
|
+
: null, showRoutePicker && selection.selectedMethod === "crypto"
|
|
521
|
+
? renderRoutePicker({
|
|
522
|
+
assets: routeAssetDisplays,
|
|
523
|
+
method: "crypto",
|
|
524
|
+
onSelectRoute: (route) => {
|
|
525
|
+
updateWizardSelection((controller) => controller.selectRoute(route));
|
|
526
|
+
}
|
|
527
|
+
})
|
|
528
|
+
: null, selection.selectedMethod === null
|
|
529
|
+
? null
|
|
530
|
+
: React.createElement("div", {
|
|
531
|
+
className: "or-wizard-results"
|
|
532
|
+
}, routeDisplays.length === 0
|
|
533
|
+
? React.createElement("p", {
|
|
534
|
+
className: "or-wizard-empty"
|
|
535
|
+
}, getOpenReceiveWizardEmptyMessage(selection.selectedMethod))
|
|
536
|
+
: routeDisplays.map((route) => React.createElement("section", {
|
|
537
|
+
className: "or-wizard-route",
|
|
538
|
+
key: route.key
|
|
539
|
+
}, React.createElement("div", {
|
|
540
|
+
className: "or-wizard-route-heading"
|
|
541
|
+
}, React.createElement("div", null, React.createElement("h3", null, route.title, wizard.selectedRail === null
|
|
542
|
+
? null
|
|
543
|
+
: renderCountrySelect({
|
|
544
|
+
countries: model.countryDisplays,
|
|
545
|
+
selectedCountryCode: selection.selectedCountryCode,
|
|
546
|
+
onSelectCountry: (countryCode) => {
|
|
547
|
+
updateWizardSelection((controller) => controller.selectCountry(countryCode));
|
|
548
|
+
}
|
|
549
|
+
})))), React.createElement("div", {
|
|
550
|
+
className: "or-provider-grid"
|
|
551
|
+
}, route.providers.map((provider) => React.createElement("article", {
|
|
552
|
+
className: provider.recommended
|
|
553
|
+
? "or-provider-card recommended"
|
|
554
|
+
: "or-provider-card",
|
|
555
|
+
key: provider.id
|
|
556
|
+
}, React.createElement("div", {
|
|
557
|
+
className: "or-provider-heading"
|
|
558
|
+
}, React.createElement("img", {
|
|
559
|
+
alt: "",
|
|
560
|
+
src: provider.icon
|
|
561
|
+
}), React.createElement("h4", null, provider.name), provider.recommendedLabel === null
|
|
562
|
+
? null
|
|
563
|
+
: React.createElement("span", null, provider.recommendedLabel)), React.createElement("p", {
|
|
564
|
+
className: "or-provider-kind"
|
|
565
|
+
}, provider.kind), React.createElement("div", {
|
|
566
|
+
className: "or-provider-actions"
|
|
567
|
+
}, renderProviderOpenAction(provider, () => setActiveTutorial({
|
|
568
|
+
providerId: provider.id,
|
|
569
|
+
index: 0,
|
|
570
|
+
copied: false
|
|
571
|
+
}))))))))), activeTutorialProvider === undefined || activeTutorial === null
|
|
572
|
+
? null
|
|
573
|
+
: renderProviderTutorialModal({
|
|
574
|
+
provider: activeTutorialProvider,
|
|
575
|
+
index: activeTutorial.index,
|
|
576
|
+
copied: activeTutorial.copied,
|
|
577
|
+
onClose: () => setActiveTutorial(null),
|
|
578
|
+
onCopy: async () => {
|
|
579
|
+
try {
|
|
580
|
+
await copyInvoiceHelper({
|
|
581
|
+
invoice: props.invoice,
|
|
582
|
+
logger: props.logger,
|
|
583
|
+
logContext: props.logContext
|
|
584
|
+
});
|
|
585
|
+
globalThis.dispatchEvent?.(createCheckoutProviderCopyEvent(activeTutorialProvider.id));
|
|
586
|
+
setActiveTutorial({
|
|
587
|
+
providerId: activeTutorialProvider.id,
|
|
588
|
+
index: 0,
|
|
589
|
+
copied: true
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
catch (error) {
|
|
593
|
+
props.onError?.(error);
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
onStep: (index) => setActiveTutorial({
|
|
597
|
+
providerId: activeTutorialProvider.id,
|
|
598
|
+
index,
|
|
599
|
+
copied: activeTutorial.copied
|
|
600
|
+
})
|
|
601
|
+
}));
|
|
602
|
+
}
|
|
603
|
+
function renderWizardBreadcrumbs(options) {
|
|
604
|
+
const method = openReceivePaymentMethods.find((candidate) => candidate.id === options.method);
|
|
605
|
+
const methodLabel = method?.title ?? openReceiveCheckoutLabels.paymentMethod;
|
|
606
|
+
const routeLabel = options.selectedRoute === null || options.routeAssets.length <= 1
|
|
607
|
+
? null
|
|
608
|
+
: options.routeAssets.find((asset) => asset.id === options.selectedRoute)?.label ?? options.selectedRoute;
|
|
609
|
+
return React.createElement("nav", {
|
|
610
|
+
"aria-label": "Payment path",
|
|
611
|
+
className: "or-wizard-breadcrumbs"
|
|
612
|
+
}, React.createElement("button", {
|
|
613
|
+
className: "or-wizard-breadcrumb",
|
|
614
|
+
onClick: options.onChangeMethod,
|
|
615
|
+
type: "button"
|
|
616
|
+
}, openReceiveCheckoutLabels.paymentMethod), React.createElement("span", { "aria-hidden": "true" }, "/"), routeLabel === null
|
|
617
|
+
? React.createElement("span", { className: "or-wizard-breadcrumb-current" }, methodLabel)
|
|
618
|
+
: React.createElement(React.Fragment, null, React.createElement("button", {
|
|
619
|
+
className: "or-wizard-breadcrumb",
|
|
620
|
+
onClick: options.onChangeRoute,
|
|
621
|
+
type: "button"
|
|
622
|
+
}, methodLabel), React.createElement("span", { "aria-hidden": "true" }, "/"), React.createElement("span", { className: "or-wizard-breadcrumb-current" }, routeLabel)));
|
|
623
|
+
}
|
|
624
|
+
function renderProviderOpenAction(provider, onOpenTutorial) {
|
|
625
|
+
if (provider.tutorials.length === 0) {
|
|
626
|
+
return React.createElement("a", {
|
|
627
|
+
href: provider.url,
|
|
628
|
+
rel: "noreferrer",
|
|
629
|
+
target: "_blank"
|
|
630
|
+
}, provider.openLabel);
|
|
631
|
+
}
|
|
632
|
+
return React.createElement("button", {
|
|
633
|
+
className: "or-provider-open",
|
|
634
|
+
onClick: onOpenTutorial,
|
|
635
|
+
type: "button"
|
|
636
|
+
}, provider.openLabel);
|
|
637
|
+
}
|
|
638
|
+
function renderProviderTutorialModal(options) {
|
|
639
|
+
const { provider } = options;
|
|
640
|
+
if (provider.tutorials.length === 0)
|
|
641
|
+
return null;
|
|
642
|
+
const totalSteps = provider.tutorials.length + 1;
|
|
643
|
+
const stepIndex = Math.max(0, Math.min(provider.tutorials.length, options.index));
|
|
644
|
+
const tutorial = stepIndex === 0 ? undefined : provider.tutorials[stepIndex - 1];
|
|
645
|
+
const previousIndex = Math.max(0, stepIndex - 1);
|
|
646
|
+
const nextIndex = Math.min(provider.tutorials.length, stepIndex + 1);
|
|
647
|
+
const isFinalStep = stepIndex === provider.tutorials.length;
|
|
648
|
+
return React.createElement("div", {
|
|
649
|
+
"aria-label": `${openReceiveCheckoutLabels.tutorialTitlePrefix} ${provider.name}`,
|
|
650
|
+
"aria-modal": true,
|
|
651
|
+
className: "or-tutorial-modal",
|
|
652
|
+
onClick: (event) => {
|
|
653
|
+
if (event.target === event.currentTarget)
|
|
654
|
+
options.onClose();
|
|
655
|
+
},
|
|
656
|
+
onKeyDown: (event) => {
|
|
657
|
+
if (event.key === "Escape")
|
|
658
|
+
options.onClose();
|
|
659
|
+
},
|
|
660
|
+
role: "dialog",
|
|
661
|
+
tabIndex: -1
|
|
662
|
+
}, React.createElement("div", {
|
|
663
|
+
className: "or-tutorial-dialog"
|
|
664
|
+
}, React.createElement("div", {
|
|
665
|
+
className: "or-tutorial-header"
|
|
666
|
+
}, React.createElement("div", {
|
|
667
|
+
className: "or-tutorial-title"
|
|
668
|
+
}, React.createElement("img", {
|
|
669
|
+
alt: "",
|
|
670
|
+
className: "or-tutorial-header-logo",
|
|
671
|
+
src: provider.icon
|
|
672
|
+
}), React.createElement("h3", null, `${openReceiveCheckoutLabels.tutorialTitlePrefix} ${provider.name}`)), React.createElement("button", {
|
|
673
|
+
"aria-label": "Close",
|
|
674
|
+
className: "or-tutorial-close",
|
|
675
|
+
onClick: options.onClose,
|
|
676
|
+
type: "button"
|
|
677
|
+
}, "X")), stepIndex === 0
|
|
678
|
+
? React.createElement("div", {
|
|
679
|
+
className: "or-tutorial-intro"
|
|
680
|
+
}, React.createElement("img", {
|
|
681
|
+
alt: "",
|
|
682
|
+
className: "or-tutorial-provider-logo",
|
|
683
|
+
src: provider.icon
|
|
684
|
+
}), React.createElement("p", null, `${openReceiveCheckoutLabels.tutorialIntroPrefix} ${provider.name}.`), React.createElement("p", null, openReceiveCheckoutLabels.tutorialIntroCopy), React.createElement("button", {
|
|
685
|
+
className: "or-tutorial-copy",
|
|
686
|
+
onClick: () => void options.onCopy(),
|
|
687
|
+
type: "button"
|
|
688
|
+
}, openReceiveCheckoutLabels.copyInvoice), options.copied
|
|
689
|
+
? React.createElement("p", {
|
|
690
|
+
className: "or-tutorial-copy-message"
|
|
691
|
+
}, openReceiveCheckoutLabels.tutorialCopiedContinue)
|
|
692
|
+
: null)
|
|
693
|
+
: React.createElement(React.Fragment, null, React.createElement("div", {
|
|
694
|
+
className: "or-tutorial-frame"
|
|
695
|
+
}, React.createElement("img", {
|
|
696
|
+
alt: tutorial?.caption ?? "",
|
|
697
|
+
className: "or-tutorial-image",
|
|
698
|
+
src: tutorial?.image ?? ""
|
|
699
|
+
})), React.createElement("p", {
|
|
700
|
+
className: "or-tutorial-caption"
|
|
701
|
+
}, tutorial?.caption ?? "")), React.createElement("div", {
|
|
702
|
+
"aria-hidden": "true",
|
|
703
|
+
className: "or-tutorial-steps"
|
|
704
|
+
}, Array.from({ length: totalSteps }, (_, index) => React.createElement("span", {
|
|
705
|
+
className: index === stepIndex
|
|
706
|
+
? "or-tutorial-step active"
|
|
707
|
+
: "or-tutorial-step",
|
|
708
|
+
key: index
|
|
709
|
+
}))), React.createElement("p", {
|
|
710
|
+
className: "or-tutorial-progress"
|
|
711
|
+
}, `Step ${stepIndex + 1} of ${totalSteps}`), React.createElement("div", {
|
|
712
|
+
className: "or-tutorial-controls"
|
|
713
|
+
}, React.createElement("button", {
|
|
714
|
+
disabled: stepIndex === 0,
|
|
715
|
+
onClick: () => options.onStep(previousIndex),
|
|
716
|
+
type: "button"
|
|
717
|
+
}, "Back"), React.createElement("button", {
|
|
718
|
+
onClick: () => {
|
|
719
|
+
if (isFinalStep) {
|
|
720
|
+
options.onClose();
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
options.onStep(nextIndex);
|
|
724
|
+
},
|
|
725
|
+
type: "button"
|
|
726
|
+
}, isFinalStep ? openReceiveCheckoutLabels.tutorialExit : "Next"))));
|
|
727
|
+
}
|
|
728
|
+
export function InvoiceSummary(props) {
|
|
729
|
+
const { amountLabel, fiatLabel, status, PaymentStateComponent = PaymentState, classNames, className, ...divProps } = props;
|
|
730
|
+
return React.createElement("div", {
|
|
731
|
+
...divProps,
|
|
732
|
+
className,
|
|
733
|
+
[OPENRECEIVE_CHECKOUT_DATA_ATTRIBUTES.meta]: ""
|
|
734
|
+
}, amountLabel === undefined
|
|
735
|
+
? null
|
|
736
|
+
: React.createElement("span", {
|
|
737
|
+
className: classNames?.amount
|
|
738
|
+
}, amountLabel), fiatLabel === undefined
|
|
739
|
+
? null
|
|
740
|
+
: React.createElement("span", {
|
|
741
|
+
className: classNames?.fiat
|
|
742
|
+
}, fiatLabel), status === undefined
|
|
743
|
+
? null
|
|
744
|
+
: React.createElement(PaymentStateComponent, {
|
|
745
|
+
state: status,
|
|
746
|
+
className: classNames?.paymentState
|
|
747
|
+
}));
|
|
748
|
+
}
|
|
749
|
+
export function Checkout(props) {
|
|
750
|
+
const { invoice, qrEncoder, logger, onError, lookupInvoice, lookupUrl, refreshInvoice, refreshUrl, refreshHeaders, refreshIdempotencyKey, refreshReason, onState, onPaid, onStartOver, polling, paymentWizard = true, themeSwitcher = false, defaultTheme, themeStorageKey, countryStorageKey, components, classNames, children, className, ...sectionProps } = props;
|
|
751
|
+
const checkoutModel = useCheckout({
|
|
752
|
+
invoice,
|
|
753
|
+
logger,
|
|
754
|
+
onError,
|
|
755
|
+
lookupInvoice,
|
|
756
|
+
lookupUrl,
|
|
757
|
+
refreshInvoice,
|
|
758
|
+
refreshUrl,
|
|
759
|
+
refreshHeaders,
|
|
760
|
+
refreshIdempotencyKey,
|
|
761
|
+
refreshReason,
|
|
762
|
+
onState,
|
|
763
|
+
onPaid,
|
|
764
|
+
polling
|
|
765
|
+
});
|
|
766
|
+
const theme = useTheme({
|
|
767
|
+
defaultTheme,
|
|
768
|
+
storageKey: themeStorageKey
|
|
769
|
+
});
|
|
770
|
+
const QRCodeComponent = components?.QRCode ?? QRCode;
|
|
771
|
+
const InvoiceSummaryComponent = components?.InvoiceSummary ?? InvoiceSummary;
|
|
772
|
+
const CopyButton = components?.CopyButton ?? CopyInvoiceButton;
|
|
773
|
+
const ButtonComponent = components?.Button;
|
|
774
|
+
const PaymentStateComponent = components?.PaymentState ?? PaymentState;
|
|
775
|
+
const customChildren = typeof children === "function" ? children(checkoutModel) : children;
|
|
776
|
+
const expired = checkoutModel.status === "expired";
|
|
777
|
+
const startOver = () => {
|
|
778
|
+
if (onStartOver !== undefined) {
|
|
779
|
+
onStartOver();
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
void checkoutModel.refreshExpiredInvoice().catch((error) => {
|
|
783
|
+
onError?.(error);
|
|
784
|
+
});
|
|
785
|
+
};
|
|
786
|
+
return React.createElement("section", {
|
|
787
|
+
...sectionProps,
|
|
788
|
+
className: joinClassNames(className, classNames?.root),
|
|
789
|
+
[OPENRECEIVE_CHECKOUT_DATA_ATTRIBUTES.root]: "",
|
|
790
|
+
...theme.attributes
|
|
791
|
+
}, customChildren === undefined
|
|
792
|
+
? [
|
|
793
|
+
themeSwitcher
|
|
794
|
+
? React.createElement(ThemeToggle, {
|
|
795
|
+
key: "theme",
|
|
796
|
+
className: classNames?.themeToggle,
|
|
797
|
+
theme: theme.theme,
|
|
798
|
+
resolvedTheme: theme.resolvedTheme,
|
|
799
|
+
onThemeChange: theme.setTheme,
|
|
800
|
+
ButtonComponent
|
|
801
|
+
})
|
|
802
|
+
: null,
|
|
803
|
+
expired
|
|
804
|
+
? null
|
|
805
|
+
: React.createElement(QRCodeComponent, {
|
|
806
|
+
key: "qr",
|
|
807
|
+
invoice: checkoutModel.invoice,
|
|
808
|
+
encoder: qrEncoder,
|
|
809
|
+
onError,
|
|
810
|
+
className: classNames?.qr,
|
|
811
|
+
style: {
|
|
812
|
+
aspectRatio: "1",
|
|
813
|
+
justifySelf: "center",
|
|
814
|
+
maxWidth: 420,
|
|
815
|
+
width: "min(100%, 420px)"
|
|
816
|
+
}
|
|
817
|
+
}),
|
|
818
|
+
React.createElement(WaitingState, {
|
|
819
|
+
key: "waiting",
|
|
820
|
+
waiting: checkoutModel.waiting,
|
|
821
|
+
statusTitle: checkoutModel.statusTitle,
|
|
822
|
+
statusDetail: checkoutModel.statusDetail,
|
|
823
|
+
className: classNames?.waiting
|
|
824
|
+
}),
|
|
825
|
+
checkoutModel.countdownLabel === undefined
|
|
826
|
+
? null
|
|
827
|
+
: React.createElement("div", {
|
|
828
|
+
key: "countdown",
|
|
829
|
+
className: joinClassNames("or-countdown", classNames?.countdown)
|
|
830
|
+
}, checkoutModel.countdownPrefix, " ", React.createElement("strong", null, checkoutModel.countdownLabel)),
|
|
831
|
+
React.createElement(InvoiceSummaryComponent, {
|
|
832
|
+
key: "summary",
|
|
833
|
+
amountLabel: checkoutModel.amountLabel,
|
|
834
|
+
fiatLabel: checkoutModel.fiatLabel,
|
|
835
|
+
status: checkoutModel.status,
|
|
836
|
+
PaymentStateComponent,
|
|
837
|
+
className: classNames?.summary,
|
|
838
|
+
classNames
|
|
839
|
+
}),
|
|
840
|
+
React.createElement("div", {
|
|
841
|
+
key: "actions",
|
|
842
|
+
className: classNames?.actions,
|
|
843
|
+
[OPENRECEIVE_CHECKOUT_DATA_ATTRIBUTES.actions]: ""
|
|
844
|
+
}, expired
|
|
845
|
+
? React.createElement(ButtonComponent ?? "button", {
|
|
846
|
+
type: "button",
|
|
847
|
+
onClick: startOver
|
|
848
|
+
}, openReceiveCheckoutLabels.startOver)
|
|
849
|
+
: React.createElement(CopyButton, {
|
|
850
|
+
invoice: checkoutModel.invoice,
|
|
851
|
+
copyInvoice: checkoutModel.copyInvoice,
|
|
852
|
+
onError,
|
|
853
|
+
logger,
|
|
854
|
+
ButtonComponent,
|
|
855
|
+
className: classNames?.copyButton
|
|
856
|
+
})),
|
|
857
|
+
paymentWizard && !expired
|
|
858
|
+
? React.createElement(PaymentWizard, {
|
|
859
|
+
key: "wizard",
|
|
860
|
+
invoice: checkoutModel.invoice,
|
|
861
|
+
className: classNames?.wizard,
|
|
862
|
+
logger,
|
|
863
|
+
onError,
|
|
864
|
+
countryStorageKey,
|
|
865
|
+
logContext: getCheckoutLogContext({
|
|
866
|
+
invoice_id: checkoutModel.invoice_id,
|
|
867
|
+
payment_hash: checkoutModel.payment_hash,
|
|
868
|
+
amount_msats: checkoutModel.amount_msats
|
|
869
|
+
})
|
|
870
|
+
})
|
|
871
|
+
: null
|
|
872
|
+
]
|
|
873
|
+
: customChildren);
|
|
874
|
+
}
|
|
875
|
+
function getCheckoutLogContext(data) {
|
|
876
|
+
return {
|
|
877
|
+
...(data.invoice_id === undefined ? {} : { invoice_id: data.invoice_id }),
|
|
878
|
+
...(data.payment_hash === undefined ? {} : { payment_hash: data.payment_hash }),
|
|
879
|
+
...(data.amount_msats === undefined ? {} : { amount_msats: data.amount_msats }),
|
|
880
|
+
...(data.transaction_state === undefined
|
|
881
|
+
? {}
|
|
882
|
+
: { transaction_state: data.transaction_state }),
|
|
883
|
+
...(data.workflow_state === undefined
|
|
884
|
+
? {}
|
|
885
|
+
: { workflow_state: data.workflow_state })
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
function renderCountrySelect(options) {
|
|
889
|
+
return React.createElement("label", {
|
|
890
|
+
className: "or-country-select"
|
|
891
|
+
}, React.createElement("span", null, openReceiveCheckoutLabels.chooseCountry), React.createElement("select", {
|
|
892
|
+
value: options.selectedCountryCode,
|
|
893
|
+
onChange: (event) => {
|
|
894
|
+
options.onSelectCountry(event.currentTarget.value);
|
|
895
|
+
}
|
|
896
|
+
}, options.countries.map((country) => React.createElement("option", {
|
|
897
|
+
key: country.code,
|
|
898
|
+
value: country.code
|
|
899
|
+
}, country.label))));
|
|
900
|
+
}
|
|
901
|
+
function renderRoutePicker(options) {
|
|
902
|
+
return React.createElement("div", {
|
|
903
|
+
className: `or-route-picker or-route-picker-${options.method}`
|
|
904
|
+
}, options.assets.map((asset) => {
|
|
905
|
+
return React.createElement("button", {
|
|
906
|
+
className: asset.selected ? "selected" : "",
|
|
907
|
+
key: asset.id,
|
|
908
|
+
onClick: () => options.onSelectRoute(asset.id),
|
|
909
|
+
type: "button"
|
|
910
|
+
}, React.createElement("img", {
|
|
911
|
+
alt: "",
|
|
912
|
+
src: asset.icon
|
|
913
|
+
}), React.createElement("span", null, asset.label), React.createElement("small", null, asset.subtitle));
|
|
914
|
+
}));
|
|
915
|
+
}
|
|
916
|
+
function joinClassNames(...values) {
|
|
917
|
+
const joined = values.filter(Boolean).join(" ");
|
|
918
|
+
return joined === "" ? undefined : joined;
|
|
919
|
+
}
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "@openreceive/browser/styles.css";
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openreceive/react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@openreceive/browser": "0.1.0"
|
|
7
|
+
},
|
|
8
|
+
"peerDependencies": {
|
|
9
|
+
"react": "*"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./dist/index.js",
|
|
13
|
+
"./styles.css": "./dist/styles.css"
|
|
14
|
+
},
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
]
|
|
21
|
+
}
|