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