Skip to content

Commit 060087b

Browse files
reneaaronrolznz
andauthored
feat: appcard improvements & linking account (#235)
* feat: app card improvements * fix: copy sidebar * feat: frontend implementation (wip) * fix: configurable budget & renewal * fix: review feedback * fix: scopes for alby account app connection --------- Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
1 parent 40e69a1 commit 060087b

File tree

16 files changed

+272
-126
lines changed

16 files changed

+272
-126
lines changed

alby/alby_oauth_service.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121
"github.com/getAlby/hub/events"
2222
"github.com/getAlby/hub/lnclient"
2323
"github.com/getAlby/hub/logger"
24-
nip47 "github.com/getAlby/hub/nip47/models"
24+
"github.com/getAlby/hub/nip47/permissions"
2525
"github.com/getAlby/hub/service/keys"
2626
)
2727

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

263263
amountSat := int64(math.Floor(
264264
balanceSat- // Alby shared node balance in sats
265-
(balanceSat*(8/1000))- // Alby service fee (0.8%)
265+
(balanceSat*(8.0/1000.0))- // Alby service fee (0.8%)
266266
(balanceSat*0.01))) - // Maximum potential routing fees (1%)
267267
10 // Alby fee reserve (10 sats)
268268

@@ -379,20 +379,30 @@ func (svc *albyOAuthService) GetAuthUrl() string {
379379
return svc.oauthConf.AuthCodeURL("unused")
380380
}
381381

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

389+
scopes, err := permissions.RequestMethodsToScopes(lnClient.GetSupportedNIP47Methods())
390+
if err != nil {
391+
logger.Logger.WithError(err).Error("Failed to get scopes from LNClient request methods")
392+
return err
393+
}
394+
notificationTypes := lnClient.GetSupportedNIP47NotificationTypes()
395+
if len(notificationTypes) > 0 {
396+
scopes = append(scopes, permissions.NOTIFICATIONS_SCOPE)
397+
}
398+
389399
app, _, err := db.NewDBService(svc.db, svc.eventPublisher).CreateApp(
390400
"getalby.com",
391401
connectionPubkey,
392-
1_000_000,
393-
nip47.BUDGET_RENEWAL_MONTHLY,
402+
budget,
403+
renewal,
394404
nil,
395-
lnClient.GetSupportedNIP47Methods(),
405+
scopes,
396406
)
397407

398408
if err != nil {

alby/models.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type AlbyOAuthService interface {
1313
GetAuthUrl() string
1414
GetUserIdentifier() (string, error)
1515
IsConnected(ctx context.Context) bool
16-
LinkAccount(ctx context.Context, lnClient lnclient.LNClient) error
16+
LinkAccount(ctx context.Context, lnClient lnclient.LNClient, budget uint64, renewal string) error
1717
CallbackHandler(ctx context.Context, code string) error
1818
GetBalance(ctx context.Context) (*AlbyBalance, error)
1919
GetMe(ctx context.Context) (*AlbyMe, error)
@@ -29,6 +29,11 @@ type AlbyPayRequest struct {
2929
Invoice string `json:"invoice"`
3030
}
3131

32+
type AlbyLinkAccountRequest struct {
33+
Budget uint64 `json:"budget"`
34+
Renewal string `json:"renewal"`
35+
}
36+
3237
type AlbyMeHub struct {
3338
LatestVersion string `json:"latest_version"`
3439
}
46.8 KB
Loading
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { budgetOptions } from "src/types";
2+
3+
function BudgetAmountSelect({
4+
value,
5+
onChange,
6+
}: {
7+
value?: number;
8+
onChange: (value: number) => void;
9+
}) {
10+
return (
11+
<div className="grid grid-cols-6 grid-rows-2 md:grid-rows-1 md:grid-cols-6 gap-2 text-xs">
12+
{Object.keys(budgetOptions).map((budget) => {
13+
const amount = budgetOptions[budget];
14+
return (
15+
<div
16+
key={budget}
17+
onClick={() => onChange(amount)}
18+
className={`col-span-2 md:col-span-1 cursor-pointer rounded border-2 ${
19+
value === amount ? "border-primary" : "border-muted"
20+
} text-center py-4`}
21+
>
22+
{budget}
23+
<br />
24+
{amount ? "sats" : "#reckless"}
25+
</div>
26+
);
27+
})}
28+
</div>
29+
);
30+
}
31+
32+
export default BudgetAmountSelect;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React from "react";
2+
import {
3+
Select,
4+
SelectContent,
5+
SelectItem,
6+
SelectTrigger,
7+
SelectValue,
8+
} from "src/components/ui/select";
9+
import { BudgetRenewalType, validBudgetRenewals } from "src/types";
10+
11+
interface BudgetRenewalProps {
12+
value: BudgetRenewalType;
13+
onChange: (value: BudgetRenewalType) => void;
14+
disabled?: boolean;
15+
}
16+
17+
const BudgetRenewalSelect: React.FC<BudgetRenewalProps> = ({
18+
value,
19+
onChange,
20+
disabled,
21+
}) => {
22+
return (
23+
<Select value={value} onValueChange={onChange} disabled={disabled}>
24+
<SelectTrigger className="w-[150px]">
25+
<SelectValue placeholder={"placeholder"} />
26+
</SelectTrigger>
27+
<SelectContent>
28+
{validBudgetRenewals.map((renewalOption) => (
29+
<SelectItem key={renewalOption} value={renewalOption}>
30+
{renewalOption.charAt(0).toUpperCase() + renewalOption.slice(1)}
31+
</SelectItem>
32+
))}
33+
</SelectContent>
34+
</Select>
35+
);
36+
};
37+
38+
export default BudgetRenewalSelect;

frontend/src/components/Permissions.tsx

Lines changed: 9 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,19 @@
11
import { PlusCircle } from "lucide-react";
22
import React, { useEffect, useState } from "react";
3+
import BudgetAmountSelect from "src/components/BudgetAmountSelect";
4+
import BudgetRenewalSelect from "src/components/BudgetRenewalSelect";
35
import { Button } from "src/components/ui/button";
46
import { Checkbox } from "src/components/ui/checkbox";
57
import { Label } from "src/components/ui/label";
6-
import {
7-
Select,
8-
SelectContent,
9-
SelectItem,
10-
SelectTrigger,
11-
SelectValue,
12-
} from "src/components/ui/select";
138
import { useCapabilities } from "src/hooks/useCapabilities";
149
import { cn } from "src/lib/utils";
1510
import {
1611
AppPermissions,
1712
BudgetRenewalType,
1813
Scope,
19-
budgetOptions,
2014
expiryOptions,
2115
iconMap,
2216
scopeDescriptions,
23-
validBudgetRenewals,
2417
} from "src/types";
2518

2619
interface PermissionsProps {
@@ -164,55 +157,17 @@ const Permissions: React.FC<PermissionsProps> = ({
164157
{!canEditPermissions ? (
165158
permissions.budgetRenewal
166159
) : (
167-
<Select
160+
<BudgetRenewalSelect
168161
value={permissions.budgetRenewal}
169-
onValueChange={handleBudgetRenewalChange}
162+
onChange={handleBudgetRenewalChange}
170163
disabled={!canEditPermissions}
171-
>
172-
<SelectTrigger className="w-[150px]">
173-
<SelectValue
174-
placeholder={permissions.budgetRenewal}
175-
/>
176-
</SelectTrigger>
177-
<SelectContent>
178-
{validBudgetRenewals.map((renewalOption) => (
179-
<SelectItem
180-
key={renewalOption}
181-
value={renewalOption}
182-
>
183-
{renewalOption.charAt(0).toUpperCase() +
184-
renewalOption.slice(1)}
185-
</SelectItem>
186-
))}
187-
</SelectContent>
188-
</Select>
164+
/>
189165
)}
190166
</div>
191-
<div
192-
id="budget-allowance-limits"
193-
className="grid grid-cols-6 grid-rows-2 md:grid-rows-1 md:grid-cols-6 gap-2 text-xs"
194-
>
195-
{Object.keys(budgetOptions).map((budget) => {
196-
return (
197-
// replace with something else and then remove dark prefixes
198-
<div
199-
key={budget}
200-
onClick={() =>
201-
handleMaxAmountChange(budgetOptions[budget])
202-
}
203-
className={`col-span-2 md:col-span-1 cursor-pointer rounded border-2 ${
204-
permissions.maxAmount == budgetOptions[budget]
205-
? "border-primary"
206-
: "border-muted"
207-
} text-center py-4 dark:text-white`}
208-
>
209-
{budget}
210-
<br />
211-
{budgetOptions[budget] ? "sats" : "#reckless"}
212-
</div>
213-
);
214-
})}
215-
</div>
167+
<BudgetAmountSelect
168+
value={permissions.maxAmount}
169+
onChange={handleMaxAmountChange}
170+
/>
216171
</>
217172
) : isNewConnection ? (
218173
<>

frontend/src/components/SidebarHint.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ function SidebarHint() {
8888
return (
8989
<SidebarHintCard
9090
icon={Link2}
91-
title="Link your Hub"
92-
description="Finish the setup by linking your Alby Account to this hub."
93-
buttonText="Link Hub"
91+
title="Link to your Alby Account"
92+
description="Finish the setup by linking this Hub to your Alby Account."
93+
buttonText="Link now"
9494
buttonLink="/apps"
9595
/>
9696
);

frontend/src/components/connections/AlbyConnectionCard.tsx

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import {
66
Link2Icon,
77
ZapIcon,
88
} from "lucide-react";
9+
import { useState } from "react";
910
import { Link } from "react-router-dom";
11+
12+
import BudgetAmountSelect from "src/components/BudgetAmountSelect";
13+
import BudgetRenewalSelect from "src/components/BudgetRenewalSelect";
1014
import ExternalLink from "src/components/ExternalLink";
1115
import Loading from "src/components/Loading";
1216
import UserAvatar from "src/components/UserAvatar";
@@ -20,22 +24,36 @@ import {
2024
CardHeader,
2125
CardTitle,
2226
} from "src/components/ui/card";
27+
import {
28+
Dialog,
29+
DialogContent,
30+
DialogDescription,
31+
DialogFooter,
32+
DialogHeader,
33+
DialogTrigger,
34+
} from "src/components/ui/dialog";
35+
import { Label } from "src/components/ui/label";
2336
import { LoadingButton } from "src/components/ui/loading-button";
2437
import { Separator } from "src/components/ui/separator";
2538
import { useAlbyMe } from "src/hooks/useAlbyMe";
2639
import { LinkStatus, useLinkAccount } from "src/hooks/useLinkAccount";
27-
import { App } from "src/types";
40+
import { App, BudgetRenewalType } from "src/types";
41+
import linkAccountIllustration from "/images/illustrations/link-account.png";
2842

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

48+
const [maxAmount, setMaxAmount] = useState(1_000_000);
49+
const [budgetRenewal, setBudgetRenewal] =
50+
useState<BudgetRenewalType>("monthly");
51+
3452
return (
3553
<Card>
3654
<CardHeader>
3755
<CardTitle className="relative">
38-
Alby Account
56+
Linked Alby Account
3957
{connection && <AppCardNotice app={connection} />}
4058
</CardTitle>
4159
<CardDescription>
@@ -47,7 +65,7 @@ function AlbyConnectionCard({ connection }: { connection?: App }) {
4765
<CardContent className="group">
4866
<div className="grid grid-cols-1 xl:grid-cols-2 mt-5 gap-3 items-center relative">
4967
<div className="flex flex-col gap-4">
50-
<div className="flex flex-row gap-4 ">
68+
<div className="flex flex-row gap-4">
5169
<UserAvatar className="h-14 w-14" />
5270
<div className="flex flex-col justify-center">
5371
<div className="text-xl font-semibold">
@@ -62,10 +80,47 @@ function AlbyConnectionCard({ connection }: { connection?: App }) {
6280
<div className="flex flex-col sm:flex-row gap-3 sm:items-center">
6381
{loadingLinkStatus && <Loading />}
6482
{!connection || linkStatus === LinkStatus.SharedNode ? (
65-
<LoadingButton onClick={linkAccount} loading={loading}>
66-
{!loading && <Link2Icon className="w-4 h-4 mr-2" />}
67-
Link your Alby Account
68-
</LoadingButton>
83+
<Dialog>
84+
<DialogTrigger asChild>
85+
<LoadingButton loading={loading}>
86+
{!loading && <Link2Icon className="w-4 h-4 mr-2" />}
87+
Link your Alby Account
88+
</LoadingButton>
89+
</DialogTrigger>
90+
<DialogContent>
91+
<DialogHeader>Link to Alby Account</DialogHeader>
92+
<DialogDescription className="flex flex-col gap-4">
93+
After you link your account, your lightning address and
94+
every app you access through your Alby Account will handle
95+
payments via the Hub.
96+
<img
97+
src={linkAccountIllustration}
98+
className="w-80 mx-auto"
99+
/>
100+
You can add a budget that will restrict how much can be
101+
spent from the Hub with your Alby Account.
102+
</DialogDescription>
103+
<div className="grid gap-1.5">
104+
<Label>Budget renewal</Label>
105+
<BudgetRenewalSelect
106+
value={budgetRenewal}
107+
onChange={setBudgetRenewal}
108+
/>
109+
</div>
110+
<BudgetAmountSelect
111+
value={maxAmount}
112+
onChange={setMaxAmount}
113+
/>
114+
<DialogFooter>
115+
<LoadingButton
116+
onClick={() => linkAccount(maxAmount, budgetRenewal)}
117+
loading={loading}
118+
>
119+
Link to Alby Account
120+
</LoadingButton>
121+
</DialogFooter>
122+
</DialogContent>
123+
</Dialog>
69124
) : linkStatus === LinkStatus.ThisNode ? (
70125
<Button
71126
variant="positive"

0 commit comments

Comments
 (0)