diff --git a/.github/workflows/wails2.yaml b/.github/workflows/wails2.yaml index bb6edcc80..5f0341271 100644 --- a/.github/workflows/wails2.yaml +++ b/.github/workflows/wails2.yaml @@ -77,8 +77,8 @@ jobs: if: runner.os == 'Windows' run: | cp `go list -m -f "{{.Dir}}" github.com/breez/breez-sdk-go`/breez_sdk/lib/windows-amd64/breez_sdk_bindings.dll ./ - cp `go list -m -f "{{.Dir}}" github.com/getAlby/glalby-go`/glalby/x86_64-pc-windows-gnu/glalby_bindings.dll ./ - cp `go list -m -f "{{.Dir}}" github.com/getAlby/ldk-node-go`/ldk_node/x86_64-pc-windows-gnu/ldk_node.dll ./ + cp `go list -m -f "{{.Dir}}" github.com/getAlby/glalby-go`/glalby/x86_64-pc-windows-msvc/glalby_bindings.dll ./ + cp `go list -m -f "{{.Dir}}" github.com/getAlby/ldk-node-go`/ldk_node/x86_64-pc-windows-msvc/ldk_node.dll ./ shell: bash - name: Copy appicon in place diff --git a/api/api.go b/api/api.go index 7f3fb9440..9881950bd 100644 --- a/api/api.go +++ b/api/api.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "strings" + "sync" "time" "github.com/sirupsen/logrus" @@ -579,7 +580,14 @@ func (api *api) SetNextBackupReminder(backupReminderRequest *BackupReminderReque return nil } +var startMutex sync.Mutex + func (api *api) Start(startRequest *StartRequest) error { + if !startMutex.TryLock() { + // do not allow to start twice in case this is somehow called twice + return errors.New("app is already starting") + } + defer startMutex.Unlock() return api.svc.StartApp(startRequest.UnlockPassword) } diff --git a/frontend/src/components/AppCard.tsx b/frontend/src/components/AppCard.tsx deleted file mode 100644 index 79bc41026..000000000 --- a/frontend/src/components/AppCard.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import dayjs from "dayjs"; -import relativeTime from "dayjs/plugin/relativeTime"; -import { Link } from "react-router-dom"; -import AppAvatar from "src/components/AppAvatar"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "src/components/ui/card"; -import { Progress } from "src/components/ui/progress"; -import { App, NIP_47_PAY_INVOICE_METHOD } from "src/types"; - -dayjs.extend(relativeTime); - -type Props = { - app: App; - csrf?: string; -}; - -export default function AppCard({ app }: Props) { - return ( - <> - - - - -
- -
- {app.name} -
-
-
-
- -
- {app.requestMethods?.includes(NIP_47_PAY_INVOICE_METHOD) ? ( - app.maxAmount > 0 ? ( - <> -
-
-

- You've spent -

-

- {new Intl.NumberFormat().format(app.budgetUsage)} sats -

-
-
- {" "} -

- Left in budget -

-

- {new Intl.NumberFormat().format( - app.maxAmount - app.budgetUsage - )}{" "} - sats -

-
-
- - - ) : ( - "No limits!" - ) - ) : ( - "Payments disabled." - )} -
-
-
-
Budget
-
- {app.maxAmount > 0 ? ( - <> - {new Intl.NumberFormat().format(app.maxAmount)} sats /{" "} - {app.budgetRenewal} - - ) : ( - "Not set" - )} -
-
-
-
Expires on
-
- {app.expiresAt ? dayjs(app.expiresAt).fromNow() : "Never"} -
-
-
-
Last used
-
- {app.lastEventAt ? dayjs(app.lastEventAt).fromNow() : "Never"} -
-
-
-
-
- - - ); -} diff --git a/frontend/src/components/SidebarHint.tsx b/frontend/src/components/SidebarHint.tsx index 370b19456..b7d5e2b78 100644 --- a/frontend/src/components/SidebarHint.tsx +++ b/frontend/src/components/SidebarHint.tsx @@ -82,7 +82,8 @@ function SidebarHint() { if ( albyMe && nodeConnectionInfo && - albyMe?.keysend_pubkey !== nodeConnectionInfo?.pubkey + albyMe?.keysend_pubkey !== nodeConnectionInfo?.pubkey && + !location.pathname.startsWith("/apps") ) { return ( - Alby Account + + Alby Account + {connection && } + Link Your Alby Account to use your lightning address with Alby Hub and use apps that you connected to your Alby Account. - -
+ +
{albyMe?.name}
-
+
{albyMe?.lightning_address}
@@ -88,65 +92,17 @@ function AlbyConnectionCard({ connection }: { connection?: App }) {
-
- {connection && ( - <> - {connection.maxAmount > 0 && ( - <> -
-
-

- You've spent -

-

- {new Intl.NumberFormat().format( - connection.budgetUsage - )}{" "} - sats -

-
-
- {" "} -

- Left in budget -

-

- {new Intl.NumberFormat().format( - connection.maxAmount - connection.budgetUsage - )}{" "} - sats -

-
-
- -
- {connection.maxAmount > 0 ? ( - <> - {new Intl.NumberFormat().format(connection.maxAmount)}{" "} - sats / {connection.budgetRenewal} - - ) : ( - "Not set" - )} -
- - - -
-
- - )} - - )} -
+ {connection && ( +
+ + + + +
+ )}
diff --git a/frontend/src/components/connections/AppCard.tsx b/frontend/src/components/connections/AppCard.tsx new file mode 100644 index 000000000..7276e3235 --- /dev/null +++ b/frontend/src/components/connections/AppCard.tsx @@ -0,0 +1,45 @@ +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; +import { Link } from "react-router-dom"; +import AppAvatar from "src/components/AppAvatar"; +import { AppCardConnectionInfo } from "src/components/connections/AppCardConnectionInfo"; +import { AppCardNotice } from "src/components/connections/AppCardNotice"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "src/components/ui/card"; +import { App } from "src/types"; + +dayjs.extend(relativeTime); + +type Props = { + app: App; + csrf?: string; +}; + +export default function AppCard({ app }: Props) { + return ( + <> + + + + + +
+ +
+ {app.name} +
+
+
+
+ + + +
+ + + ); +} diff --git a/frontend/src/components/connections/AppCardConnectionInfo.tsx b/frontend/src/components/connections/AppCardConnectionInfo.tsx new file mode 100644 index 000000000..ebc639e08 --- /dev/null +++ b/frontend/src/components/connections/AppCardConnectionInfo.tsx @@ -0,0 +1,59 @@ +import dayjs from "dayjs"; +import { Progress } from "src/components/ui/progress"; +import { formatAmount } from "src/lib/utils"; +import { App } from "src/types"; + +type AppCardConnectionInfoProps = { + connection: App; +}; + +export function AppCardConnectionInfo({ + connection, +}: AppCardConnectionInfoProps) { + return ( + <> + {connection.maxAmount > 0 && ( + <> +
+
+

+ Left in budget +

+

+ {new Intl.NumberFormat().format( + connection.maxAmount - connection.budgetUsage + )}{" "} + sats +

+
+
+ +
+
+ {connection.maxAmount && ( + <>{formatAmount(connection.maxAmount * 1000)} sats + )} +
+
+ {connection.maxAmount > 0 && + connection.budgetRenewal !== "never" && ( + <>Renews {connection.budgetRenewal} + )} +
+
+ {connection.lastEventAt && ( +
+
+
Last used: 
+
{dayjs(connection.lastEventAt).fromNow()}
+
+
+ )} + + )} + + ); +} diff --git a/frontend/src/components/connections/AppCardNotice.tsx b/frontend/src/components/connections/AppCardNotice.tsx new file mode 100644 index 000000000..eb4f0aac5 --- /dev/null +++ b/frontend/src/components/connections/AppCardNotice.tsx @@ -0,0 +1,61 @@ +import dayjs from "dayjs"; +import isBetween from "dayjs/plugin/isBetween"; // Add this line +import { CalendarClock } from "lucide-react"; +import { Link } from "react-router-dom"; +import { Badge } from "src/components/ui/badge"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "src/components/ui/tooltip"; +import { App } from "src/types"; + +dayjs.extend(isBetween); // Extend dayjs with the isBetween plugin + +type AppCardNoticeProps = { + app: App; +}; + +export function AppCardNotice({ app }: AppCardNoticeProps) { + const now = dayjs(); + const expiresAt = dayjs(app.expiresAt); + const isExpired = expiresAt.isBefore(now); + const expiresSoon = expiresAt.isBetween(now, now.add(7, "days")); + + return ( +
+ {app.expiresAt ? ( + isExpired ? ( + + + + + + + Expired + + + + Expired {expiresAt.fromNow()} + + + ) : expiresSoon ? ( + + + + + + + Expires Soon + + + + Expires {expiresAt.fromNow()} + + + ) : null + ) : null} +
+ ); +} diff --git a/frontend/src/hooks/useLinkAccount.ts b/frontend/src/hooks/useLinkAccount.ts index 6d66a8532..9a858982d 100644 --- a/frontend/src/hooks/useLinkAccount.ts +++ b/frontend/src/hooks/useLinkAccount.ts @@ -1,6 +1,7 @@ import { useState } from "react"; import { toast } from "src/components/ui/use-toast"; import { useAlbyMe } from "src/hooks/useAlbyMe"; +import { useApps } from "src/hooks/useApps"; import { useCSRF } from "src/hooks/useCSRF"; import { useNodeConnectionInfo } from "src/hooks/useNodeConnectionInfo"; import { request } from "src/utils/request"; @@ -14,6 +15,7 @@ export enum LinkStatus { export function useLinkAccount() { const { data: csrf } = useCSRF(); const { data: me, mutate: reloadAlbyMe } = useAlbyMe(); + const { mutate: reloadApps } = useApps(); const { data: nodeConnectionInfo } = useNodeConnectionInfo(); const [loading, setLoading] = useState(false); @@ -43,7 +45,8 @@ export function useLinkAccount() { "Content-Type": "application/json", }, }); - await reloadAlbyMe(); + // update the link status and get the newly-created Alby Account app + await Promise.all([reloadAlbyMe(), reloadApps()]); toast({ title: "Your Alby Hub has successfully been linked to your Alby Account", diff --git a/frontend/src/screens/apps/AppList.tsx b/frontend/src/screens/apps/AppList.tsx index de7d4eaa8..8d4176692 100644 --- a/frontend/src/screens/apps/AppList.tsx +++ b/frontend/src/screens/apps/AppList.tsx @@ -1,10 +1,10 @@ import { Cable, CirclePlus } from "lucide-react"; import { Link } from "react-router-dom"; -import AlbyConnectionCard from "src/components/AlbyConnectionCard"; -import AppCard from "src/components/AppCard"; import AppHeader from "src/components/AppHeader"; import EmptyState from "src/components/EmptyState"; import Loading from "src/components/Loading"; +import AlbyConnectionCard from "src/components/connections/AlbyConnectionCard"; +import AppCard from "src/components/connections/AppCard"; import { Button } from "src/components/ui/button"; import { useApps } from "src/hooks/useApps"; import { useInfo } from "src/hooks/useInfo"; @@ -52,7 +52,7 @@ function AppList() { )} {otherApps.length > 0 && ( -
+
{otherApps.map((app, index) => ( ))} diff --git a/go.mod b/go.mod index 91436de42..f2c84b2f5 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,8 @@ require ( github.com/breez/breez-sdk-go v0.3.4 github.com/davrux/echo-logrus/v4 v4.0.3 github.com/elnosh/gonuts v0.1.1-0.20240602162005-49da741613e4 - github.com/getAlby/glalby-go v0.0.0-20240416174357-e6e2faa2fbd8 - github.com/getAlby/ldk-node-go v0.0.0-20240614062656-d4de573a1996 + github.com/getAlby/glalby-go v0.0.0-20240616134525-322750d01f8d + github.com/getAlby/ldk-node-go v0.0.0-20240616134337-9740e8149bc0 github.com/go-gormigrate/gormigrate/v2 v2.1.1 github.com/gorilla/sessions v1.2.2 github.com/labstack/echo-contrib v0.14.1 diff --git a/go.sum b/go.sum index 09aa7378c..558dbdf57 100644 --- a/go.sum +++ b/go.sum @@ -188,8 +188,12 @@ github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwV github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/getAlby/glalby-go v0.0.0-20240416174357-e6e2faa2fbd8 h1:mJsdhUb8hmSSSLR2GQFw9BGtnJP7xmKB/XQxDt3DvAo= github.com/getAlby/glalby-go v0.0.0-20240416174357-e6e2faa2fbd8/go.mod h1:ViyJvjlvv0GCesTJ7mb3fBo4G+/qsujDAFN90xZ7a9U= +github.com/getAlby/glalby-go v0.0.0-20240616134525-322750d01f8d h1:ouIUrgIJXgf+foCDfOU67oNSraBeED15vCoPvaanU1I= +github.com/getAlby/glalby-go v0.0.0-20240616134525-322750d01f8d/go.mod h1:ViyJvjlvv0GCesTJ7mb3fBo4G+/qsujDAFN90xZ7a9U= github.com/getAlby/ldk-node-go v0.0.0-20240614062656-d4de573a1996 h1:UULF8HX3z0kxgppzDX67oG/7t1Es+tpZogqtsYsguX0= github.com/getAlby/ldk-node-go v0.0.0-20240614062656-d4de573a1996/go.mod h1:8BRjtKcz8E0RyYTPEbMS8VIdgredcGSLne8vHDtcRLg= +github.com/getAlby/ldk-node-go v0.0.0-20240616134337-9740e8149bc0 h1:7BiZOIL+rAbR+waoXVYJCsMXIOTVBy/Ex4u2WoV4kTw= +github.com/getAlby/ldk-node-go v0.0.0-20240616134337-9740e8149bc0/go.mod h1:8BRjtKcz8E0RyYTPEbMS8VIdgredcGSLne8vHDtcRLg= github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/service/start.go b/service/start.go index 7b05178f8..be9ffc3d6 100644 --- a/service/start.go +++ b/service/start.go @@ -103,6 +103,9 @@ func (svc *service) StartNostr(ctx context.Context, encryptionKey string) error } func (svc *service) StartApp(encryptionKey string) error { + if svc.lnClient != nil { + return errors.New("app already started") + } if !svc.cfg.CheckUnlockPassword(encryptionKey) { logger.Logger.Errorf("Invalid password") return errors.New("invalid password")