Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: appcard improvements & linking account #235

Merged
merged 7 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions alby/alby_oauth_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
nip47 "github.com/getAlby/hub/nip47/models"
"github.com/getAlby/hub/nip47/permissions"
"github.com/getAlby/hub/service/keys"
)

Expand Down Expand Up @@ -262,7 +262,7 @@ func (svc *albyOAuthService) DrainSharedWallet(ctx context.Context, lnClient lnc

amountSat := int64(math.Floor(
balanceSat- // Alby shared node balance in sats
(balanceSat*(8/1000))- // Alby service fee (0.8%)
(balanceSat*(8.0/1000.0))- // Alby service fee (0.8%)
reneaaron marked this conversation as resolved.
Show resolved Hide resolved
(balanceSat*0.01))) - // Maximum potential routing fees (1%)
10 // Alby fee reserve (10 sats)

Expand Down Expand Up @@ -379,20 +379,30 @@ func (svc *albyOAuthService) GetAuthUrl() string {
return svc.oauthConf.AuthCodeURL("unused")
}

func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient.LNClient) error {
func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient.LNClient, budget uint64, renewal string) error {
connectionPubkey, err := svc.createAlbyAccountNWCNode(ctx)
if err != nil {
logger.Logger.WithError(err).Error("Failed to create alby account nwc node")
return err
}

scopes, err := permissions.RequestMethodsToScopes(lnClient.GetSupportedNIP47Methods())
if err != nil {
logger.Logger.WithError(err).Error("Failed to get scopes from LNClient request methods")
return err
}
notificationTypes := lnClient.GetSupportedNIP47NotificationTypes()
if len(notificationTypes) > 0 {
scopes = append(scopes, permissions.NOTIFICATIONS_SCOPE)
}

app, _, err := db.NewDBService(svc.db, svc.eventPublisher).CreateApp(
"getalby.com",
connectionPubkey,
1_000_000,
nip47.BUDGET_RENEWAL_MONTHLY,
budget,
renewal,
nil,
lnClient.GetSupportedNIP47Methods(),
scopes,
)

if err != nil {
Expand Down
7 changes: 6 additions & 1 deletion alby/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type AlbyOAuthService interface {
GetAuthUrl() string
GetUserIdentifier() (string, error)
IsConnected(ctx context.Context) bool
LinkAccount(ctx context.Context, lnClient lnclient.LNClient) error
LinkAccount(ctx context.Context, lnClient lnclient.LNClient, budget uint64, renewal string) error
CallbackHandler(ctx context.Context, code string) error
GetBalance(ctx context.Context) (*AlbyBalance, error)
GetMe(ctx context.Context) (*AlbyMe, error)
Expand All @@ -29,6 +29,11 @@ type AlbyPayRequest struct {
Invoice string `json:"invoice"`
}

type AlbyLinkAccountRequest struct {
Budget uint64 `json:"budget"`
Renewal string `json:"renewal"`
}

type AlbyMeHub struct {
LatestVersion string `json:"latest_version"`
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions frontend/src/components/BudgetAmountSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { budgetOptions } from "src/types";

function BudgetAmountSelect({
value,
onChange,
}: {
value?: number;
onChange: (value: number) => void;
}) {
return (
<div className="grid grid-cols-6 grid-rows-2 md:grid-rows-1 md:grid-cols-6 gap-2 text-xs">
{Object.keys(budgetOptions).map((budget) => {
const amount = budgetOptions[budget];
return (
<div
key={budget}
onClick={() => onChange(amount)}
className={`col-span-2 md:col-span-1 cursor-pointer rounded border-2 ${
value === amount ? "border-primary" : "border-muted"
} text-center py-4`}
>
{budget}
<br />
{amount ? "sats" : "#reckless"}
</div>
);
})}
</div>
);
}

export default BudgetAmountSelect;
38 changes: 38 additions & 0 deletions frontend/src/components/BudgetRenewalSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from "react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "src/components/ui/select";
import { BudgetRenewalType, validBudgetRenewals } from "src/types";

interface BudgetRenewalProps {
value: BudgetRenewalType;
onChange: (value: BudgetRenewalType) => void;
disabled?: boolean;
}

const BudgetRenewalSelect: React.FC<BudgetRenewalProps> = ({
value,
onChange,
disabled,
}) => {
return (
<Select value={value} onValueChange={onChange} disabled={disabled}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder={"placeholder"} />
</SelectTrigger>
<SelectContent>
{validBudgetRenewals.map((renewalOption) => (
<SelectItem key={renewalOption} value={renewalOption}>
{renewalOption.charAt(0).toUpperCase() + renewalOption.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
);
};

export default BudgetRenewalSelect;
63 changes: 9 additions & 54 deletions frontend/src/components/Permissions.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
import { PlusCircle } from "lucide-react";
import React, { useEffect, useState } from "react";
import BudgetAmountSelect from "src/components/BudgetAmountSelect";
import BudgetRenewalSelect from "src/components/BudgetRenewalSelect";
reneaaron marked this conversation as resolved.
Show resolved Hide resolved
import { Button } from "src/components/ui/button";
import { Checkbox } from "src/components/ui/checkbox";
import { Label } from "src/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "src/components/ui/select";
import { useCapabilities } from "src/hooks/useCapabilities";
import { cn } from "src/lib/utils";
import {
AppPermissions,
BudgetRenewalType,
Scope,
budgetOptions,
expiryOptions,
iconMap,
scopeDescriptions,
validBudgetRenewals,
} from "src/types";

interface PermissionsProps {
Expand Down Expand Up @@ -164,55 +157,17 @@ const Permissions: React.FC<PermissionsProps> = ({
{!canEditPermissions ? (
permissions.budgetRenewal
) : (
<Select
<BudgetRenewalSelect
value={permissions.budgetRenewal}
onValueChange={handleBudgetRenewalChange}
onChange={handleBudgetRenewalChange}
disabled={!canEditPermissions}
>
<SelectTrigger className="w-[150px]">
<SelectValue
placeholder={permissions.budgetRenewal}
/>
</SelectTrigger>
<SelectContent>
{validBudgetRenewals.map((renewalOption) => (
<SelectItem
key={renewalOption}
value={renewalOption}
>
{renewalOption.charAt(0).toUpperCase() +
renewalOption.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
/>
)}
</div>
<div
id="budget-allowance-limits"
className="grid grid-cols-6 grid-rows-2 md:grid-rows-1 md:grid-cols-6 gap-2 text-xs"
>
{Object.keys(budgetOptions).map((budget) => {
return (
// replace with something else and then remove dark prefixes
<div
key={budget}
onClick={() =>
handleMaxAmountChange(budgetOptions[budget])
}
className={`col-span-2 md:col-span-1 cursor-pointer rounded border-2 ${
permissions.maxAmount == budgetOptions[budget]
? "border-primary"
: "border-muted"
} text-center py-4 dark:text-white`}
>
{budget}
<br />
{budgetOptions[budget] ? "sats" : "#reckless"}
</div>
);
})}
</div>
<BudgetAmountSelect
value={permissions.maxAmount}
onChange={handleMaxAmountChange}
/>
</>
) : isNewConnection ? (
<>
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/SidebarHint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ function SidebarHint() {
return (
<SidebarHintCard
icon={Link2}
title="Link your Hub"
description="Finish the setup by linking your Alby Account to this hub."
buttonText="Link Hub"
title="Link to your Alby Account"
description="Finish the setup by linking this Hub to your Alby Account."
buttonText="Link now"
buttonLink="/apps"
/>
);
Expand Down
69 changes: 62 additions & 7 deletions frontend/src/components/connections/AlbyConnectionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import {
Link2Icon,
ZapIcon,
} from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom";

import BudgetAmountSelect from "src/components/BudgetAmountSelect";
import BudgetRenewalSelect from "src/components/BudgetRenewalSelect";
import ExternalLink from "src/components/ExternalLink";
import Loading from "src/components/Loading";
import UserAvatar from "src/components/UserAvatar";
Expand All @@ -20,22 +24,36 @@ import {
CardHeader,
CardTitle,
} from "src/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTrigger,
} from "src/components/ui/dialog";
import { Label } from "src/components/ui/label";
import { LoadingButton } from "src/components/ui/loading-button";
import { Separator } from "src/components/ui/separator";
import { useAlbyMe } from "src/hooks/useAlbyMe";
import { LinkStatus, useLinkAccount } from "src/hooks/useLinkAccount";
import { App } from "src/types";
import { App, BudgetRenewalType } from "src/types";
import linkAccountIllustration from "/images/illustrations/link-account.png";

function AlbyConnectionCard({ connection }: { connection?: App }) {
const { data: albyMe } = useAlbyMe();
const { loading, linkStatus, loadingLinkStatus, linkAccount } =
useLinkAccount();

const [maxAmount, setMaxAmount] = useState(1_000_000);
const [budgetRenewal, setBudgetRenewal] =
useState<BudgetRenewalType>("monthly");

return (
<Card>
<CardHeader>
<CardTitle className="relative">
Alby Account
Linked Alby Account
{connection && <AppCardNotice app={connection} />}
</CardTitle>
<CardDescription>
Expand All @@ -47,7 +65,7 @@ function AlbyConnectionCard({ connection }: { connection?: App }) {
<CardContent className="group">
<div className="grid grid-cols-1 xl:grid-cols-2 mt-5 gap-3 items-center relative">
<div className="flex flex-col gap-4">
<div className="flex flex-row gap-4 ">
<div className="flex flex-row gap-4">
<UserAvatar className="h-14 w-14" />
<div className="flex flex-col justify-center">
<div className="text-xl font-semibold">
Expand All @@ -62,10 +80,47 @@ function AlbyConnectionCard({ connection }: { connection?: App }) {
<div className="flex flex-col sm:flex-row gap-3 sm:items-center">
{loadingLinkStatus && <Loading />}
{!connection || linkStatus === LinkStatus.SharedNode ? (
<LoadingButton onClick={linkAccount} loading={loading}>
{!loading && <Link2Icon className="w-4 h-4 mr-2" />}
Link your Alby Account
</LoadingButton>
<Dialog>
<DialogTrigger asChild>
<LoadingButton loading={loading}>
{!loading && <Link2Icon className="w-4 h-4 mr-2" />}
Link your Alby Account
</LoadingButton>
</DialogTrigger>
<DialogContent>
<DialogHeader>Link to Alby Account</DialogHeader>
<DialogDescription className="flex flex-col gap-4">
After you link your account, your lightning address and
every app you access through your Alby Account will handle
payments via the Hub.
<img
src={linkAccountIllustration}
className="w-80 mx-auto"
/>
You can add a budget that will restrict how much can be
spent from the Hub with your Alby Account.
</DialogDescription>
<div className="grid gap-1.5">
<Label>Budget renewal</Label>
<BudgetRenewalSelect
value={budgetRenewal}
onChange={setBudgetRenewal}
/>
</div>
<BudgetAmountSelect
value={maxAmount}
onChange={setMaxAmount}
/>
<DialogFooter>
<LoadingButton
onClick={() => linkAccount(maxAmount, budgetRenewal)}
loading={loading}
>
Link to Alby Account
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
) : linkStatus === LinkStatus.ThisNode ? (
<Button
variant="positive"
Expand Down
Loading
Loading