import { CodeIcon, HandCoinsIcon, PlusCircleIcon, RefreshCwIcon, SparklesIcon, } from "lucide-react"; import React from "react"; import { useNavigate } from "react-router"; import { toast } from "sonner"; import AppHeader from "src/components/AppHeader"; import ExternalLink from "src/components/ExternalLink"; import { Button } from "src/components/ui/button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "src/components/ui/card"; import { LoadingButton } from "src/components/ui/custom/loading-button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "src/components/ui/dialog"; import { Input } from "src/components/ui/input"; import { Label } from "src/components/ui/label"; import { UpgradeDialog } from "src/components/UpgradeDialog"; import { SUPPORT_ALBY_CONNECTION_NAME, SUPPORT_ALBY_LIGHTNING_ADDRESS, } from "src/constants"; import { useInfo } from "src/hooks/useInfo"; import { createApp } from "src/requests/createApp"; import { CreateAppRequest, UpdateAppRequest } from "src/types"; import { formatBitcoinAmount } from "src/utils/bitcoinFormatting"; import { handleRequestError } from "src/utils/handleRequestError"; import { request } from "src/utils/request"; function SupportAlby() { const navigate = useNavigate(); const { data: info } = useInfo(); const [amountSat, setAmountSat] = React.useState(""); const [senderName, setSenderName] = React.useState(""); const [isSubmitting, setSubmitting] = React.useState(false); const [open, setOpen] = React.useState(false); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!info) { return; } setSubmitting(true); try { const parsedAmountSat = Number(amountSat); if (!Number.isInteger(parsedAmountSat) || parsedAmountSat < 1) { throw new Error("Invalid amount"); } if (parsedAmountSat < 1000) { toast.error("Amount too low", { description: `Minimum payment is ${formatBitcoinAmount( 1_000 * 1000, info.bitcoinDisplayFormat )}`, }); return; } // TODO: extract below code as is duplicated with ZapPlanner // with fee reserve of max(1% or 10 sats) + 30% to avoid nwc_budget_warning (see transactions service) const maxAmountSat = Math.floor((parsedAmountSat * 1.01 + 10) * 1.3); const isolated = false; const createAppRequest: CreateAppRequest = { name: SUPPORT_ALBY_CONNECTION_NAME, scopes: ["pay_invoice"], budgetRenewal: "monthly", maxAmountSat, isolated, metadata: { app_store_app_id: "zapplanner", recipient_lightning_address: SUPPORT_ALBY_LIGHTNING_ADDRESS, }, }; const createAppResponse = await createApp(createAppRequest); // TODO: proxy through hub backend and remove CSRF exceptions for zapplanner.albylabs.com const createSubscriptionResponse = await fetch( "https://zapplanner.albylabs.com/api/subscriptions", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ recipientLightningAddress: SUPPORT_ALBY_LIGHTNING_ADDRESS, amount: parsedAmountSat, message: "ZapPlanner payment from Alby Hub", payerData: JSON.stringify({ ...(senderName ? { name: senderName } : {}), }), nostrWalletConnectUrl: createAppResponse.pairingUri, cronExpression: "0 0 1 * *", // at the start of each month }), } ); if (!createSubscriptionResponse.ok) { throw new Error( "Failed to create subscription: " + createSubscriptionResponse.status ); } const { subscriptionId } = await createSubscriptionResponse.json(); if (!subscriptionId) { throw new Error("no subscription ID in create subscription response"); } // add the ZapPlanner subscription ID to the app metadata // Only send metadata since that's the only thing changing const updateAppRequest: UpdateAppRequest = { metadata: { ...createAppRequest.metadata, zapplanner_subscription_id: subscriptionId, }, }; await request(`/api/apps/${createAppResponse.pairingPublicKey}`, { method: "PATCH", headers: { "Content-Type": "application/json", }, body: JSON.stringify(updateAppRequest), }); toast("Thank you for becoming a supporter", { description: "Payment will be made at the start of each month", }); navigate("/"); } catch (error) { handleRequestError("Failed to create app", error); } finally { setSubmitting(false); } }; return ( <>

Become a Supporter

Upgrade to Pro Upgrade your Alby Account to Pro for a small fee and enjoy additional perks that come with it! Donate to Alby Hub development Set up a recurring value4value payment to support the development of Alby Hub, Alby Go, and the NWC ecosystem.
Become a Supporter A new app connection will be established to facilitate monthly payments to Alby. You can cancel it anytime through the connections page.
setAmountSat(e.target.value)} />
setSenderName(e.target.value)} placeholder={`Nickname, npub, @twitter, etc.`} />
Complete Setup

Why Your Contribution Is Important

); } export default SupportAlby;