UpgradeDialog.tsx raw

   1  import { CheckIcon, SparklesIcon, StarsIcon } from "lucide-react";
   2  import { ReactNode, useState } from "react";
   3  import { Badge } from "src/components/ui/badge";
   4  import { ExternalLinkButton } from "src/components/ui/custom/external-link-button";
   5  import {
   6    Dialog,
   7    DialogContent,
   8    DialogDescription,
   9    DialogHeader,
  10    DialogTitle,
  11  } from "src/components/ui/dialog";
  12  import { DropdownMenuItem } from "src/components/ui/dropdown-menu";
  13  import { Separator } from "src/components/ui/separator";
  14  import { useAlbyMe } from "src/hooks/useAlbyMe";
  15  import { useInfo } from "src/hooks/useInfo";
  16  
  17  interface UpgradeDialogProps {
  18    children?: ReactNode;
  19    open?: boolean;
  20    onOpenChange?: (open: boolean) => void;
  21  }
  22  
  23  interface ProDropdownMenuItemProps {
  24    children: ReactNode;
  25    onClick?: () => void;
  26  }
  27  
  28  export function ProDropdownMenuItem({
  29    children,
  30    onClick,
  31  }: ProDropdownMenuItemProps) {
  32    const { data: albyMe } = useAlbyMe();
  33  
  34    if (!albyMe?.subscription.plan_code) {
  35      return (
  36        <UpgradeDialog>
  37          <div className="cursor-pointer">
  38            <DropdownMenuItem className="w-full pointer-events-none">
  39              {children}
  40              <Badge variant="outline">
  41                <StarsIcon />
  42                Pro
  43              </Badge>
  44            </DropdownMenuItem>
  45          </div>
  46        </UpgradeDialog>
  47      );
  48    }
  49  
  50    return (
  51      <DropdownMenuItem className="cursor-pointer" onClick={onClick}>
  52        {children}
  53        <Badge variant="outline">
  54          <StarsIcon />
  55          Pro
  56        </Badge>
  57      </DropdownMenuItem>
  58    );
  59  }
  60  
  61  export const UpgradeDialog = ({
  62    children,
  63    open: controlledOpen,
  64    onOpenChange: controlledOnOpenChange,
  65  }: UpgradeDialogProps) => {
  66    const [internalOpen, setInternalOpen] = useState(false);
  67    const open = controlledOpen ?? internalOpen;
  68    const setOpen = controlledOnOpenChange ?? setInternalOpen;
  69  
  70    const { data: albyMe } = useAlbyMe();
  71    const { data: info } = useInfo();
  72  
  73    if (!info || (info.albyAccountConnected && !albyMe)) {
  74      // still loading - make sure button does not incorrectly show
  75      return;
  76    }
  77  
  78    if (albyMe?.subscription.plan_code) {
  79      return null;
  80    }
  81  
  82    return (
  83      <Dialog open={open} onOpenChange={setOpen}>
  84        {children && (
  85          <span onClick={() => setOpen(true)} className="cursor-pointer">
  86            {children}
  87          </span>
  88        )}
  89        <DialogContent className="md:max-w-md">
  90          <DialogHeader className="sr-only">
  91            <DialogTitle>Upgrade to Pro</DialogTitle>
  92            <DialogDescription>
  93              Upgrade to Alby Hub Pro to unlock advanced features and help fund
  94              Alby's open-source work.
  95            </DialogDescription>
  96          </DialogHeader>
  97  
  98          <div className="flex flex-col">
  99            <h2 className="flex flex-wrap items-center gap-2 text-xl font-semibold tracking-tight">
 100              Do more with your Hub
 101              <Badge
 102                variant="outline"
 103                className="border-primary/40 bg-primary/10 text-foreground"
 104              >
 105                <SparklesIcon />
 106                Pro
 107              </Badge>
 108            </h2>
 109            <p className="mt-1.5 text-sm text-muted-foreground">
 110              Unlock advanced features and help fund Alby's open-source work.
 111            </p>
 112  
 113            <div className="mt-5 flex items-baseline gap-1.5">
 114              <span className="text-4xl font-semibold tracking-tight tabular-nums">
 115                $3
 116              </span>
 117              <span className="text-sm text-muted-foreground">/ month</span>
 118            </div>
 119            <p className="mt-1 text-xs text-muted-foreground">
 120              Billed $36 yearly · bitcoin or credit card · cancel anytime
 121            </p>
 122          </div>
 123  
 124          <Separator />
 125  
 126          <ul className="grid gap-2.5 text-sm">
 127            {[
 128              "Unlimited sub-wallets",
 129              info.backendType === "LDK" && "Encrypted remote backups",
 130              "Custom lightning address",
 131              "Export transactions",
 132              "Email notifications",
 133              "Priority support",
 134            ]
 135              .filter((b): b is string => Boolean(b))
 136              .map((benefit) => (
 137                <li key={benefit} className="flex items-center gap-2.5">
 138                  <span className="flex size-4.5 shrink-0 items-center justify-center rounded-full bg-primary/15">
 139                    <CheckIcon
 140                      className="size-3 text-foreground/70"
 141                      strokeWidth={2.5}
 142                    />
 143                  </span>
 144                  <span>{benefit}</span>
 145                </li>
 146              ))}
 147          </ul>
 148  
 149          <div className="mt-2 space-y-2.5">
 150            <ExternalLinkButton
 151              size="lg"
 152              className="w-full"
 153              to="https://www.getalby.com/subscription/pro"
 154            >
 155              Upgrade to Pro
 156            </ExternalLinkButton>
 157            <p className="text-center text-xs text-muted-foreground">
 158              30-day money-back guarantee
 159            </p>
 160          </div>
 161        </DialogContent>
 162      </Dialog>
 163    );
 164  };
 165