From 63cd230159c26ee5bad00d538fb29241cdd8bb5a Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Thu, 12 Sep 2024 22:43:25 +0700 Subject: [PATCH] Make Alby Account Optional (#639) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: optional Alby Account (WIP) * fix: onboarding checklist loading check * fix: channel suggestions without alby account * feat: add illustrations to manual flows * fix: show alby hub version with no alby account connected * chore: minor ui improvements * fix: re-enable auth redirects if user has an alby identifier, remove unused onboarding page * chore: rename page to connect alby account * fix: alert dialog copy * fix: button sizes * feat: use new alby info endpoint for latest alby hub version * chore: remove unexpected comma from copy * chore: move alby api URLs to the alby oauth service * chore: improve connect alby account copy and spacing * chore: minor ui improvements to app layout and connect alby account pages * chore: improve check for alby automatic auth on setup finish --------- Co-authored-by: René Aaron --- alby/alby_oauth_service.go | 78 ++++++++++++++----- alby/models.go | 14 +++- config/models.go | 2 - frontend/src/components/layouts/AppLayout.tsx | 75 ++++++++++++------ .../src/components/layouts/SettingsLayout.tsx | 14 +++- .../components/redirects/DefaultRedirect.tsx | 7 +- .../src/components/redirects/HomeRedirect.tsx | 2 +- .../redirects/OnboardingRedirect.tsx | 23 ------ frontend/src/hooks/useAlbyBalance.ts | 12 ++- frontend/src/hooks/useAlbyInfo.ts | 10 +++ frontend/src/hooks/useAlbyMe.ts | 13 +++- frontend/src/hooks/useOnboardingData.ts | 30 +++---- frontend/src/routes.tsx | 17 ++-- frontend/src/screens/ConnectAlbyAccount.tsx | 32 ++++++++ frontend/src/screens/Home.tsx | 58 +++++++------- frontend/src/screens/apps/AppList.tsx | 4 +- .../screens/channels/CurrentChannelOrder.tsx | 50 +++++++++++- .../channels/IncreaseIncomingCapacity.tsx | 8 ++ .../channels/IncreaseOutgoingCapacity.tsx | 10 +++ .../screens/channels/first/FirstChannel.tsx | 8 +- frontend/src/screens/onboarding/Success.tsx | 53 ------------- frontend/src/screens/settings/AlbyAccount.tsx | 52 +++++++++++++ frontend/src/screens/setup/SetupFinish.tsx | 23 +++++- .../screens/wallet/OnboardingChecklist.tsx | 7 +- frontend/src/types.ts | 10 ++- http/alby_http_service.go | 13 ++++ wails/wails_handlers.go | 11 +++ 27 files changed, 440 insertions(+), 196 deletions(-) delete mode 100644 frontend/src/components/redirects/OnboardingRedirect.tsx create mode 100644 frontend/src/hooks/useAlbyInfo.ts create mode 100644 frontend/src/screens/ConnectAlbyAccount.tsx delete mode 100644 frontend/src/screens/onboarding/Success.tsx diff --git a/alby/alby_oauth_service.go b/alby/alby_oauth_service.go index 8b00cf1e..9f65b48a 100644 --- a/alby/alby_oauth_service.go +++ b/alby/alby_oauth_service.go @@ -49,6 +49,12 @@ const ( lightningAddressKey = "AlbyLightningAddress" ) +const ( + albyOAuthAPIURL = "https://api.getalby.com" + albyInternalAPIURL = "https://getalby.com/api" + albyOAuthAuthUrl = "https://getalby.com/oauth" +) + const ALBY_ACCOUNT_APP_NAME = "getalby.com" func NewAlbyOAuthService(db *gorm.DB, cfg config.Config, keys keys.Keys, eventPublisher events.EventPublisher) *albyOAuthService { @@ -57,8 +63,8 @@ func NewAlbyOAuthService(db *gorm.DB, cfg config.Config, keys keys.Keys, eventPu ClientSecret: cfg.GetEnv().AlbyClientSecret, Scopes: []string{"account:read", "balance:read", "payments:send"}, Endpoint: oauth2.Endpoint{ - TokenURL: cfg.GetEnv().AlbyAPIURL + "/oauth/token", - AuthURL: cfg.GetEnv().AlbyOAuthAuthUrl, + TokenURL: albyOAuthAPIURL + "/oauth/token", + AuthURL: albyOAuthAuthUrl, AuthStyle: 2, // use HTTP Basic Authorization https://pkg.go.dev/golang.org/x/oauth2#AuthStyle }, } @@ -212,6 +218,48 @@ func (svc *albyOAuthService) fetchUserToken(ctx context.Context) (*oauth2.Token, return newToken, nil } +func (svc *albyOAuthService) GetInfo(ctx context.Context) (*AlbyInfo, error) { + client := &http.Client{Timeout: 10 * time.Second} + + req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/info", albyInternalAPIURL), nil) + if err != nil { + logger.Logger.WithError(err).Error("Error creating request to alby info endpoint") + return nil, err + } + + setDefaultRequestHeaders(req) + + res, err := client.Do(req) + if err != nil { + logger.Logger.WithError(err).Error("Failed to fetch /info") + return nil, err + } + + type albyInfoHub struct { + LatestVersion string `json:"latest_version"` + LatestReleaseNotes string `json:"latest_release_notes"` + } + + type albyInfo struct { + Hub albyInfoHub `json:"hub"` + // TODO: consider getting healthcheck/incident info and showing in the hub + } + + info := &albyInfo{} + err = json.NewDecoder(res.Body).Decode(info) + if err != nil { + logger.Logger.WithError(err).Error("Failed to decode API response") + return nil, err + } + + return &AlbyInfo{ + Hub: AlbyInfoHub{ + LatestVersion: info.Hub.LatestVersion, + LatestReleaseNotes: info.Hub.LatestReleaseNotes, + }, + }, nil +} + func (svc *albyOAuthService) GetMe(ctx context.Context) (*AlbyMe, error) { token, err := svc.fetchUserToken(ctx) if err != nil { @@ -221,7 +269,7 @@ func (svc *albyOAuthService) GetMe(ctx context.Context) (*AlbyMe, error) { client := svc.oauthConf.Client(ctx, token) - req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/users", svc.cfg.GetEnv().AlbyAPIURL), nil) + req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/users", albyOAuthAPIURL), nil) if err != nil { logger.Logger.WithError(err).Error("Error creating request /me") return nil, err @@ -258,7 +306,7 @@ func (svc *albyOAuthService) GetBalance(ctx context.Context) (*AlbyBalance, erro client := svc.oauthConf.Client(ctx, token) - req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/lndhub/balance", svc.cfg.GetEnv().AlbyAPIURL), nil) + req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/lndhub/balance", albyOAuthAPIURL), nil) if err != nil { logger.Logger.WithError(err).Error("Error creating request to balance endpoint") return nil, err @@ -342,7 +390,7 @@ func (svc *albyOAuthService) SendPayment(ctx context.Context, invoice string) er return err } - req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/lndhub/bolt11", svc.cfg.GetEnv().AlbyAPIURL), body) + req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/lndhub/bolt11", albyOAuthAPIURL), body) if err != nil { logger.Logger.WithError(err).Error("Error creating request bolt11 endpoint") return err @@ -615,7 +663,7 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve return } - req, err := http.NewRequest("POST", fmt.Sprintf("%s/events", svc.cfg.GetEnv().AlbyAPIURL), body) + req, err := http.NewRequest("POST", fmt.Sprintf("%s/events", albyOAuthAPIURL), body) if err != nil { logger.Logger.WithError(err).Error("Error creating request /events") return @@ -684,7 +732,7 @@ func (svc *albyOAuthService) backupChannels(ctx context.Context, event *events.E return fmt.Errorf("failed to encode channels backup request payload: %w", err) } - req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/backups", svc.cfg.GetEnv().AlbyAPIURL), body) + req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/backups", albyOAuthAPIURL), body) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -727,7 +775,7 @@ func (svc *albyOAuthService) createAlbyAccountNWCNode(ctx context.Context) (stri return "", err } - req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/nwcs", svc.cfg.GetEnv().AlbyAPIURL), body) + req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/nwcs", albyOAuthAPIURL), body) if err != nil { logger.Logger.WithError(err).Error("Error creating request /internal/nwcs") return "", err @@ -777,7 +825,7 @@ func (svc *albyOAuthService) destroyAlbyAccountNWCNode(ctx context.Context) erro client := svc.oauthConf.Client(ctx, token) - req, err := http.NewRequest("DELETE", fmt.Sprintf("%s/internal/nwcs", svc.cfg.GetEnv().AlbyAPIURL), nil) + req, err := http.NewRequest("DELETE", fmt.Sprintf("%s/internal/nwcs", albyOAuthAPIURL), nil) if err != nil { logger.Logger.WithError(err).Error("Error creating request /internal/nwcs") return err @@ -811,7 +859,7 @@ func (svc *albyOAuthService) activateAlbyAccountNWCNode(ctx context.Context) err client := svc.oauthConf.Client(ctx, token) - req, err := http.NewRequest("PUT", fmt.Sprintf("%s/internal/nwcs/activate", svc.cfg.GetEnv().AlbyAPIURL), nil) + req, err := http.NewRequest("PUT", fmt.Sprintf("%s/internal/nwcs/activate", albyOAuthAPIURL), nil) if err != nil { logger.Logger.WithError(err).Error("Error creating request /internal/nwcs/activate") return err @@ -839,15 +887,9 @@ func (svc *albyOAuthService) activateAlbyAccountNWCNode(ctx context.Context) err func (svc *albyOAuthService) GetChannelPeerSuggestions(ctx context.Context) ([]ChannelPeerSuggestion, error) { - token, err := svc.fetchUserToken(ctx) - if err != nil { - logger.Logger.WithError(err).Error("Failed to fetch user token") - return nil, err - } - - client := svc.oauthConf.Client(ctx, token) + client := &http.Client{Timeout: 10 * time.Second} - req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/channel_suggestions", svc.cfg.GetEnv().AlbyAPIURL), nil) + req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/channel_suggestions", albyInternalAPIURL), nil) if err != nil { logger.Logger.WithError(err).Error("Error creating request to channel_suggestions endpoint") return nil, err diff --git a/alby/models.go b/alby/models.go index 059e8751..4e6a8c7a 100644 --- a/alby/models.go +++ b/alby/models.go @@ -9,6 +9,7 @@ import ( type AlbyOAuthService interface { events.EventSubscriber + GetInfo(ctx context.Context) (*AlbyInfo, error) GetChannelPeerSuggestions(ctx context.Context) ([]ChannelPeerSuggestion, error) GetAuthUrl() string GetUserIdentifier() (string, error) @@ -47,9 +48,18 @@ type AutoChannelResponse struct { Fee uint64 `json:"fee"` } +type AlbyInfoHub struct { + LatestVersion string `json:"latestVersion"` + LatestReleaseNotes string `json:"latestReleaseNotes"` +} + +type AlbyInfo struct { + Hub AlbyInfoHub `json:"hub"` + // TODO: consider getting healthcheck/incident info and showing in the hub +} + type AlbyMeHub struct { - LatestVersion string `json:"latest_version"` - Name string `json:"name"` + Name string `json:"name"` } type AlbyMe struct { Identifier string `json:"identifier"` diff --git a/config/models.go b/config/models.go index 84008a58..37a53f8e 100644 --- a/config/models.go +++ b/config/models.go @@ -29,10 +29,8 @@ type AppConfig struct { LDKGossipSource string `envconfig:"LDK_GOSSIP_SOURCE"` LDKLogLevel string `envconfig:"LDK_LOG_LEVEL" default:"3"` MempoolApi string `envconfig:"MEMPOOL_API" default:"https://mempool.space/api"` - AlbyAPIURL string `envconfig:"ALBY_API_URL" default:"https://api.getalby.com"` AlbyClientId string `envconfig:"ALBY_OAUTH_CLIENT_ID" default:"J2PbXS1yOf"` AlbyClientSecret string `envconfig:"ALBY_OAUTH_CLIENT_SECRET" default:"rABK2n16IWjLTZ9M1uKU"` - AlbyOAuthAuthUrl string `envconfig:"ALBY_OAUTH_AUTH_URL" default:"https://getalby.com/oauth"` BaseUrl string `envconfig:"BASE_URL"` FrontendUrl string `envconfig:"FRONTEND_URL"` LogEvents bool `envconfig:"LOG_EVENTS" default:"true"` diff --git a/frontend/src/components/layouts/AppLayout.tsx b/frontend/src/components/layouts/AppLayout.tsx index cd517421..82ef2148 100644 --- a/frontend/src/components/layouts/AppLayout.tsx +++ b/frontend/src/components/layouts/AppLayout.tsx @@ -9,6 +9,7 @@ import { Megaphone, Menu, MessageCircleQuestion, + PlugZapIcon, Settings, ShieldAlertIcon, ShieldCheckIcon, @@ -46,6 +47,7 @@ import { } from "src/components/ui/tooltip"; import { useAlbyMe } from "src/hooks/useAlbyMe"; +import { useAlbyInfo } from "src/hooks/useAlbyInfo"; import { useInfo } from "src/hooks/useInfo"; import { useRemoveSuccessfulChannelOrder } from "src/hooks/useRemoveSuccessfulChannelOrder"; import { deleteAuthToken } from "src/lib/auth"; @@ -88,15 +90,28 @@ export default function AppLayout() { return ( - - - -

Alby Account Settings

-
-
+ {!info?.albyAccountConnected && ( + + + +

Connect Alby Account

+ +
+ )} + {info?.albyAccountConnected && ( + + + +

Alby Account Settings

+
+
+ )}
{isHttpMode && ( @@ -183,7 +198,7 @@ export default function AppLayout() { Live Support - {!albyMe?.hub.name && ( + {!albyMe?.hub.name && info?.albyAccountConnected && ( { @@ -204,7 +219,7 @@ export default function AppLayout() {
-
+
diff --git a/frontend/src/components/redirects/DefaultRedirect.tsx b/frontend/src/components/redirects/DefaultRedirect.tsx index dd5b428a..fe3f0001 100644 --- a/frontend/src/components/redirects/DefaultRedirect.tsx +++ b/frontend/src/components/redirects/DefaultRedirect.tsx @@ -10,7 +10,12 @@ export function DefaultRedirect() { const navigate = useNavigate(); React.useEffect(() => { - if (!info || (info.running && info.unlocked && info.albyAccountConnected)) { + if ( + !info || + (info.running && + info.unlocked && + (info.albyAccountConnected || !info.albyUserIdentifier)) + ) { return; } const returnTo = location.pathname + location.search; diff --git a/frontend/src/components/redirects/HomeRedirect.tsx b/frontend/src/components/redirects/HomeRedirect.tsx index 0fbeb37a..843952d9 100644 --- a/frontend/src/components/redirects/HomeRedirect.tsx +++ b/frontend/src/components/redirects/HomeRedirect.tsx @@ -16,7 +16,7 @@ export function HomeRedirect() { let to: string | undefined; if (info.setupCompleted && info.running) { if (info.unlocked) { - if (info.albyAccountConnected) { + if (info.albyAccountConnected || !info.albyUserIdentifier) { const returnTo = window.localStorage.getItem( localStorageKeys.returnTo ); diff --git a/frontend/src/components/redirects/OnboardingRedirect.tsx b/frontend/src/components/redirects/OnboardingRedirect.tsx deleted file mode 100644 index c9ce6c0d..00000000 --- a/frontend/src/components/redirects/OnboardingRedirect.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react"; -import { Outlet, useLocation, useNavigate } from "react-router-dom"; -import Loading from "src/components/Loading"; -import { useInfo } from "src/hooks/useInfo"; - -export function OnboardingRedirect() { - const { data: info } = useInfo(); - const location = useLocation(); - const navigate = useNavigate(); - - React.useEffect(() => { - if (!info || (info.running && info.unlocked && info.albyAccountConnected)) { - return; - } - navigate("/"); - }, [info, location, navigate]); - - if (!info) { - return ; - } - - return ; -} diff --git a/frontend/src/hooks/useAlbyBalance.ts b/frontend/src/hooks/useAlbyBalance.ts index 67ac7dd8..16c1e25f 100644 --- a/frontend/src/hooks/useAlbyBalance.ts +++ b/frontend/src/hooks/useAlbyBalance.ts @@ -1,10 +1,16 @@ import useSWR from "swr"; +import { useInfo } from "src/hooks/useInfo"; import { AlbyBalance } from "src/types"; import { swrFetcher } from "src/utils/swr"; export function useAlbyBalance() { - return useSWR("/api/alby/balance", swrFetcher, { - dedupingInterval: 5 * 60 * 1000, // 5 minutes - }); + const { data: info } = useInfo(); + return useSWR( + info?.albyAccountConnected ? "/api/alby/balance" : undefined, + swrFetcher, + { + dedupingInterval: 5 * 60 * 1000, // 5 minutes + } + ); } diff --git a/frontend/src/hooks/useAlbyInfo.ts b/frontend/src/hooks/useAlbyInfo.ts new file mode 100644 index 00000000..85c097a8 --- /dev/null +++ b/frontend/src/hooks/useAlbyInfo.ts @@ -0,0 +1,10 @@ +import useSWR from "swr"; + +import { AlbyInfo } from "src/types"; +import { swrFetcher } from "src/utils/swr"; + +export function useAlbyInfo() { + return useSWR("/api/alby/info", swrFetcher, { + dedupingInterval: 5 * 60 * 1000, // 5 minutes + }); +} diff --git a/frontend/src/hooks/useAlbyMe.ts b/frontend/src/hooks/useAlbyMe.ts index 688fa92f..8a2d47d0 100644 --- a/frontend/src/hooks/useAlbyMe.ts +++ b/frontend/src/hooks/useAlbyMe.ts @@ -1,10 +1,17 @@ import useSWR from "swr"; +import { useInfo } from "src/hooks/useInfo"; import { AlbyMe } from "src/types"; import { swrFetcher } from "src/utils/swr"; export function useAlbyMe() { - return useSWR("/api/alby/me", swrFetcher, { - dedupingInterval: 5 * 60 * 1000, // 5 minutes - }); + const { data: info } = useInfo(); + + return useSWR( + info?.albyAccountConnected ? "/api/alby/me" : undefined, + swrFetcher, + { + dedupingInterval: 5 * 60 * 1000, // 5 minutes + } + ); } diff --git a/frontend/src/hooks/useOnboardingData.ts b/frontend/src/hooks/useOnboardingData.ts index d94eedc2..0735e408 100644 --- a/frontend/src/hooks/useOnboardingData.ts +++ b/frontend/src/hooks/useOnboardingData.ts @@ -31,20 +31,19 @@ export const useOnboardingData = (): UseOnboardingDataResponse => { const { data: transactions } = useTransactions(false, 1); const isLoading = - !albyMe || !apps || !channels || !info || !nodeConnectionInfo || !transactions || - !albyBalance; + (info.albyAccountConnected && (!albyMe || !albyBalance)); if (isLoading) { return { isLoading: true, checklistItems: [] }; } const isLinked = - albyMe && + !!albyMe && nodeConnectionInfo && albyMe?.keysend_pubkey === nodeConnectionInfo?.pubkey; const hasChannel = @@ -60,27 +59,32 @@ export const useOnboardingData = (): UseOnboardingDataResponse => { const checklistItems: Omit[] = [ { - title: "1. Open your first channel", + title: "Open your first channel", description: "Establish a new Lightning channel to enable fast and low-fee Bitcoin transactions.", checked: hasChannel, to: "/channels/first", }, + ...(info.albyAccountConnected + ? [ + { + title: "Link to your Alby Account", + description: + "Link your lightning address & other apps to this Hub.", + checked: isLinked, + to: "/apps", + }, + ] + : []), { - title: "2. Link to your Alby Account", - description: "Link your lightning address & other apps to this Hub.", - checked: isLinked, - to: "/apps", - }, - { - title: "3. Send or receive your first payment", + title: "Send or receive your first payment", description: "Use your newly opened channel to make a transaction on the Lightning Network.", checked: hasTransaction, to: "/wallet", }, { - title: "4. Connect your first app", + title: "Connect your first app", description: "Seamlessly connect apps and integrate your wallet with other apps from your Hub.", checked: hasCustomApp, @@ -89,7 +93,7 @@ export const useOnboardingData = (): UseOnboardingDataResponse => { ...(hasMnemonic ? [ { - title: "5. Backup your keys", + title: "Backup your keys", description: "Secure your keys by creating a backup to ensure you don't lose access.", checked: hasBackedUp === true, diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index bd842876..c3754a60 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -5,12 +5,12 @@ import SettingsLayout from "src/components/layouts/SettingsLayout"; import TwoColumnFullScreenLayout from "src/components/layouts/TwoColumnFullScreenLayout"; import { DefaultRedirect } from "src/components/redirects/DefaultRedirect"; import { HomeRedirect } from "src/components/redirects/HomeRedirect"; -import { OnboardingRedirect } from "src/components/redirects/OnboardingRedirect"; import { SetupRedirect } from "src/components/redirects/SetupRedirect"; import { StartRedirect } from "src/components/redirects/StartRedirect"; import { BackupMnemonic } from "src/screens/BackupMnemonic"; import { BackupNode } from "src/screens/BackupNode"; import { BackupNodeSuccess } from "src/screens/BackupNodeSuccess"; +import { ConnectAlbyAccount } from "src/screens/ConnectAlbyAccount"; import Home from "src/screens/Home"; import { Intro } from "src/screens/Intro"; import NotFound from "src/screens/NotFound"; @@ -35,7 +35,6 @@ import { OpenedFirstChannel } from "src/screens/channels/first/OpenedFirstChanne import { OpeningFirstChannel } from "src/screens/channels/first/OpeningFirstChannel"; import { BuzzPay } from "src/screens/internal-apps/BuzzPay"; import { UncleJim } from "src/screens/internal-apps/UncleJim"; -import { Success } from "src/screens/onboarding/Success"; import BuyBitcoin from "src/screens/onchain/BuyBitcoin"; import DepositBitcoin from "src/screens/onchain/DepositBitcoin"; import ConnectPeer from "src/screens/peers/ConnectPeer"; @@ -331,6 +330,10 @@ const routes = [ ), }, + { + path: "alby/account", + element: , + }, { path: "alby/auth", element: , @@ -409,16 +412,6 @@ const routes = [ }, ], }, - { - path: "onboarding", - element: , - children: [ - { - path: "success", - element: , - }, - ], - }, { path: "alby/auth", element: , diff --git a/frontend/src/screens/ConnectAlbyAccount.tsx b/frontend/src/screens/ConnectAlbyAccount.tsx new file mode 100644 index 00000000..06548386 --- /dev/null +++ b/frontend/src/screens/ConnectAlbyAccount.tsx @@ -0,0 +1,32 @@ +import { LinkButton } from "src/components/ui/button"; + +export function ConnectAlbyAccount() { + return ( +
+

Connect Your Alby Account

+
+ + +
+

+ Your Alby Account gives your hub a lightning address, Nostr address and + zaps, email notifications, fiat topups, priority support, automatic + channel backups, access to podcasting apps & more. +

+
+ + Connect now + + + Maybe later + +
+
+ ); +} diff --git a/frontend/src/screens/Home.tsx b/frontend/src/screens/Home.tsx index 68a07e83..3b9717d3 100644 --- a/frontend/src/screens/Home.tsx +++ b/frontend/src/screens/Home.tsx @@ -28,7 +28,7 @@ function getGreeting(name: string | undefined) { greeting = "Good Evening"; } - return `${greeting}${name && `, ${name}`}!`; + return `${greeting}${name ? `, ${name}` : ""}!`; } function Home() { @@ -39,7 +39,7 @@ function Home() { /* eslint-disable @typescript-eslint/no-explicit-any */ const extensionInstalled = (window as any).alby !== undefined; - if (!info || !balances || !albyMe) { + if (!info || !balances) { return ; } @@ -48,33 +48,35 @@ function Home() {
- - - -
-
- -
-
- -
- Alby Web -
-
- - Install Alby Web on your phone and use your Hub on the go. - + {info.albyAccountConnected && ( + + + +
+
+ +
+
+ +
+ Alby Web +
+
+ + Install Alby Web on your phone and use your Hub on the go. + +
-
- - - - - - + + + + + + + )} {!extensionInstalled && ( diff --git a/frontend/src/screens/apps/AppList.tsx b/frontend/src/screens/apps/AppList.tsx index cc4e8270..dcc1539a 100644 --- a/frontend/src/screens/apps/AppList.tsx +++ b/frontend/src/screens/apps/AppList.tsx @@ -39,7 +39,9 @@ function AppList() { } /> - + {info.albyAccountConnected && ( + + )} {!otherApps.length && ( { + for (let i = 0; i < 10; i++) { + setTimeout( + () => { + confetti({ + origin: { + x: Math.random(), + y: Math.random(), + }, + colors: ["#000", "#333", "#666", "#999", "#BBB", "#FFF"], + }); + }, + Math.floor(Math.random() * 1000) + ); + } + }, []); + + return ( +
+ + +

+ Congratulations! Your channel is active and can be used to send and + receive payments. +

+

+ To ensure you can both send and receive, make sure to balance your{" "} + + channel's liquidity + + . +

+ + + + +
+ ); +} + function ChannelOpening({ fundingTxId }: { fundingTxId: string | undefined }) { const { data: channels } = useChannels(true); const channel = fundingTxId diff --git a/frontend/src/screens/channels/IncreaseIncomingCapacity.tsx b/frontend/src/screens/channels/IncreaseIncomingCapacity.tsx index 42b2aab6..f58e4b82 100644 --- a/frontend/src/screens/channels/IncreaseIncomingCapacity.tsx +++ b/frontend/src/screens/channels/IncreaseIncomingCapacity.tsx @@ -205,6 +205,14 @@ function NewChannelInternal({ />
+ +
diff --git a/frontend/src/screens/channels/IncreaseOutgoingCapacity.tsx b/frontend/src/screens/channels/IncreaseOutgoingCapacity.tsx index 3ddb67e3..39ca6626 100644 --- a/frontend/src/screens/channels/IncreaseOutgoingCapacity.tsx +++ b/frontend/src/screens/channels/IncreaseOutgoingCapacity.tsx @@ -228,6 +228,14 @@ function NewChannelInternal({ network }: { network: Network }) { />
+ + { setPubkey(e.target.value.trim()); @@ -517,6 +526,7 @@ function NewChannelOnchain(props: NewChannelOnchainProps) { id="host" type="text" value={host} + required placeholder="0.0.0.0:9735 or [2600::]:9735" onChange={(e) => { setHost(e.target.value.trim()); diff --git a/frontend/src/screens/channels/first/FirstChannel.tsx b/frontend/src/screens/channels/first/FirstChannel.tsx index 908cfdbe..6afb710e 100644 --- a/frontend/src/screens/channels/first/FirstChannel.tsx +++ b/frontend/src/screens/channels/first/FirstChannel.tsx @@ -43,7 +43,13 @@ export function FirstChannel() { } }, [channels, navigate]); - if (!info || !channels) { + React.useEffect(() => { + if (info && !info.albyAccountConnected) { + navigate("/channels/incoming"); + } + }, [info, navigate]); + + if (!info?.albyAccountConnected || !channels) { return ; } diff --git a/frontend/src/screens/onboarding/Success.tsx b/frontend/src/screens/onboarding/Success.tsx deleted file mode 100644 index a0bae223..00000000 --- a/frontend/src/screens/onboarding/Success.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import confetti from "canvas-confetti"; -import React from "react"; -import { Link } from "react-router-dom"; -import ExternalLink from "src/components/ExternalLink"; -import TwoColumnLayoutHeader from "src/components/TwoColumnLayoutHeader"; -import { Button } from "src/components/ui/button"; - -export function Success() { - React.useEffect(() => { - for (let i = 0; i < 10; i++) { - setTimeout( - () => { - confetti({ - origin: { - x: Math.random(), - y: Math.random(), - }, - colors: ["#000", "#333", "#666", "#999", "#BBB", "#FFF"], - }); - }, - Math.floor(Math.random() * 1000) - ); - } - }, []); - - return ( -
- - -

- Congratulations! Your channel is active and can be used to send and - receive payments. -

-

- To ensure you can both send and receive, make sure to balance your{" "} - - channel's liquidity - - . -

- - - - -
- ); -} diff --git a/frontend/src/screens/settings/AlbyAccount.tsx b/frontend/src/screens/settings/AlbyAccount.tsx index 22c5efe1..b3f42961 100644 --- a/frontend/src/screens/settings/AlbyAccount.tsx +++ b/frontend/src/screens/settings/AlbyAccount.tsx @@ -29,6 +29,27 @@ export function AlbyAccount() { const { toast } = useToast(); const navigate = useNavigate(); + const disconnect = async () => { + try { + await request("/api/alby/unlink-account", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + navigate("/"); + toast({ + title: "Alby Account Disconnected", + description: "Your hub is no longer connected to an Alby Account.", + }); + } catch (error) { + toast({ + title: "Disconnect account failed", + description: (error as Error).message, + variant: "destructive", + }); + } + }; const unlink = async () => { try { await request("/api/alby/unlink-account", { @@ -105,6 +126,37 @@ export function AlbyAccount() { + + + + + Disconnect Alby Account + + Use Alby Hub without an Alby + Account + + + + + + + Disconnect Alby Account + +
+

Are you sure you want to disconnect your Alby Account?

+

+ Your Alby Account will be disconnected and all Alby Account + features such as your lightning address will stop working. +

+
+
+
+ + Cancel + Confirm + +
+
); } diff --git a/frontend/src/screens/setup/SetupFinish.tsx b/frontend/src/screens/setup/SetupFinish.tsx index 61b713f4..24bff60f 100644 --- a/frontend/src/screens/setup/SetupFinish.tsx +++ b/frontend/src/screens/setup/SetupFinish.tsx @@ -5,6 +5,7 @@ import animationData from "src/assets/lotties/loading.json"; import Container from "src/components/Container"; import { Button } from "src/components/ui/button"; import { ToastSignature, useToast } from "src/components/ui/use-toast"; +import { localStorageKeys } from "src/constants"; import { useInfo } from "src/hooks/useInfo"; import { saveAuthToken } from "src/lib/auth"; @@ -73,6 +74,9 @@ export function SetupFinish() { }, [loading]); useEffect(() => { + if (!info) { + return; + } // ensure setup call is only called once if (hasFetchedRef.current) { return; @@ -81,14 +85,19 @@ export function SetupFinish() { (async () => { setLoading(true); - const succeeded = await finishSetup(nodeInfo, unlockPassword, toast); + const succeeded = await finishSetup( + nodeInfo, + unlockPassword, + toast, + info.oauthRedirect + ); // only setup call is successful as start is async if (!succeeded) { setLoading(false); setConnectionError(true); } })(); - }, [nodeInfo, navigate, unlockPassword, toast]); + }, [nodeInfo, navigate, unlockPassword, toast, info]); if (connectionError) { return ( @@ -125,9 +134,17 @@ export function SetupFinish() { const finishSetup = async ( nodeInfo: SetupNodeInfo, unlockPassword: string, - toast: ToastSignature + toast: ToastSignature, + autoAuth: boolean ): Promise => { try { + let redirectTo = "/alby/account"; + if (autoAuth) { + redirectTo = "/alby/auth"; + } + + window.localStorage.setItem(localStorageKeys.returnTo, redirectTo); + await request("/api/setup", { method: "POST", headers: { diff --git a/frontend/src/screens/wallet/OnboardingChecklist.tsx b/frontend/src/screens/wallet/OnboardingChecklist.tsx index 2fd83ad3..b27d7636 100644 --- a/frontend/src/screens/wallet/OnboardingChecklist.tsx +++ b/frontend/src/screens/wallet/OnboardingChecklist.tsx @@ -18,6 +18,7 @@ interface ChecklistItemProps { description: string; to: string; disabled: boolean; + index: number; } function OnboardingChecklist() { @@ -37,9 +38,10 @@ function OnboardingChecklist() { - {checklistItems.map((item) => ( + {checklistItems.map((item, index) => ( - {title} + {index + 1}. {title}
{!checked && ( diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 2864e493..0852198b 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -320,7 +320,14 @@ export type RecommendedChannelPeer = { } ); -// TODO: move to different file +export type AlbyInfo = { + hub: { + latestVersion: string; + latestReleaseNotes: string; + }; +}; + +// TODO: use camel case (needs mapping in the Alby OAuth Service - see how AlbyInfo is done above) export type AlbyMe = { identifier: string; nostr_pubkey: string; @@ -331,7 +338,6 @@ export type AlbyMe = { keysend_pubkey: string; shared_node: boolean; hub: { - latest_version: string; name?: string; }; }; diff --git a/http/alby_http_service.go b/http/alby_http_service.go index 74153ee4..dde0f98b 100644 --- a/http/alby_http_service.go +++ b/http/alby_http_service.go @@ -28,6 +28,7 @@ func NewAlbyHttpService(svc service.Service, albyOAuthSvc alby.AlbyOAuthService, func (albyHttpSvc *AlbyHttpService) RegisterSharedRoutes(restrictedGroup *echo.Group, e *echo.Echo) { e.GET("/api/alby/callback", albyHttpSvc.albyCallbackHandler) + e.GET("/api/alby/info", albyHttpSvc.albyInfoHandler) restrictedGroup.GET("/api/alby/me", albyHttpSvc.albyMeHandler) restrictedGroup.GET("/api/alby/balance", albyHttpSvc.albyBalanceHandler) restrictedGroup.POST("/api/alby/pay", albyHttpSvc.albyPayHandler) @@ -72,6 +73,18 @@ func (albyHttpSvc *AlbyHttpService) unlinkHandler(c echo.Context) error { return c.NoContent(http.StatusNoContent) } +func (albyHttpSvc *AlbyHttpService) albyInfoHandler(c echo.Context) error { + info, err := albyHttpSvc.albyOAuthSvc.GetInfo(c.Request().Context()) + if err != nil { + logger.Logger.WithError(err).Error("Failed to request alby info endpoint") + return c.JSON(http.StatusInternalServerError, ErrorResponse{ + Message: fmt.Sprintf("Failed to request alby info endpoint: %s", err.Error()), + }) + } + + return c.JSON(http.StatusOK, info) +} + func (albyHttpSvc *AlbyHttpService) albyCallbackHandler(c echo.Context) error { code := c.QueryParam("code") diff --git a/wails/wails_handlers.go b/wails/wails_handlers.go index 8cf2af77..36088e2f 100644 --- a/wails/wails_handlers.go +++ b/wails/wails_handlers.go @@ -261,6 +261,17 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } switch route { + case "/api/alby/info": + info, err := app.svc.GetAlbyOAuthSvc().GetInfo(ctx) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "route": route, + "method": method, + "body": body, + }).WithError(err).Error("Failed to decode request to wails router") + return WailsRequestRouterResponse{Body: nil, Error: err.Error()} + } + return WailsRequestRouterResponse{Body: info, Error: ""} case "/api/alby/me": me, err := app.svc.GetAlbyOAuthSvc().GetMe(ctx) if err != nil {