mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
feat: support alby (#1059)
* feat: draft page * fix: sidebar card content font size * fix: onboarding item * fix: copy * fix: spacing * fix: hub lightning address * fix: exclude radix-ui imports * feat: progress on alby support page * fix: update copy, preset amounts * fix: replace success screen with toast for now * fix: extract constants, address review feedback * fix: use constant * chore: minor spacing improvements * fix: add required on support alby amount field --------- Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
This commit is contained in:
parent
99d8c8fb5a
commit
a6ff88f5ca
8 changed files with 365 additions and 3 deletions
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
|
@ -5,6 +5,7 @@
|
|||
},
|
||||
"editor.formatOnSave": true,
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"typescript.preferences.autoImportFileExcludePatterns": ["@radix-ui"],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "always"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,9 @@ function SidebarHintCard({
|
|||
<CardTitle>{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 pt-0">
|
||||
<div className="text-muted-foreground mb-4">{description}</div>
|
||||
<div className="text-muted-foreground mb-4 text-sm">
|
||||
{description}
|
||||
</div>
|
||||
<Link to={buttonLink}>
|
||||
<Button size="sm" className="w-full">
|
||||
{buttonText}
|
||||
|
|
|
|||
|
|
@ -8,3 +8,6 @@ export const localStorageKeys = {
|
|||
export const ONCHAIN_DUST_SATS = 1000;
|
||||
export const ALBY_HIDE_HOSTED_BALANCE_BELOW = 100;
|
||||
export const ALBY_MIN_HOSTED_BALANCE_FOR_FIRST_CHANNEL = 30_000;
|
||||
|
||||
export const SUPPORT_ALBY_CONNECTION_NAME = `ZapPlanner - Alby Hub`;
|
||||
export const SUPPORT_ALBY_LIGHTNING_ADDRESS = "hub@getalby.com";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// src/hooks/useOnboardingData.ts
|
||||
|
||||
import { SUPPORT_ALBY_CONNECTION_NAME } from "src/constants";
|
||||
import { useAlbyBalance } from "src/hooks/useAlbyBalance";
|
||||
import { useAlbyMe } from "src/hooks/useAlbyMe";
|
||||
import { useApps } from "src/hooks/useApps";
|
||||
|
|
@ -56,6 +57,9 @@ export const useOnboardingData = (): UseOnboardingDataResponse => {
|
|||
const hasCustomApp =
|
||||
apps && apps.find((x) => x.name !== "getalby.com") !== undefined;
|
||||
const hasTransaction = transactions.length > 0;
|
||||
const hasSetupSupportPayment =
|
||||
apps &&
|
||||
apps.find((x) => x.name === SUPPORT_ALBY_CONNECTION_NAME) !== undefined;
|
||||
|
||||
const checklistItems: Omit<ChecklistItem, "disabled">[] = [
|
||||
{
|
||||
|
|
@ -101,6 +105,17 @@ export const useOnboardingData = (): UseOnboardingDataResponse => {
|
|||
},
|
||||
]
|
||||
: []),
|
||||
...(!info.oauthRedirect
|
||||
? [
|
||||
{
|
||||
title: "Support Alby Hub",
|
||||
description:
|
||||
"Setup a recurring payment to support the development of Alby Hub",
|
||||
checked: hasSetupSupportPayment,
|
||||
to: "/support-alby",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const nextStep = checklistItems.find((x) => !x.checked);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import Start from "src/screens/Start";
|
|||
import Unlock from "src/screens/Unlock";
|
||||
import { Welcome } from "src/screens/Welcome";
|
||||
import AlbyAuthRedirect from "src/screens/alby/AlbyAuthRedirect";
|
||||
import SupportAlby from "src/screens/alby/SupportAlby";
|
||||
import AppCreated from "src/screens/apps/AppCreated";
|
||||
import AppList from "src/screens/apps/AppList";
|
||||
import NewApp from "src/screens/apps/NewApp";
|
||||
|
|
@ -357,6 +358,10 @@ const routes = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "support-alby",
|
||||
element: <SupportAlby />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
335
frontend/src/screens/alby/SupportAlby.tsx
Normal file
335
frontend/src/screens/alby/SupportAlby.tsx
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
import { Code, PlusCircle, RefreshCw } from "lucide-react";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import ExternalLink from "src/components/ExternalLink";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "src/components/ui/alert-dialog";
|
||||
import { Button, LinkButton } from "src/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "src/components/ui/card";
|
||||
import { Input } from "src/components/ui/input";
|
||||
import { Label } from "src/components/ui/label";
|
||||
import { LoadingButton } from "src/components/ui/loading-button";
|
||||
import { useToast } from "src/components/ui/use-toast";
|
||||
import {
|
||||
SUPPORT_ALBY_CONNECTION_NAME,
|
||||
SUPPORT_ALBY_LIGHTNING_ADDRESS,
|
||||
} from "src/constants";
|
||||
import { useApps } from "src/hooks/useApps";
|
||||
import { createApp } from "src/requests/createApp";
|
||||
import { CreateAppRequest, UpdateAppRequest } from "src/types";
|
||||
import { handleRequestError } from "src/utils/handleRequestError";
|
||||
import { request } from "src/utils/request";
|
||||
|
||||
function SupportAlby() {
|
||||
const { data: apps } = useApps();
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [amount, setAmount] = 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();
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
if (
|
||||
apps?.some(
|
||||
(existingApp) => existingApp.name === SUPPORT_ALBY_CONNECTION_NAME
|
||||
)
|
||||
) {
|
||||
throw new Error("A connection with the same name already exists.");
|
||||
}
|
||||
|
||||
const parsedAmount = parseInt(amount);
|
||||
if (isNaN(parsedAmount) || parsedAmount < 1) {
|
||||
throw new Error("Invalid amount");
|
||||
}
|
||||
|
||||
if (+amount < 1000) {
|
||||
toast({
|
||||
title: "Amount too low",
|
||||
description: "Minimum payment is 1000 sats",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const maxAmount = Math.floor(parsedAmount * 1.01) + 10; // with fee reserve
|
||||
const isolated = false;
|
||||
|
||||
const createAppRequest: CreateAppRequest = {
|
||||
name: SUPPORT_ALBY_CONNECTION_NAME,
|
||||
scopes: ["pay_invoice"],
|
||||
budgetRenewal: "monthly",
|
||||
maxAmount,
|
||||
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: parsedAmount,
|
||||
message: "ZapPlanner payment from Alby Hub",
|
||||
payerData: JSON.stringify({
|
||||
...(senderName ? { name: senderName } : {}),
|
||||
}),
|
||||
nostrWalletConnectUrl: createAppResponse.pairingUri,
|
||||
sleepDuration: "31 days",
|
||||
}),
|
||||
}
|
||||
);
|
||||
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
|
||||
const updateAppRequest: UpdateAppRequest = {
|
||||
name: createAppRequest.name,
|
||||
scopes: createAppRequest.scopes,
|
||||
budgetRenewal: createAppRequest.budgetRenewal!,
|
||||
expiresAt: createAppRequest.expiresAt,
|
||||
maxAmount,
|
||||
isolated,
|
||||
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({
|
||||
title: "Thank you for becoming a supporter",
|
||||
description: "The first payment is scheduled immediately.",
|
||||
});
|
||||
|
||||
navigate("/");
|
||||
} catch (error) {
|
||||
handleRequestError(toast, "Failed to create app", error);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full w-full max-w-screen-sm mx-auto flex flex-col justify-center">
|
||||
<div className="flex flex-col items-center justify-center gap-6">
|
||||
<section className="text-center">
|
||||
<h2 className="text-3xl font-semibold mb-4 max-sm:mt-8">
|
||||
✨ Your Support Matters
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-balance">
|
||||
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 even more
|
||||
extraordinary.
|
||||
</p>
|
||||
</section>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Why Your Contribution Is Important</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="flex flex-col gap-5">
|
||||
<li className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<PlusCircle className="w-4 h-4 mr-2" />
|
||||
Unlock New Features
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
Your support empowers us to design and implement
|
||||
cutting-edge{" "}
|
||||
<ExternalLink
|
||||
className="underline"
|
||||
to="https://github.com/getAlby/hub/issues"
|
||||
>
|
||||
features
|
||||
</ExternalLink>{" "}
|
||||
that enhance your experience and keep us at the forefront of
|
||||
technology.
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex flex-col ">
|
||||
<div className="flex flex-row items-center">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Ensure Continuous Improvement
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
With your contributions, we can provide{" "}
|
||||
<ExternalLink
|
||||
className="underline"
|
||||
to="https://github.com/getAlby/hub/releases"
|
||||
>
|
||||
regular updates
|
||||
</ExternalLink>{" "}
|
||||
and ongoing maintenance, ensuring everything runs smoothly
|
||||
and efficiently for all users.
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex flex-col ">
|
||||
<div className="flex flex-row items-center">
|
||||
<Code className="w-4 h-4 mr-2" />
|
||||
Support Open-Source Freedom
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
Your support helps us keep Alby Hub true to the principles
|
||||
of{" "}
|
||||
<ExternalLink
|
||||
className="underline"
|
||||
to="https://github.com/getAlby/hub/blob/master/LICENSE"
|
||||
>
|
||||
free and open-source software
|
||||
</ExternalLink>{" "}
|
||||
and remains accessible for everyone to use, modify and
|
||||
improve.
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button size="lg">Become a Supporter</Button>
|
||||
</AlertDialogTrigger>
|
||||
<LinkButton
|
||||
size="sm"
|
||||
variant="link"
|
||||
to="/"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
Maybe later
|
||||
</LinkButton>
|
||||
</div>
|
||||
<AlertDialogContent>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Become a Supporter</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
A new app connection will be established to facilitate
|
||||
monthly payments to Alby. You can cancel it anytime through
|
||||
the connections page.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-3 my-5">
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Label htmlFor="amount" className="text-right mt-2">
|
||||
Amount <br></br>
|
||||
<span className="font-normal text-muted-foreground">
|
||||
(sats / month)
|
||||
</span>
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
<Input
|
||||
id="amount"
|
||||
value={amount}
|
||||
required
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
/>
|
||||
<div className="grid grid-cols-3 gap-1 mt-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setAmount("3000")}
|
||||
>
|
||||
🙏 3000
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setAmount("6000")}
|
||||
>
|
||||
💪 6000
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setAmount("10000")}
|
||||
>
|
||||
✨ 10000
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="comment" className="text-right">
|
||||
Name{" "}
|
||||
<span className="font-normal text-muted-foreground">
|
||||
(optional)
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="sender-name"
|
||||
value={senderName}
|
||||
onChange={(e) => setSenderName(e.target.value)}
|
||||
placeholder={`Nickname, npub, @twitter, etc.`}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
disabled={!!isSubmitting}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Complete Setup
|
||||
</LoadingButton>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SupportAlby;
|
||||
|
|
@ -34,6 +34,7 @@ import { Input } from "src/components/ui/input";
|
|||
import { Label } from "src/components/ui/label";
|
||||
import { LoadingButton } from "src/components/ui/loading-button";
|
||||
import { Textarea } from "src/components/ui/textarea";
|
||||
import { SUPPORT_ALBY_LIGHTNING_ADDRESS } from "src/constants";
|
||||
import { request } from "src/utils/request";
|
||||
|
||||
type Recipient = {
|
||||
|
|
@ -49,7 +50,7 @@ const recipients: Recipient[] = [
|
|||
logo: alby,
|
||||
description:
|
||||
"Support the open-source development of Hub, Go, Lightning Browser Extension, developer tools and open protocols.",
|
||||
lightningAddress: "hello@getalby.com",
|
||||
lightningAddress: SUPPORT_ALBY_LIGHTNING_ADDRESS,
|
||||
},
|
||||
{
|
||||
name: "HRF",
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ function ChecklistItem({
|
|||
</div>
|
||||
</div>
|
||||
{!checked && (
|
||||
<div className="text-muted-foreground text-sm ml-7">{description}</div>
|
||||
<div className="text-muted-foreground text-sm mx-7">{description}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue