SupportAlby.tsx raw

   1  import {
   2    CodeIcon,
   3    HandCoinsIcon,
   4    PlusCircleIcon,
   5    RefreshCwIcon,
   6    SparklesIcon,
   7  } from "lucide-react";
   8  import React from "react";
   9  import { useNavigate } from "react-router";
  10  import { toast } from "sonner";
  11  import AppHeader from "src/components/AppHeader";
  12  import ExternalLink from "src/components/ExternalLink";
  13  import { Button } from "src/components/ui/button";
  14  import {
  15    Card,
  16    CardContent,
  17    CardDescription,
  18    CardFooter,
  19    CardHeader,
  20    CardTitle,
  21  } from "src/components/ui/card";
  22  import { LoadingButton } from "src/components/ui/custom/loading-button";
  23  import {
  24    Dialog,
  25    DialogContent,
  26    DialogDescription,
  27    DialogFooter,
  28    DialogHeader,
  29    DialogTitle,
  30    DialogTrigger,
  31  } from "src/components/ui/dialog";
  32  import { Input } from "src/components/ui/input";
  33  import { Label } from "src/components/ui/label";
  34  import { UpgradeDialog } from "src/components/UpgradeDialog";
  35  import {
  36    SUPPORT_ALBY_CONNECTION_NAME,
  37    SUPPORT_ALBY_LIGHTNING_ADDRESS,
  38  } from "src/constants";
  39  import { useInfo } from "src/hooks/useInfo";
  40  import { createApp } from "src/requests/createApp";
  41  import { CreateAppRequest, UpdateAppRequest } from "src/types";
  42  import { formatBitcoinAmount } from "src/utils/bitcoinFormatting";
  43  import { handleRequestError } from "src/utils/handleRequestError";
  44  import { request } from "src/utils/request";
  45  
  46  function SupportAlby() {
  47    const navigate = useNavigate();
  48    const { data: info } = useInfo();
  49  
  50    const [amountSat, setAmountSat] = React.useState("");
  51    const [senderName, setSenderName] = React.useState("");
  52    const [isSubmitting, setSubmitting] = React.useState(false);
  53    const [open, setOpen] = React.useState(false);
  54  
  55    const handleSubmit = async (e: React.FormEvent) => {
  56      e.preventDefault();
  57  
  58      if (!info) {
  59        return;
  60      }
  61  
  62      setSubmitting(true);
  63      try {
  64        const parsedAmountSat = Number(amountSat);
  65        if (!Number.isInteger(parsedAmountSat) || parsedAmountSat < 1) {
  66          throw new Error("Invalid amount");
  67        }
  68  
  69        if (parsedAmountSat < 1000) {
  70          toast.error("Amount too low", {
  71            description: `Minimum payment is ${formatBitcoinAmount(
  72              1_000 * 1000,
  73              info.bitcoinDisplayFormat
  74            )}`,
  75          });
  76          return;
  77        }
  78  
  79        // TODO: extract below code as is duplicated with ZapPlanner
  80        // with fee reserve of max(1% or 10 sats) + 30% to avoid nwc_budget_warning (see transactions service)
  81        const maxAmountSat = Math.floor((parsedAmountSat * 1.01 + 10) * 1.3);
  82        const isolated = false;
  83  
  84        const createAppRequest: CreateAppRequest = {
  85          name: SUPPORT_ALBY_CONNECTION_NAME,
  86          scopes: ["pay_invoice"],
  87          budgetRenewal: "monthly",
  88          maxAmountSat,
  89          isolated,
  90          metadata: {
  91            app_store_app_id: "zapplanner",
  92            recipient_lightning_address: SUPPORT_ALBY_LIGHTNING_ADDRESS,
  93          },
  94        };
  95  
  96        const createAppResponse = await createApp(createAppRequest);
  97  
  98        // TODO: proxy through hub backend and remove CSRF exceptions for zapplanner.albylabs.com
  99        const createSubscriptionResponse = await fetch(
 100          "https://zapplanner.albylabs.com/api/subscriptions",
 101          {
 102            method: "POST",
 103            headers: {
 104              "Content-Type": "application/json",
 105            },
 106            body: JSON.stringify({
 107              recipientLightningAddress: SUPPORT_ALBY_LIGHTNING_ADDRESS,
 108              amount: parsedAmountSat,
 109              message: "ZapPlanner payment from Alby Hub",
 110              payerData: JSON.stringify({
 111                ...(senderName ? { name: senderName } : {}),
 112              }),
 113              nostrWalletConnectUrl: createAppResponse.pairingUri,
 114              cronExpression: "0 0 1 * *", // at the start of each month
 115            }),
 116          }
 117        );
 118        if (!createSubscriptionResponse.ok) {
 119          throw new Error(
 120            "Failed to create subscription: " + createSubscriptionResponse.status
 121          );
 122        }
 123  
 124        const { subscriptionId } = await createSubscriptionResponse.json();
 125        if (!subscriptionId) {
 126          throw new Error("no subscription ID in create subscription response");
 127        }
 128  
 129        // add the ZapPlanner subscription ID to the app metadata
 130        // Only send metadata since that's the only thing changing
 131        const updateAppRequest: UpdateAppRequest = {
 132          metadata: {
 133            ...createAppRequest.metadata,
 134            zapplanner_subscription_id: subscriptionId,
 135          },
 136        };
 137  
 138        await request(`/api/apps/${createAppResponse.pairingPublicKey}`, {
 139          method: "PATCH",
 140          headers: {
 141            "Content-Type": "application/json",
 142          },
 143          body: JSON.stringify(updateAppRequest),
 144        });
 145  
 146        toast("Thank you for becoming a supporter", {
 147          description: "Payment will be made at the start of each month",
 148        });
 149  
 150        navigate("/");
 151      } catch (error) {
 152        handleRequestError("Failed to create app", error);
 153      } finally {
 154        setSubmitting(false);
 155      }
 156    };
 157  
 158    return (
 159      <>
 160        <AppHeader
 161          title="Support Alby Hub"
 162          pageTitle="Support Alby"
 163          description="We are committed to elevating the Bitcoin ecosystem by offering reliable, efficient, and user-friendly software solutions for seamless transactions. With your help, we can keep pushing boundaries and evolving Alby Hub into something extraordinary."
 164        />
 165        <h2 className="text-2xl font-semibold">Become a Supporter</h2>
 166        <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
 167          <Card className="flex flex-col">
 168            <CardHeader className="grow">
 169              <CardTitle>Upgrade to Pro</CardTitle>
 170              <CardDescription>
 171                Upgrade your Alby Account to Pro for a small fee and enjoy
 172                additional perks that come with it!
 173              </CardDescription>
 174            </CardHeader>
 175            <CardFooter className="flex justify-end">
 176              <UpgradeDialog>
 177                <Button>
 178                  <SparklesIcon />
 179                  Upgrade to Pro
 180                </Button>
 181              </UpgradeDialog>
 182            </CardFooter>
 183          </Card>
 184          <Card className="flex flex-col">
 185            <CardHeader className="grow">
 186              <CardTitle>Donate to Alby Hub development</CardTitle>
 187              <CardDescription>
 188                Set up a recurring value4value payment to support the development
 189                of Alby Hub, Alby Go, and the NWC ecosystem.
 190              </CardDescription>
 191            </CardHeader>
 192            <CardContent className="flex-1 grow" />
 193            <CardFooter className="flex justify-end">
 194              <Dialog open={open} onOpenChange={setOpen}>
 195                <div className="flex flex-col items-center justify-center gap-2">
 196                  <DialogTrigger asChild>
 197                    <Button>
 198                      <HandCoinsIcon />
 199                      Setup Donation
 200                    </Button>
 201                  </DialogTrigger>
 202                </div>
 203                <DialogContent>
 204                  <form onSubmit={handleSubmit}>
 205                    <DialogHeader>
 206                      <DialogTitle>Become a Supporter</DialogTitle>
 207                      <DialogDescription>
 208                        A new app connection will be established to facilitate
 209                        monthly payments to Alby. You can cancel it anytime
 210                        through the connections page.
 211                      </DialogDescription>
 212                    </DialogHeader>
 213                    <div className="flex flex-col gap-3 my-5">
 214                      <div className="grid grid-cols-4 gap-4">
 215                        <Label htmlFor="amount" className="text-right mt-2">
 216                          Amount <br></br>
 217                          <span className="font-normal text-muted-foreground">
 218                            (sats / month)
 219                          </span>
 220                        </Label>
 221                        <div className="col-span-3">
 222                          <Input
 223                            id="amount"
 224                            value={amountSat}
 225                            required
 226                            onChange={(e) => setAmountSat(e.target.value)}
 227                          />
 228                          <div className="grid grid-cols-3 gap-1 mt-1">
 229                            <Button
 230                              type="button"
 231                              variant="outline"
 232                              onClick={() => setAmountSat("3000")}
 233                            >
 234                              🙏 3000
 235                            </Button>
 236                            <Button
 237                              type="button"
 238                              variant="outline"
 239                              onClick={() => setAmountSat("6000")}
 240                            >
 241                              💪 6000
 242                            </Button>
 243                            <Button
 244                              type="button"
 245                              variant="outline"
 246                              onClick={() => setAmountSat("10000")}
 247                            >
 248                              ✨ 10000
 249                            </Button>
 250                          </div>
 251                        </div>
 252                      </div>
 253                      <div className="grid grid-cols-4 items-center gap-4">
 254                        <Label htmlFor="comment" className="text-right">
 255                          Name{" "}
 256                          <span className="font-normal text-muted-foreground">
 257                            (optional)
 258                          </span>
 259                        </Label>
 260                        <div className="col-span-3">
 261                          <Input
 262                            id="sender-name"
 263                            value={senderName}
 264                            onChange={(e) => setSenderName(e.target.value)}
 265                            placeholder={`Nickname, npub, @twitter, etc.`}
 266                          />
 267                        </div>
 268                      </div>
 269                    </div>
 270  
 271                    <DialogFooter>
 272                      <LoadingButton
 273                        type="submit"
 274                        disabled={!!isSubmitting}
 275                        loading={isSubmitting}
 276                      >
 277                        Complete Setup
 278                      </LoadingButton>
 279                    </DialogFooter>
 280                  </form>
 281                </DialogContent>
 282              </Dialog>
 283            </CardFooter>
 284          </Card>
 285        </div>
 286        <div className="mt-4">
 287          <h2 className="text-2xl font-semibold mb-4">
 288            Why Your Contribution Is Important
 289          </h2>
 290          <ul className="flex flex-col gap-5">
 291            <li className="flex flex-col">
 292              <div className="flex flex-row items-center">
 293                <PlusCircleIcon className="size-4 mr-2" />
 294                Unlock New Features
 295              </div>
 296              <div className="text-muted-foreground text-sm">
 297                Your support empowers us to design and implement cutting-edge{" "}
 298                <ExternalLink
 299                  className="underline"
 300                  to="https://github.com/getAlby/hub/issues"
 301                >
 302                  features
 303                </ExternalLink>{" "}
 304                that enhance your experience and keep us at the forefront of
 305                technology.
 306              </div>
 307            </li>
 308            <li className="flex flex-col ">
 309              <div className="flex flex-row items-center">
 310                <RefreshCwIcon className="size-4 mr-2" />
 311                Ensure Continuous Improvement
 312              </div>
 313              <div className="text-muted-foreground text-sm">
 314                With your contributions, we can provide{" "}
 315                <ExternalLink
 316                  className="underline"
 317                  to="https://github.com/getAlby/hub/releases"
 318                >
 319                  regular updates
 320                </ExternalLink>{" "}
 321                and ongoing maintenance, ensuring everything runs smoothly and
 322                efficiently for all users.
 323              </div>
 324            </li>
 325            <li className="flex flex-col ">
 326              <div className="flex flex-row items-center">
 327                <CodeIcon className="size-4 mr-2" />
 328                Support Open-Source Freedom
 329              </div>
 330              <div className="text-muted-foreground text-sm">
 331                Your support helps us keep Alby Hub true to the principles of{" "}
 332                <ExternalLink
 333                  className="underline"
 334                  to="https://github.com/getAlby/hub/blob/master/LICENSE"
 335                >
 336                  free and open-source software
 337                </ExternalLink>{" "}
 338                and remains accessible for everyone to use, modify and improve.
 339              </div>
 340            </li>
 341          </ul>
 342        </div>
 343      </>
 344    );
 345  }
 346  
 347  export default SupportAlby;
 348