Skip to content

Commit

Permalink
feat: appcard improvements & linking account (#235)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
reneaaron and rolznz authored Jul 11, 2024
1 parent 40e69a1 commit 060087b
Show file tree
Hide file tree
Showing 16 changed files with 272 additions and 126 deletions.
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%)
(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";
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

0 comments on commit 060087b

Please sign in to comment.