diff --git a/.vscode/settings.json b/.vscode/settings.json index d979d797..84a94474 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ }, "editor.formatOnSave": true, "typescript.preferences.importModuleSpecifier": "non-relative", + "typescript.preferences.autoImportFileExcludePatterns": ["@radix-ui"], "editor.codeActionsOnSave": { "source.organizeImports": "always" } diff --git a/frontend/src/components/SidebarHint.tsx b/frontend/src/components/SidebarHint.tsx index 059ec548..d7afa81b 100644 --- a/frontend/src/components/SidebarHint.tsx +++ b/frontend/src/components/SidebarHint.tsx @@ -91,7 +91,9 @@ function SidebarHintCard({ {title} - {description} + + {description} + {buttonText} diff --git a/frontend/src/constants.ts b/frontend/src/constants.ts index 4c3f4466..e32df61a 100644 --- a/frontend/src/constants.ts +++ b/frontend/src/constants.ts @@ -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"; diff --git a/frontend/src/hooks/useOnboardingData.ts b/frontend/src/hooks/useOnboardingData.ts index 9c771b67..798ab8de 100644 --- a/frontend/src/hooks/useOnboardingData.ts +++ b/frontend/src/hooks/useOnboardingData.ts @@ -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[] = [ { @@ -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); diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index acd647ec..945c1ac4 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -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: , + }, ], }, { diff --git a/frontend/src/screens/alby/SupportAlby.tsx b/frontend/src/screens/alby/SupportAlby.tsx new file mode 100644 index 00000000..f47cf8ec --- /dev/null +++ b/frontend/src/screens/alby/SupportAlby.tsx @@ -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 ( + <> + + + + + ✨ Your Support Matters + + + 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. + + + + + Why Your Contribution Is Important + + + + + + + Unlock New Features + + + Your support empowers us to design and implement + cutting-edge{" "} + + features + {" "} + that enhance your experience and keep us at the forefront of + technology. + + + + + + Ensure Continuous Improvement + + + With your contributions, we can provide{" "} + + regular updates + {" "} + and ongoing maintenance, ensuring everything runs smoothly + and efficiently for all users. + + + + + + Support Open-Source Freedom + + + Your support helps us keep Alby Hub true to the principles + of{" "} + + free and open-source software + {" "} + and remains accessible for everyone to use, modify and + improve. + + + + + + + + + Become a Supporter + + + Maybe later + + + + + + 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. + + + + + + + Amount + + (sats / month) + + + + setAmount(e.target.value)} + /> + + setAmount("3000")} + > + 🙏 3000 + + setAmount("6000")} + > + 💪 6000 + + setAmount("10000")} + > + ✨ 10000 + + + + + + + Name{" "} + + (optional) + + + setSenderName(e.target.value)} + placeholder={`Nickname, npub, @twitter, etc.`} + className="col-span-3" + /> + + + + + Cancel + + Complete Setup + + + + + + + + > + ); +} + +export default SupportAlby; diff --git a/frontend/src/screens/internal-apps/ZapPlanner.tsx b/frontend/src/screens/internal-apps/ZapPlanner.tsx index 55f309c2..d07474e1 100644 --- a/frontend/src/screens/internal-apps/ZapPlanner.tsx +++ b/frontend/src/screens/internal-apps/ZapPlanner.tsx @@ -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", diff --git a/frontend/src/screens/wallet/OnboardingChecklist.tsx b/frontend/src/screens/wallet/OnboardingChecklist.tsx index b27d7636..8750bf83 100644 --- a/frontend/src/screens/wallet/OnboardingChecklist.tsx +++ b/frontend/src/screens/wallet/OnboardingChecklist.tsx @@ -91,7 +91,7 @@ function ChecklistItem({ {!checked && ( - {description} + {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 even more + extraordinary. +