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 (
<>