QRCode.tsx raw
1 import { type ReactNode, useEffect, useMemo, useRef } from "react";
2 import QRCodeStyling, { type Options } from "qr-code-styling";
3 import { BitcoinPaymentIcon } from "src/components/icons/BitcoinPayment";
4 import { LightningIcon } from "src/components/icons/Lightning";
5 import { cn } from "src/lib/utils";
6
7 export type Props = {
8 value: string;
9 size?: number;
10 className?: string;
11 showAvatar?: boolean;
12 frameType?: "lightning" | "onchain";
13 paymentType?: "lightning" | "onchain";
14 centerContent?: ReactNode;
15
16 // Use Q when an external overlay covers part of the QR code.
17 level?: "Q" | undefined;
18 };
19
20 function QRCode({
21 value,
22 size = 256,
23 level,
24 className,
25 showAvatar = false,
26 frameType,
27 paymentType,
28 centerContent,
29 }: Props) {
30 const resolvedFrameType = paymentType ?? frameType;
31 const hasCenterContent = Boolean(centerContent);
32 const containerRef = useRef<HTMLDivElement>(null);
33 const options = useMemo<Options>(
34 () => ({
35 type: "svg",
36 width: size,
37 height: size,
38 data: value,
39 image: showAvatar ? "/icon-lightmode.svg" : undefined,
40 margin: 0,
41 qrOptions: {
42 errorCorrectionLevel:
43 level ?? (showAvatar || paymentType || hasCenterContent ? "Q" : "M"),
44 },
45 imageOptions: {
46 crossOrigin: "anonymous",
47 hideBackgroundDots: true,
48 imageSize: 0.15,
49 margin: 4,
50 },
51 dotsOptions: {
52 color: "var(--qr-foreground)",
53 type: "dots",
54 roundSize: false,
55 },
56 cornersSquareOptions: {
57 color: "var(--qr-foreground)",
58 type: "extra-rounded",
59 },
60 cornersDotOptions: {
61 color: "var(--qr-foreground)",
62 type: "dot",
63 },
64 backgroundOptions: {
65 color: "var(--qr-background)",
66 },
67 }),
68 [hasCenterContent, level, paymentType, showAvatar, size, value]
69 );
70
71 useEffect(() => {
72 const container = containerRef.current;
73 if (!container) {
74 return;
75 }
76
77 const qrCode = new QRCodeStyling(options);
78 qrCode.append(container);
79
80 return () => {
81 container.replaceChildren();
82 };
83 }, [options]);
84
85 return (
86 <div
87 className={cn(
88 "relative w-full rounded-[28px] p-2",
89 resolvedFrameType === "lightning"
90 ? "bg-payment-lightning"
91 : resolvedFrameType === "onchain"
92 ? "bg-payment-onchain"
93 : "bg-primary",
94 className
95 )}
96 style={{ maxWidth: size }}
97 >
98 <div className="rounded-3xl bg-qr-background p-4">
99 <div
100 ref={containerRef}
101 className="aspect-square w-full overflow-hidden [&_svg]:size-full"
102 />
103 </div>
104 {(paymentType || centerContent) && (
105 <div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-qr-background p-1 text-qr-background">
106 {centerContent ??
107 (paymentType === "lightning" ? (
108 <LightningIcon className="size-12" />
109 ) : (
110 <BitcoinPaymentIcon className="size-12" />
111 ))}
112 </div>
113 )}
114 </div>
115 );
116 }
117
118 export default QRCode;
119