React Material UI Recipe
This recipe composes OpenReceive’s React checkout with Material UI components. It is a composition example, not a dependency of OpenReceive.
Frontend code still receives only display-safe checkout data. The backend creates the checkout, verifies payment, and owns fulfillment.
Create mode (the default integration)
<Checkout reference> prepares, mints, and polls by itself; Material UI comes in
through the component and class-name slots. Use the slots when your
design-system button accepts native button props:
import Button from "@mui/material/Button";
import { Checkout } from "@openreceive/react";
import "@openreceive/react/styles.css";
export function MaterialOrderCheckout({ reference }) {
return (
<Checkout
reference={reference}
prefix="/openreceive"
components={{
Button,
InvoiceSummary: MaterialInvoiceSummary,
}}
classNames={{
root: "or-material-root",
actions: "or-material-actions",
}}
/>
);
}
Snapshot variant (custom layout)
For a fully custom layout — here a Material Dialog — drive a checkout
snapshot through useCheckout. Unlike <Checkout>, the hook does not default
prefix, so the bare useCheckout({ checkout }) below renders the snapshot it
is handed and never polls — the dialog updates only when the parent passes a
newer snapshot. Add prefix to make the hook poll ${prefix}/payments/check
itself (useCheckout({ checkout, prefix: "/openreceive" })), then tune it with
pollIntervalMs, or stop it again with polling: false:
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import {
QRCode,
PaymentState,
useCheckout
} from "@openreceive/react";
export function MaterialCheckoutDialog({
checkout,
open,
onClose
}) {
const model = useCheckout({ checkout });
const isTouchDevice = window.matchMedia("(pointer: coarse)").matches;
return (
<Dialog open={open} onClose={onClose} fullWidth maxWidth="xs">
<DialogContent>
<Stack spacing={2} alignItems="center">
<QRCode
invoice={model.invoice}
style={{
inlineSize: 256,
blockSize: 256
}}
/>
<Typography variant="body2" sx={{ overflowWrap: "anywhere" }}>
{model.invoice}
</Typography>
<PaymentState state={model.status} />
</Stack>
</DialogContent>
<DialogActions>
{/* Copy is the primary action, and it is the only one on a desktop.
`model.openWallet` hands the payer a `lightning:` URI by navigating
the CURRENT window: with no registered handler the click is inert,
and with one it takes the payer off a checkout that is still
polling. Draw it only where it works — which is why the shipped
<Checkout> ships no wallet button and exposes
`components.OpenWalletButton` as an opt-in slot instead. */}
<Button onClick={model.copyInvoice} variant="contained">
Copy invoice
</Button>
{isTouchDevice && (
<Button onClick={model.openWallet}>Open Wallet</Button>
)}
</DialogActions>
</Dialog>
);
}
Everything above about components and classNames applies to both modes.
The QR code is the desktop payment path, so the dialog above leads with Copy and
keeps the wallet button behind a touch check. See
Headless checkout
for what openWallet actually does.
Integration Notes
- Do not pass
nwcor any receive-only NWC code into React. - Create orders through your backend; the checkout mints from the order ID.
- Treat client polling as UI state only.
- Fulfill products only from your backend
onPaidpath. - Keep Material UI theme ownership in your app.