diff --git a/.github/workflows/http.yml b/.github/workflows/http.yml index 604332fd..3ad2fc1a 100644 --- a/.github/workflows/http.yml +++ b/.github/workflows/http.yml @@ -1,6 +1,10 @@ name: HTTP build - Linux and MacOS on: push: + branches: + - master + pull_request: + types: [opened, synchronize] workflow_call: inputs: build-release: diff --git a/.github/workflows/wails.yml b/.github/workflows/wails.yml index e19c6967..15363468 100644 --- a/.github/workflows/wails.yml +++ b/.github/workflows/wails.yml @@ -2,6 +2,10 @@ name: Wails build - all platforms on: push: + branches: + - master + pull_request: + types: [opened, synchronize] workflow_call: inputs: diff --git a/alby/alby_oauth_service.go b/alby/alby_oauth_service.go index c143858d..4e2dbf0d 100644 --- a/alby/alby_oauth_service.go +++ b/alby/alby_oauth_service.go @@ -439,6 +439,7 @@ func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient. nil, scopes, false, + nil, ) if err != nil { diff --git a/api/api.go b/api/api.go index 274c5f4f..edaff3e5 100644 --- a/api/api.go +++ b/api/api.go @@ -75,7 +75,8 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons createAppRequest.BudgetRenewal, expiresAt, createAppRequest.Scopes, - createAppRequest.Isolated) + createAppRequest.Isolated, + createAppRequest.Metadata) if err != nil { return nil, err @@ -220,6 +221,16 @@ func (api *api) GetApp(dbApp *db.App) *App { maxAmount := uint64(paySpecificPermission.MaxAmountSat) budgetUsage = queries.GetBudgetUsageSat(api.db, &paySpecificPermission) + var metadata Metadata + if dbApp.Metadata != nil { + jsonErr := json.Unmarshal(dbApp.Metadata, &metadata) + if jsonErr != nil { + logger.Logger.WithError(jsonErr).WithFields(logrus.Fields{ + "app_id": dbApp.ID, + }).Error("Failed to deserialize app metadata") + } + } + response := App{ ID: dbApp.ID, Name: dbApp.Name, @@ -233,6 +244,7 @@ func (api *api) GetApp(dbApp *db.App) *App { BudgetUsage: budgetUsage, BudgetRenewal: paySpecificPermission.BudgetRenewal, Isolated: dbApp.Isolated, + Metadata: metadata, } if dbApp.Isolated { @@ -300,6 +312,17 @@ func (api *api) ListApps() ([]App, error) { apiApp.LastEventAt = &lastEvent.CreatedAt } + var metadata Metadata + if dbApp.Metadata != nil { + jsonErr := json.Unmarshal(dbApp.Metadata, &metadata) + if jsonErr != nil { + logger.Logger.WithError(jsonErr).WithFields(logrus.Fields{ + "app_id": dbApp.ID, + }).Error("Failed to deserialize app metadata") + } + apiApp.Metadata = metadata + } + apiApps = append(apiApps, apiApp) } return apiApps, nil diff --git a/api/models.go b/api/models.go index f7583030..fd01e69d 100644 --- a/api/models.go +++ b/api/models.go @@ -70,6 +70,7 @@ type App struct { BudgetRenewal string `json:"budgetRenewal"` Isolated bool `json:"isolated"` Balance uint64 `json:"balance"` + Metadata Metadata `json:"metadata,omitempty"` } type ListAppsResponse struct { @@ -93,6 +94,7 @@ type CreateAppRequest struct { Scopes []string `json:"scopes"` ReturnTo string `json:"returnTo"` Isolated bool `json:"isolated"` + Metadata Metadata `json:"metadata,omitempty"` } type StartRequest struct { diff --git a/cmd/http/main.go b/cmd/http/main.go index 60abfd30..72d7d4b8 100644 --- a/cmd/http/main.go +++ b/cmd/http/main.go @@ -17,7 +17,7 @@ import ( ) func main() { - log.Info("NWC Starting in HTTP mode") + log.Info("AlbyHub Starting in HTTP mode") // Create a channel to receive OS signals. osSignalChannel := make(chan os.Signal, 1) diff --git a/db/db_service.go b/db/db_service.go index 39660772..32753ba6 100644 --- a/db/db_service.go +++ b/db/db_service.go @@ -2,6 +2,7 @@ package db import ( "encoding/hex" + "encoding/json" "errors" "fmt" "slices" @@ -11,6 +12,7 @@ import ( "github.com/getAlby/hub/events" "github.com/getAlby/hub/logger" "github.com/nbd-wtf/go-nostr" + "gorm.io/datatypes" "gorm.io/gorm" ) @@ -26,7 +28,7 @@ func NewDBService(db *gorm.DB, eventPublisher events.EventPublisher) *dbService } } -func (svc *dbService) CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string, isolated bool) (*App, string, error) { +func (svc *dbService) CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string, isolated bool, metadata map[string]interface{}) (*App, string, error) { if isolated && (slices.Contains(scopes, constants.SIGN_MESSAGE_SCOPE)) { // cannot sign messages because the isolated app is a custodial subaccount return nil, "", errors.New("isolated app cannot have sign_message scope") @@ -47,7 +49,17 @@ func (svc *dbService) CreateApp(name string, pubkey string, maxAmountSat uint64, } } - app := App{Name: name, NostrPubkey: pairingPublicKey, Isolated: isolated} + var metadataBytes []byte + if metadata != nil { + var err error + metadataBytes, err = json.Marshal(metadata) + if err != nil { + logger.Logger.WithError(err).Error("Failed to serialize metadata") + return nil, "", err + } + } + + app := App{Name: name, NostrPubkey: pairingPublicKey, Isolated: isolated, Metadata: datatypes.JSON(metadataBytes)} err := svc.db.Transaction(func(tx *gorm.DB) error { err := tx.Save(&app).Error diff --git a/db/migrations/202408291715_app_metadata.go b/db/migrations/202408291715_app_metadata.go new file mode 100644 index 00000000..b0d2b063 --- /dev/null +++ b/db/migrations/202408291715_app_metadata.go @@ -0,0 +1,25 @@ +package migrations + +import ( + _ "embed" + + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" +) + +var _202408291715_app_metadata = &gormigrate.Migration{ + ID: "202408291715_app_metadata", + Migrate: func(tx *gorm.DB) error { + + if err := tx.Exec(` + ALTER TABLE apps ADD COLUMN metadata JSON; +`).Error; err != nil { + return err + } + + return nil + }, + Rollback: func(tx *gorm.DB) error { + return nil + }, +} diff --git a/db/migrations/migrate.go b/db/migrations/migrate.go index 89842711..00fd48bb 100644 --- a/db/migrations/migrate.go +++ b/db/migrations/migrate.go @@ -21,6 +21,7 @@ func Migrate(gormDB *gorm.DB) error { _202407262257_remove_invalid_scopes, _202408061737_add_boostagrams_and_use_json, _202408191242_transaction_failure_reason, + _202408291715_app_metadata, }) return m.Migrate() diff --git a/db/models.go b/db/models.go index 869a8f4f..3126ed8c 100644 --- a/db/models.go +++ b/db/models.go @@ -23,6 +23,7 @@ type App struct { CreatedAt time.Time UpdatedAt time.Time Isolated bool + Metadata datatypes.JSON } type AppPermission struct { @@ -86,7 +87,7 @@ type Transaction struct { } type DBService interface { - CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string, isolated bool) (*App, string, error) + CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string, isolated bool, metadata map[string]interface{}) (*App, string, error) } const ( diff --git a/fly.toml b/fly.toml index 9bb1f1df..af8cbb75 100644 --- a/fly.toml +++ b/fly.toml @@ -6,6 +6,7 @@ app = 'nwc' primary_region = 'lax' swap_size_mb = 2048 +kill_timeout = 120 [build] image = 'ghcr.io/getalby/hub:latest' diff --git a/frontend/src/assets/suggested-apps/alby.png b/frontend/src/assets/suggested-apps/alby.png index 144fe016..d85407a9 100644 Binary files a/frontend/src/assets/suggested-apps/alby.png and b/frontend/src/assets/suggested-apps/alby.png differ diff --git a/frontend/src/assets/suggested-apps/amethyst.png b/frontend/src/assets/suggested-apps/amethyst.png index a47ffdb3..f1c5d0b6 100644 Binary files a/frontend/src/assets/suggested-apps/amethyst.png and b/frontend/src/assets/suggested-apps/amethyst.png differ diff --git a/frontend/src/assets/suggested-apps/damus.png b/frontend/src/assets/suggested-apps/damus.png index 3c9a0a24..88293c46 100644 Binary files a/frontend/src/assets/suggested-apps/damus.png and b/frontend/src/assets/suggested-apps/damus.png differ diff --git a/frontend/src/assets/suggested-apps/habla-news.png b/frontend/src/assets/suggested-apps/habla-news.png index 056989ff..6e44d75d 100644 Binary files a/frontend/src/assets/suggested-apps/habla-news.png and b/frontend/src/assets/suggested-apps/habla-news.png differ diff --git a/frontend/src/assets/suggested-apps/kiwi.png b/frontend/src/assets/suggested-apps/kiwi.png index d3fcb0ec..edb11323 100644 Binary files a/frontend/src/assets/suggested-apps/kiwi.png and b/frontend/src/assets/suggested-apps/kiwi.png differ diff --git a/frontend/src/assets/suggested-apps/lume.png b/frontend/src/assets/suggested-apps/lume.png index 44bbc7ed..2c8c8f77 100644 Binary files a/frontend/src/assets/suggested-apps/lume.png and b/frontend/src/assets/suggested-apps/lume.png differ diff --git a/frontend/src/assets/suggested-apps/nostrudel.png b/frontend/src/assets/suggested-apps/nostrudel.png index d79e6756..26371f6b 100644 Binary files a/frontend/src/assets/suggested-apps/nostrudel.png and b/frontend/src/assets/suggested-apps/nostrudel.png differ diff --git a/frontend/src/assets/suggested-apps/nostur.png b/frontend/src/assets/suggested-apps/nostur.png index 2a5fd192..704e023f 100644 Binary files a/frontend/src/assets/suggested-apps/nostur.png and b/frontend/src/assets/suggested-apps/nostur.png differ diff --git a/frontend/src/assets/suggested-apps/paper-scissors-hodl.png b/frontend/src/assets/suggested-apps/paper-scissors-hodl.png index 27e2b15f..4fe1fd0c 100644 Binary files a/frontend/src/assets/suggested-apps/paper-scissors-hodl.png and b/frontend/src/assets/suggested-apps/paper-scissors-hodl.png differ diff --git a/frontend/src/assets/suggested-apps/primal.png b/frontend/src/assets/suggested-apps/primal.png index c7774153..2cac98c7 100644 Binary files a/frontend/src/assets/suggested-apps/primal.png and b/frontend/src/assets/suggested-apps/primal.png differ diff --git a/frontend/src/assets/suggested-apps/snort.png b/frontend/src/assets/suggested-apps/snort.png index c4ec14d1..ad2d88a2 100644 Binary files a/frontend/src/assets/suggested-apps/snort.png and b/frontend/src/assets/suggested-apps/snort.png differ diff --git a/frontend/src/assets/suggested-apps/stacker-news.png b/frontend/src/assets/suggested-apps/stacker-news.png new file mode 100644 index 00000000..7e494294 Binary files /dev/null and b/frontend/src/assets/suggested-apps/stacker-news.png differ diff --git a/frontend/src/assets/suggested-apps/stackernews.png b/frontend/src/assets/suggested-apps/stackernews.png deleted file mode 100644 index d2c5cd0d..00000000 Binary files a/frontend/src/assets/suggested-apps/stackernews.png and /dev/null differ diff --git a/frontend/src/assets/suggested-apps/uncle-jim.png b/frontend/src/assets/suggested-apps/uncle-jim.png index 851109fb..f4269a30 100644 Binary files a/frontend/src/assets/suggested-apps/uncle-jim.png and b/frontend/src/assets/suggested-apps/uncle-jim.png differ diff --git a/frontend/src/assets/suggested-apps/wavelake.png b/frontend/src/assets/suggested-apps/wavelake.png deleted file mode 100644 index 096dd952..00000000 Binary files a/frontend/src/assets/suggested-apps/wavelake.png and /dev/null differ diff --git a/frontend/src/assets/suggested-apps/wavlake.png b/frontend/src/assets/suggested-apps/wavlake.png new file mode 100644 index 00000000..5cd433af Binary files /dev/null and b/frontend/src/assets/suggested-apps/wavlake.png differ diff --git a/frontend/src/assets/suggested-apps/wherostr.png b/frontend/src/assets/suggested-apps/wherostr.png index dffe3542..70e776a6 100644 Binary files a/frontend/src/assets/suggested-apps/wherostr.png and b/frontend/src/assets/suggested-apps/wherostr.png differ diff --git a/frontend/src/assets/suggested-apps/yakihonne.png b/frontend/src/assets/suggested-apps/yakihonne.png index 96255ca1..06eb2af6 100644 Binary files a/frontend/src/assets/suggested-apps/yakihonne.png and b/frontend/src/assets/suggested-apps/yakihonne.png differ diff --git a/frontend/src/assets/suggested-apps/zap-stream.png b/frontend/src/assets/suggested-apps/zap-stream.png index 8d0102e9..7de66f44 100644 Binary files a/frontend/src/assets/suggested-apps/zap-stream.png and b/frontend/src/assets/suggested-apps/zap-stream.png differ diff --git a/frontend/src/assets/suggested-apps/zapplanner.png b/frontend/src/assets/suggested-apps/zapplanner.png index f41d987e..45b25ac8 100644 Binary files a/frontend/src/assets/suggested-apps/zapplanner.png and b/frontend/src/assets/suggested-apps/zapplanner.png differ diff --git a/frontend/src/assets/suggested-apps/zapple-pay.png b/frontend/src/assets/suggested-apps/zapple-pay.png index 1d541846..5e32abce 100644 Binary files a/frontend/src/assets/suggested-apps/zapple-pay.png and b/frontend/src/assets/suggested-apps/zapple-pay.png differ diff --git a/frontend/src/assets/suggested-apps/zappy-bird.png b/frontend/src/assets/suggested-apps/zappy-bird.png index 13a7f095..d0dc5b69 100644 Binary files a/frontend/src/assets/suggested-apps/zappy-bird.png and b/frontend/src/assets/suggested-apps/zappy-bird.png differ diff --git a/frontend/src/components/AppAvatar.tsx b/frontend/src/components/AppAvatar.tsx index 8f200dac..21f4b735 100644 --- a/frontend/src/components/AppAvatar.tsx +++ b/frontend/src/components/AppAvatar.tsx @@ -1,27 +1,48 @@ +import { suggestedApps } from "src/components/SuggestedAppData"; +import UserAvatar from "src/components/UserAvatar"; import { cn } from "src/lib/utils"; +import { App } from "src/types"; type Props = { - appName: string; + app: App; className?: string; }; -export default function AppAvatar({ appName, className }: Props) { +export default function AppAvatar({ app, className }: Props) { + if (app.name === "getalby.com") { + return ; + } + const appStoreApp = app?.metadata?.app_store_app_id + ? suggestedApps.find( + (suggestedApp) => suggestedApp.id === app.metadata?.app_store_app_id + ) + : undefined; + const image = appStoreApp?.logo; + const gradient = - appName + app.name .split("") .map((c) => c.charCodeAt(0)) .reduce((a, b) => a + b, 0) % 10; return (
- - {appName.charAt(0)} - + {image && ( + + )} + {!image && ( + + {app.name.charAt(0)} + + )}
); } diff --git a/frontend/src/components/SuggestedAppData.tsx b/frontend/src/components/SuggestedAppData.tsx index cadfa6b9..083f8905 100644 --- a/frontend/src/components/SuggestedAppData.tsx +++ b/frontend/src/components/SuggestedAppData.tsx @@ -9,9 +9,9 @@ import nostur from "src/assets/suggested-apps/nostur.png"; import paperScissorsHodl from "src/assets/suggested-apps/paper-scissors-hodl.png"; import primal from "src/assets/suggested-apps/primal.png"; import snort from "src/assets/suggested-apps/snort.png"; -import stackernews from "src/assets/suggested-apps/stackernews.png"; +import stackernews from "src/assets/suggested-apps/stacker-news.png"; import uncleJim from "src/assets/suggested-apps/uncle-jim.png"; -import wavelake from "src/assets/suggested-apps/wavelake.png"; +import wavlake from "src/assets/suggested-apps/wavlake.png"; import wherostr from "src/assets/suggested-apps/wherostr.png"; import yakihonne from "src/assets/suggested-apps/yakihonne.png"; import zapstream from "src/assets/suggested-apps/zap-stream.png"; @@ -85,7 +85,7 @@ export const suggestedApps: SuggestedApp[] = [ title: "Wavlake", description: "Creators platform", webLink: "https://www.wavlake.com/", - logo: wavelake, + logo: wavlake, }, { id: "snort", diff --git a/frontend/src/components/SuggestedApps.tsx b/frontend/src/components/SuggestedApps.tsx index c6cb7841..b412a84e 100644 --- a/frontend/src/components/SuggestedApps.tsx +++ b/frontend/src/components/SuggestedApps.tsx @@ -1,4 +1,4 @@ -import { Globe } from "lucide-react"; +import { ExternalLinkIcon, Globe } from "lucide-react"; import { Link } from "react-router-dom"; import ExternalLink from "src/components/ExternalLink"; import { AppleIcon } from "src/components/icons/Apple"; @@ -84,7 +84,7 @@ function InternalAppCard({ id, title, description, logo }: SuggestedApp) { diff --git a/frontend/src/components/TransactionItem.tsx b/frontend/src/components/TransactionItem.tsx index 22904214..ccbfe3c0 100644 --- a/frontend/src/components/TransactionItem.tsx +++ b/frontend/src/components/TransactionItem.tsx @@ -9,6 +9,7 @@ import { CopyIcon, } from "lucide-react"; import React from "react"; +import { Link } from "react-router-dom"; import AppAvatar from "src/components/AppAvatar"; import PodcastingInfo from "src/components/PodcastingInfo"; import { @@ -38,7 +39,10 @@ function TransactionItem({ tx }: Props) { const [showDetails, setShowDetails] = React.useState(false); const type = tx.type; const Icon = tx.type == "outgoing" ? ArrowUpIcon : ArrowDownIcon; - const app = tx.appId && apps?.find((app) => app.id === tx.appId); + const app = + tx.appId !== undefined + ? apps?.find((app) => app.id === tx.appId) + : undefined; const copy = (text: string) => { copyToClipboard(text, toast); @@ -56,36 +60,40 @@ function TransactionItem({ tx }: Props) { {/* flex wrap is used as a last resort to stop horizontal scrollbar on mobile. */}
- {app ? ( - - ) : ( -
+ - -
- )} + /> + {app && ( +
+ +
+ )} +
-
+

- {app ? app.name : type == "incoming" ? "Received" : "Sent"} + {type == "incoming" ? "Received" : "Sent"}

{dayjs(tx.settledAt).fromNow()} @@ -154,7 +162,23 @@ function TransactionItem({ tx }: Props) {

*/}
-
+ {app && ( +
+

App

+ +
+ +

+ {app.name === "getalby.com" ? "Alby Account" : app.name} +

+
+ +
+ )} +

Date & Time

{dayjs(tx.settledAt) diff --git a/frontend/src/components/TransferFundsButton.tsx b/frontend/src/components/TransferFundsButton.tsx new file mode 100644 index 00000000..91b24902 --- /dev/null +++ b/frontend/src/components/TransferFundsButton.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import { ButtonProps, LoadingButton } from "src/components/ui/loading-button"; +import { useToast } from "src/components/ui/use-toast"; +import { AlbyBalance, Channel } from "src/types"; +import { request } from "src/utils/request"; + +type TransferFundsButtonProps = { + channels: Channel[] | undefined; + albyBalance: AlbyBalance; + reloadAlbyBalance: () => void; +} & ButtonProps; + +export function TransferFundsButton({ + channels, + albyBalance, + reloadAlbyBalance, + children, + ...props +}: TransferFundsButtonProps) { + const [loading, setLoading] = React.useState(false); + + const { toast } = useToast(); + + return ( + { + if (!albyBalance) { + return; + } + if ( + !channels?.some( + (channel) => channel.remoteBalance / 1000 > albyBalance.sats + ) + ) { + toast({ + title: "Please increase your receiving capacity first", + }); + return; + } + setLoading(true); + try { + await request("/api/alby/drain", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + await reloadAlbyBalance(); + toast({ + title: + "🎉 Funds from Alby shared wallet transferred to your Alby Hub!", + }); + } catch (error) { + console.error(error); + toast({ + variant: "destructive", + description: "Something went wrong: " + error, + }); + } finally { + setLoading(false); + } + }} + {...props} + > + {children} + + ); +} diff --git a/frontend/src/components/connections/AppCard.tsx b/frontend/src/components/connections/AppCard.tsx index 55eb2656..ba18e57d 100644 --- a/frontend/src/components/connections/AppCard.tsx +++ b/frontend/src/components/connections/AppCard.tsx @@ -30,7 +30,7 @@ export default function AppCard({ app }: Props) {

- +
{app.name}
diff --git a/frontend/src/components/icons/SelfCustodyIcon.tsx b/frontend/src/components/icons/SelfCustodyIcon.tsx new file mode 100644 index 00000000..49e8b6d0 --- /dev/null +++ b/frontend/src/components/icons/SelfCustodyIcon.tsx @@ -0,0 +1,48 @@ +import { createLucideIcon } from "lucide-react"; + +export const SelfCustodyIcon = createLucideIcon("RowIcon", [ + [ + "path", + { + d: "M29.8624 25.3804C20.5432 25.3804 12.6577 31.14 9.96325 39.1491C9.07335 41.72 10.5812 44.4144 13.1026 45.4526C18.4173 47.6032 24.1275 48.7156 29.8624 48.7156C35.5974 48.7156 41.2829 47.6032 46.6223 45.4526C49.1437 44.4144 50.6516 41.72 49.7617 39.1491C47.0672 31.14 39.157 25.3804 29.8624 25.3804ZM22.6444 41.72C19.9499 41.72 17.7746 39.9896 17.7746 37.839C17.7746 35.6884 19.9746 33.958 22.6444 33.958C25.3141 33.958 27.5141 35.6884 27.5141 37.839C27.5141 39.9896 25.3388 41.72 22.6444 41.72ZM36.5614 41.72C33.8917 41.72 31.7164 39.9896 31.7164 37.839C31.7164 35.6884 33.867 33.958 36.5614 33.958C39.2559 33.958 41.4312 35.6884 41.4312 37.839C41.4312 39.9896 39.2559 41.72 36.5614 41.72ZM48.4021 21.5983L50.6516 19.3488C52.5055 20.387 54.7797 20.3376 56.5842 19.2005C59.3281 17.4949 60.1438 13.9105 58.4382 11.1667C58.0332 10.5165 57.5039 9.95274 56.8806 9.50756C56.2574 9.06237 55.5524 8.74455 54.8061 8.57232C54.0597 8.4001 53.2868 8.37684 52.5314 8.50389C51.7761 8.63094 51.0533 8.90579 50.4044 9.3127C49.753 9.71638 49.1876 10.2445 48.7405 10.867C48.2935 11.4894 47.9736 12.1938 47.7991 12.94C47.6246 13.6862 47.5989 14.4595 47.7236 15.2156C47.8483 15.9718 48.1208 16.6959 48.5257 17.3465L46.2515 19.6207C41.6289 15.8387 35.9929 13.5892 29.8624 13.5892C23.732 13.5892 17.9724 15.8881 13.3004 19.7691L10.8284 17.2971C11.9408 15.4679 11.9655 13.1937 10.9026 11.3397C10.5215 10.6755 10.0133 10.0929 9.40685 9.62527C8.80045 9.15763 8.10782 8.81413 7.36856 8.61439C6.6293 8.41465 5.85792 8.3626 5.09853 8.46122C4.33913 8.55983 3.60663 8.80718 2.94291 9.18911C2.27734 9.56882 1.6931 10.0759 1.22356 10.6815C0.75402 11.287 0.408375 11.9792 0.206369 12.7183C0.00436257 13.4575 -0.0500464 14.2292 0.0462495 14.9894C0.142545 15.7496 0.387659 16.4833 0.767591 17.1488C1.53923 18.4947 2.81319 19.4795 4.31002 19.8873C5.80684 20.2951 7.40431 20.0926 8.75199 19.3241L11.1745 21.7466C6.40364 26.5422 3.01707 33.1423 1.70693 40.5334C1.01479 44.6369 3.11594 48.6661 6.87331 50.3965C14.0914 53.8325 21.9769 55.6123 29.9613 55.5876C37.8716 55.5876 45.6829 53.8572 52.8269 50.4954C54.423 49.7664 55.7754 48.5931 56.7223 47.1158C57.6692 45.6385 58.1705 43.9197 58.1663 42.1649C58.1663 41.6705 58.1416 41.1514 58.0427 40.6323C56.7078 33.1176 53.2718 26.4186 48.4021 21.5983ZM51.5909 47.8751C44.8278 51.0663 37.4395 52.7128 29.9613 52.6954C22.3972 52.7202 14.9319 51.0392 8.134 47.7762C6.88628 47.2138 5.86014 46.2537 5.21608 45.0461C4.57202 43.8385 4.34636 42.4515 4.57439 41.102C7.1205 26.8635 17.6263 16.5308 29.8624 16.5308C42.0986 16.5308 52.6538 26.913 55.1752 41.1761C55.6449 43.9447 54.1864 46.6639 51.5909 47.8751Z", + fill: "currentColor", + }, + ], + [ + "path", + { + d: "M104.44 22.7127C104.857 22.2961 105.532 22.2961 105.949 22.7127L114.482 31.246C114.899 31.6626 114.899 32.338 114.482 32.7545L105.949 41.288C105.532 41.7044 104.857 41.7044 104.44 41.288C104.024 40.8713 104.024 40.1959 104.44 39.7793L111.153 33.067H92.3947C91.8056 33.067 91.328 32.5894 91.328 32.0003C91.328 31.4112 91.8056 30.9336 92.3947 30.9336H111.153L104.44 24.2212C104.024 23.8046 104.024 23.1293 104.44 22.7127Z", + fill: "currentColor", + fillRule: "evenodd", + clipRule: "evenodd", + }, + ], + [ + "path", + { + d: "M193.696 28.5841C193.459 27.5279 193.107 26.5165 192.659 25.5564C192.16 24.481 191.539 23.4696 190.809 22.5414C190.054 21.5813 189.19 20.7171 188.231 19.9618C187.303 19.232 186.292 18.6111 185.216 18.1118C184.257 17.6638 183.245 17.3181 182.19 17.0749C181.095 16.8188 179.95 16.6908 178.779 16.6908C177.608 16.6908 176.462 16.8188 175.368 17.0749C175.208 17.8622 175.054 18.6879 174.92 19.5521C174.67 21.0884 174.465 22.7463 174.306 24.5322C174.229 25.4283 174.165 26.3629 174.114 27.3295C174.037 28.8081 173.992 30.3636 173.992 32.0023C173.992 33.641 174.037 35.1901 174.114 36.6751C175.592 36.752 177.147 36.7968 178.785 36.7968C180.423 36.7968 181.972 36.752 183.457 36.6751C184.417 36.6239 185.351 36.5599 186.247 36.4831C188.039 36.3231 189.696 36.1182 191.232 35.8686C192.096 35.7278 192.921 35.5805 193.708 35.4205C193.958 34.3259 194.092 33.1801 194.092 32.0087C194.092 30.8373 193.964 29.6915 193.708 28.5969L193.696 28.5841ZM189.261 27.4255C188.87 27.4831 188.493 27.5791 188.122 27.7071C187.456 27.9376 186.842 28.2769 186.285 28.7057C184.896 29.7619 183.898 31.3494 183.61 33.0585C183.61 33.0905 183.578 33.1033 183.559 33.0969C183.54 33.0969 183.521 33.0777 183.521 33.0585C183.066 30.146 180.711 27.8736 177.87 27.4191C177.819 27.4127 177.819 27.3359 177.87 27.3295C178.068 27.2975 178.26 27.2591 178.452 27.2079C180.052 26.8046 181.441 25.8124 182.369 24.4938C182.669 24.0713 182.919 23.6168 183.117 23.1303C183.309 22.6631 183.45 22.1702 183.527 21.6645C183.533 21.6133 183.61 21.6133 183.617 21.6645C183.789 22.7655 184.237 23.7705 184.871 24.6282C185.236 25.1211 185.658 25.5564 186.138 25.934C187.034 26.6446 188.103 27.1374 189.261 27.3359C189.312 27.3423 189.312 27.4191 189.261 27.4255Z", + fill: "currentColor", + }, + ], + [ + "path", + { + d: "M210.795 30.9087L210.769 33.0915C210.769 36.3561 207.256 39.3262 200.882 41.4514C198.636 42.2003 196.108 42.8212 193.388 43.3077C193.19 43.3461 192.992 43.3781 192.793 43.4165C192.051 43.5382 191.302 43.6534 190.528 43.7558C189.773 43.8582 189.005 43.9478 188.231 44.031C187.322 44.1271 186.407 44.2103 185.472 44.2807C184.538 44.3447 183.597 44.4023 182.637 44.4407C181.364 44.4919 180.078 44.5175 178.779 44.5175C177.48 44.5175 176.187 44.4919 174.92 44.4407C173.96 44.3959 173.019 44.3447 172.085 44.2807C171.151 44.2103 170.236 44.1335 169.333 44.031C168.552 43.9478 167.791 43.8582 167.036 43.7558C166.268 43.6534 165.513 43.5382 164.777 43.4165C164.572 43.3845 164.374 43.3461 164.175 43.3077C161.456 42.8276 158.928 42.2067 156.682 41.4514C150.308 39.3262 146.795 36.3561 146.795 33.0915V30.8959C146.795 27.6313 150.308 24.6612 156.682 22.536C158.928 21.7871 161.456 21.1662 164.175 20.6797C164.143 20.9486 164.111 21.211 164.079 21.4862C163.913 22.8881 163.785 24.3155 163.695 25.775C162.991 25.9158 162.313 26.0567 161.654 26.2167C155.447 27.6761 151.345 29.8397 150.282 31.9969C151.345 34.1541 155.447 36.3177 161.654 37.7707C162.313 37.9244 162.991 38.0716 163.695 38.2124C164.092 38.2892 164.495 38.366 164.911 38.4365C165.436 38.5261 165.967 38.6157 166.511 38.6925C168.092 38.9293 169.762 39.1278 171.515 39.2686C172.431 39.3454 173.365 39.4094 174.318 39.4542C175.765 39.5246 177.262 39.5631 178.798 39.5631C180.334 39.5631 181.825 39.5246 183.277 39.4542C184.231 39.4094 185.165 39.3454 186.074 39.2686C187.827 39.1214 189.498 38.9293 191.078 38.6925C191.622 38.6093 192.16 38.5261 192.678 38.4365C193.088 38.366 193.491 38.2892 193.894 38.2124C194.598 38.078 195.276 37.9308 195.935 37.7771C202.143 36.3177 206.245 34.1541 207.307 31.9969C206.245 29.8397 202.143 27.6761 195.935 26.2231C195.359 24.514 194.54 22.9265 193.51 21.4926C193.286 21.179 193.056 20.8781 192.812 20.5837C193.011 20.6157 193.216 20.6541 193.414 20.6925C196.134 21.1726 198.661 21.7935 200.908 22.5488C207.281 24.674 210.795 27.6441 210.795 30.9087Z", + fill: "currentColor", + }, + ], + [ + "path", + { + d: "M190.182 17.9744C189.888 17.7311 189.587 17.5007 189.274 17.2767C187.84 16.2461 186.247 15.4267 184.545 14.8506C183.092 8.64153 180.929 4.5384 178.772 3.47582C176.616 4.5384 174.453 8.64153 173 14.8506C172.84 15.51 172.699 16.1885 172.559 16.8926C172.482 17.2895 172.405 17.6991 172.335 18.1088C172.245 18.6337 172.155 19.165 172.079 19.7091C171.842 21.2902 171.643 22.9609 171.503 24.7148C171.426 25.6237 171.362 26.5583 171.317 27.5121C171.247 28.9587 171.208 30.4566 171.208 31.9929C171.208 33.5291 171.247 35.0206 171.317 36.4737C169.525 36.3136 167.868 36.1088 166.338 35.8592C166.287 34.5853 166.262 33.2987 166.262 31.9993C166.262 30.6998 166.287 29.4068 166.338 28.1394C166.383 27.1792 166.434 26.2383 166.505 25.3037C166.569 24.3691 166.652 23.4474 166.748 22.5512C166.831 21.7703 166.921 21.0021 167.023 20.2468C167.125 19.4787 167.241 18.7233 167.362 17.9872C167.394 17.7888 167.433 17.5839 167.471 17.3855C167.951 14.665 168.572 12.1366 169.327 9.88975C171.451 3.51422 174.421 0 177.684 0H179.879C183.143 0 186.112 3.51422 188.237 9.88975C188.986 12.1366 189.606 14.665 190.093 17.3855C190.131 17.5839 190.163 17.7888 190.202 17.9872L190.182 17.9744Z", + fill: "currentColor", + }, + ], + [ + "path", + { + d: "M190.08 46.6151C189.601 49.3356 188.98 51.864 188.225 54.1108C186.1 60.4863 183.131 64.0006 179.867 64.0006H177.672C174.408 64.0006 171.439 60.4863 169.315 54.1108C168.566 51.864 167.945 49.3356 167.459 46.6151C167.728 46.6471 167.99 46.6791 168.265 46.7111C169.667 46.8775 171.094 47.0055 172.553 47.0952C172.693 47.7993 172.834 48.4778 172.994 49.1371C174.447 55.3462 176.61 59.4494 178.766 60.5119C180.923 59.4494 183.086 55.3462 184.545 49.1371C184.699 48.4778 184.846 47.7993 184.98 47.0952C186.439 46.9991 187.873 46.8711 189.268 46.7111C189.537 46.6855 189.805 46.6471 190.074 46.6151H190.08Z", + fill: "currentColor", + }, + ], +]); diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 6decd5cb..843a0439 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -27,6 +27,9 @@ import Channels from "src/screens/channels/Channels"; import { CurrentChannelOrder } from "src/screens/channels/CurrentChannelOrder"; import IncreaseIncomingCapacity from "src/screens/channels/IncreaseIncomingCapacity"; import IncreaseOutgoingCapacity from "src/screens/channels/IncreaseOutgoingCapacity"; +import { AutoChannel } from "src/screens/channels/auto/AutoChannel"; +import { OpenedAutoChannel } from "src/screens/channels/auto/OpenedAutoChannel"; +import { OpeningAutoChannel } from "src/screens/channels/auto/OpeningAutoChannel"; import { FirstChannel } from "src/screens/channels/first/FirstChannel"; import { OpenedFirstChannel } from "src/screens/channels/first/OpenedFirstChannel"; import { OpeningFirstChannel } from "src/screens/channels/first/OpeningFirstChannel"; @@ -233,18 +236,39 @@ const routes = [ }, { path: "first", - element: , - handle: { crumb: () => "Open Your First Channel" }, - }, - { - path: "first/opening", - element: , - handle: { crumb: () => "Opening Your First Channel" }, + handle: { crumb: () => "Your First Channel" }, + children: [ + { + index: true, + element: , + }, + { + path: "opening", + element: , + }, + { + path: "opened", + element: , + }, + ], }, { - path: "first/opened", - element: , - handle: { crumb: () => "First Channel Opened!" }, + path: "auto", + handle: { crumb: () => "New Channel" }, + children: [ + { + index: true, + element: , + }, + { + path: "opening", + element: , + }, + { + path: "opened", + element: , + }, + ], }, { path: "outgoing", diff --git a/frontend/src/screens/Intro.tsx b/frontend/src/screens/Intro.tsx index 9bd1aa2c..58490dcb 100644 --- a/frontend/src/screens/Intro.tsx +++ b/frontend/src/screens/Intro.tsx @@ -10,6 +10,7 @@ import React, { ReactElement } from "react"; import { useNavigate } from "react-router-dom"; import Cloud from "src/assets/images/cloud.png"; import Cloud2 from "src/assets/images/cloud2.png"; +import { SelfCustodyIcon } from "src/components/icons/SelfCustodyIcon"; import { Button } from "src/components/ui/button"; import { Carousel, @@ -87,6 +88,14 @@ export function Intro() {
+ + + - + {Icon === SelfCustodyIcon ? ( + + ) : ( + + )}
{title} diff --git a/frontend/src/screens/alby/AlbyAuthRedirect.tsx b/frontend/src/screens/alby/AlbyAuthRedirect.tsx index c3ae34fa..d3702e6e 100644 --- a/frontend/src/screens/alby/AlbyAuthRedirect.tsx +++ b/frontend/src/screens/alby/AlbyAuthRedirect.tsx @@ -11,7 +11,17 @@ export default function AlbyAuthRedirect() { const queryParams = new URLSearchParams(location.search); const forceLogin = !!queryParams.get("force_login"); const url = info?.albyAuthUrl - ? `${info.albyAuthUrl}${forceLogin ? "&force_login=true" : ""}` + ? (() => { + const _url = new URL(info.albyAuthUrl); + if (forceLogin) { + _url.searchParams.append("force_login", "true"); + } + if (info.albyUserIdentifier) { + _url.searchParams.append("identifier", info.albyUserIdentifier); + } + + return _url.toString(); + })() : undefined; React.useEffect(() => { diff --git a/frontend/src/screens/apps/NewApp.tsx b/frontend/src/screens/apps/NewApp.tsx index a1c42b38..278db7b2 100644 --- a/frontend/src/screens/apps/NewApp.tsx +++ b/frontend/src/screens/apps/NewApp.tsx @@ -53,13 +53,15 @@ const NewAppInternal = ({ capabilities }: NewAppInternalProps) => { const queryParams = new URLSearchParams(location.search); const appId = queryParams.get("app") ?? ""; - const app = suggestedApps.find((app) => app.id === appId); + const appStoreApp = suggestedApps.find((app) => app.id === appId); const pubkey = queryParams.get("pubkey") ?? ""; const returnTo = queryParams.get("return_to") ?? ""; const nameParam = (queryParams.get("name") || queryParams.get("c")) ?? ""; - const [appName, setAppName] = useState(app ? app.title : nameParam); + const [appName, setAppName] = useState( + appStoreApp ? appStoreApp.title : nameParam + ); const budgetRenewalParam = queryParams.get( "budget_renewal" @@ -197,6 +199,9 @@ const NewAppInternal = ({ capabilities }: NewAppInternalProps) => { expiresAt: permissions.expiresAt?.toISOString(), returnTo: returnTo, isolated: permissions.isolated, + metadata: { + app_store_app_id: appStoreApp?.id, + }, }; const createAppResponse = await request("/api/apps", { @@ -216,7 +221,7 @@ const NewAppInternal = ({ capabilities }: NewAppInternalProps) => { window.location.href = createAppResponse.returnTo; return; } - navigate(`/apps/created${app ? `?app=${app.id}` : ""}`, { + navigate(`/apps/created${appStoreApp ? `?app=${appStoreApp.id}` : ""}`, { state: createAppResponse, }); toast({ title: "App created" }); @@ -246,10 +251,10 @@ const NewAppInternal = ({ capabilities }: NewAppInternalProps) => { acceptCharset="UTF-8" className="flex flex-col items-start gap-5 max-w-lg" > - {app && ( + {appStoreApp && (
- -

{app.title}

+ +

{appStoreApp.title}

)} {!nameParam && ( diff --git a/frontend/src/screens/apps/ShowApp.tsx b/frontend/src/screens/apps/ShowApp.tsx index 7f405dca..0ebc8c5e 100644 --- a/frontend/src/screens/apps/ShowApp.tsx +++ b/frontend/src/screens/apps/ShowApp.tsx @@ -139,6 +139,8 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) { } }; + const appName = app.name === "getalby.com" ? "Alby Account" : app.name; + return ( <>
@@ -146,7 +148,7 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) { - + {isEditingName ? (
setIsEditingName(true)} >

- {app.name} + {appName}

- + {app.name !== "getalby.com" && ( + + )}
)}
diff --git a/frontend/src/screens/channels/Channels.tsx b/frontend/src/screens/channels/Channels.tsx index 1e3136fe..0d019772 100644 --- a/frontend/src/screens/channels/Channels.tsx +++ b/frontend/src/screens/channels/Channels.tsx @@ -17,6 +17,7 @@ import { ChannelsCards } from "src/components/channels/ChannelsCards.tsx"; import { ChannelsTable } from "src/components/channels/ChannelsTable.tsx"; import EmptyState from "src/components/EmptyState.tsx"; import ExternalLink from "src/components/ExternalLink"; +import { TransferFundsButton } from "src/components/TransferFundsButton"; import { Alert, AlertDescription, @@ -39,7 +40,6 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "src/components/ui/dropdown-menu.tsx"; -import { LoadingButton } from "src/components/ui/loading-button.tsx"; import { CircleProgress } from "src/components/ui/progress.tsx"; import { Tooltip, @@ -73,8 +73,6 @@ export default function Channels() { const [nodes, setNodes] = React.useState([]); const { toast } = useToast(); - const [drainingAlbySharedFunds, setDrainingAlbySharedFunds] = - React.useState(false); const isDesktop = useIsDesktop(); const nodeHealth = channels ? getNodeHealth(channels) : 0; @@ -252,51 +250,18 @@ export default function Channels() {
- {new Intl.NumberFormat().format(albyBalance?.sats)} sats + {new Intl.NumberFormat().format(albyBalance.sats)} sats
- { - if ( - !channels?.some( - (channel) => - channel.remoteBalance / 1000 > albyBalance.sats - ) - ) { - toast({ - title: "Please increase your receiving capacity first", - }); - return; - } - - setDrainingAlbySharedFunds(true); - try { - await request("/api/alby/drain", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }); - await reloadAlbyBalance(); - toast({ - description: - "🎉 Funds from Alby shared wallet transferred to your Alby Hub!", - }); - } catch (error) { - console.error(error); - toast({ - variant: "destructive", - description: "Something went wrong: " + error, - }); - } - setDrainingAlbySharedFunds(false); - }} + Transfer - + )} diff --git a/frontend/src/screens/channels/auto/AutoChannel.tsx b/frontend/src/screens/channels/auto/AutoChannel.tsx new file mode 100644 index 00000000..82ae82a8 --- /dev/null +++ b/frontend/src/screens/channels/auto/AutoChannel.tsx @@ -0,0 +1,201 @@ +import { Payment } from "@getalby/bitcoin-connect-react"; +import { ChevronDown } from "lucide-react"; +import React from "react"; +import { Link, useNavigate } from "react-router-dom"; +import AppHeader from "src/components/AppHeader"; +import ExternalLink from "src/components/ExternalLink"; +import Loading from "src/components/Loading"; +import { Button } from "src/components/ui/button"; +import { Checkbox } from "src/components/ui/checkbox"; +import { Label } from "src/components/ui/label"; +import { LoadingButton } from "src/components/ui/loading-button"; +import { Separator } from "src/components/ui/separator"; +import { useToast } from "src/components/ui/use-toast"; +import { useChannels } from "src/hooks/useChannels"; + +import { useInfo } from "src/hooks/useInfo"; +import { AutoChannelRequest, AutoChannelResponse } from "src/types"; +import { request } from "src/utils/request"; + +import { MempoolAlert } from "src/components/MempoolAlert"; + +export function AutoChannel() { + const { data: info } = useInfo(); + const { data: channels } = useChannels(true); + const [isLoading, setLoading] = React.useState(false); + const [showAdvanced, setShowAdvanced] = React.useState(false); + const [isPublic, setPublic] = React.useState(false); + + const navigate = useNavigate(); + const { toast } = useToast(); + const [invoice, setInvoice] = React.useState(); + const [channelSize, setChannelSize] = React.useState(); + const [, setPrevChannelIds] = React.useState(); + + React.useEffect(() => { + if (channels) { + setPrevChannelIds((current) => { + if (current) { + const newChannelId = channels.find( + (channel) => !current?.includes(channel.id) && channel.fundingTxId + )?.id; + + if (newChannelId) { + console.info("Found new channel", newChannelId); + navigate("/channels/auto/opening", { + state: { + newChannelId, + }, + }); + } + + return current; + } + + return channels.map((channel) => channel.id); + }); + } + }, [channels, navigate]); + + if (!info || !channels) { + return ; + } + + async function openChannel() { + if (!info || !channels) { + return; + } + setLoading(true); + try { + const newInstantChannelInvoiceRequest: AutoChannelRequest = { + isPublic, + }; + const autoChannelResponse = await request( + "/api/alby/auto-channel", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(newInstantChannelInvoiceRequest), + } + ); + if (!autoChannelResponse) { + throw new Error("unexpected auto channel response"); + } + + setInvoice(autoChannelResponse.invoice); + setChannelSize(autoChannelResponse.channelSize); + } catch (error) { + setLoading(false); + console.error(error); + toast({ + title: "Something went wrong. Please try again", + variant: "destructive", + }); + } + } + + return ( + <> + + + {invoice && channelSize && ( +
+

+ Please pay the lightning invoice below which will cover the costs of + opening your channel. You will receive a channel with{" "} + {new Intl.NumberFormat().format(channelSize)} sats of incoming + liquidity. +

+ + + +

+ Other options +

+ + + + + + +
+ )} + {!invoice && ( + <> +
+ + + + <> +

+ You're now going to open a new lightning channel that you can + use to send and receive payments using your Hub in the booming + bitcoin economy! To make things easy, Alby has picked a channel + partner for you from one of our recommended channel partners. +

+

+ After paying a lightning invoice to cover on-chain fees, you'll + immediately be able to receive and send bitcoin through this + channel with your Hub. +

+ + {showAdvanced && ( + <> +
+ setPublic(!isPublic)} + className="mr-2" + /> +
+ +

+ Only enable if you want to receive keysend payments. (e.g. + podcasting) +

+
+
+ + )} + {!showAdvanced && ( +
+ +
+ )} + + Open Channel + +
+ + )} + + ); +} diff --git a/frontend/src/screens/channels/auto/OpenedAutoChannel.tsx b/frontend/src/screens/channels/auto/OpenedAutoChannel.tsx new file mode 100644 index 00000000..4b3d7f93 --- /dev/null +++ b/frontend/src/screens/channels/auto/OpenedAutoChannel.tsx @@ -0,0 +1,53 @@ +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 OpenedAutoChannel() { + 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 lightning 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/channels/auto/OpeningAutoChannel.tsx b/frontend/src/screens/channels/auto/OpeningAutoChannel.tsx new file mode 100644 index 00000000..83b8f4a5 --- /dev/null +++ b/frontend/src/screens/channels/auto/OpeningAutoChannel.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { ChannelWaitingForConfirmations } from "src/components/channels/ChannelWaitingForConfirmations"; +import { useChannels } from "src/hooks/useChannels"; +import { useSyncWallet } from "src/hooks/useSyncWallet"; + +export function OpeningAutoChannel() { + useSyncWallet(); + const { data: channels } = useChannels(true); + const navigate = useNavigate(); + + const { state } = useLocation(); + const newChannelId = state?.newChannelId as string | undefined; + + const channel = channels?.find( + (channel) => channel.id && channel.id === newChannelId + ); + + React.useEffect(() => { + if (channel?.active) { + navigate("/channels/auto/opened"); + } + }, [channel, navigate]); + + return ; +} diff --git a/frontend/src/screens/internal-apps/UncleJimApp.tsx b/frontend/src/screens/internal-apps/UncleJimApp.tsx index 06ad1fcb..93d26d9e 100644 --- a/frontend/src/screens/internal-apps/UncleJimApp.tsx +++ b/frontend/src/screens/internal-apps/UncleJimApp.tsx @@ -1,6 +1,7 @@ import { CopyIcon } from "lucide-react"; import React from "react"; import AppHeader from "src/components/AppHeader"; +import AppCard from "src/components/connections/AppCard"; import ExternalLink from "src/components/ExternalLink"; import { Accordion, @@ -54,6 +55,9 @@ export function UncleJimApp() { "pay_invoice", ], isolated: true, + metadata: { + app_store_app_id: "uncle-jim", + }, }; const createAppResponse = await request("/api/apps", { @@ -71,7 +75,7 @@ export function UncleJimApp() { setConnectionSecret(createAppResponse.pairingUri); setAppPublicKey(createAppResponse.pairingPublicKey); - toast({ title: "New wallet created for " + name }); + toast({ title: "New subaccount created for " + name }); } catch (error) { handleRequestError(toast, "Failed to create app", error); } @@ -83,6 +87,10 @@ export function UncleJimApp() { `; + const onboardedApps = apps?.filter( + (app) => app.metadata?.app_store_app_id === "uncle-jim" + ); + return (
- Create Wallet + Create Subaccount + + {!!onboardedApps?.length && ( + <> +

+ Great job! You've onboarded {onboardedApps.length} friends and + family members so far. +

+
+ {onboardedApps.map((app, index) => ( + + ))} +
{" "} + + )} )} {connectionSecret && ( diff --git a/frontend/src/screens/setup/SetupPassword.tsx b/frontend/src/screens/setup/SetupPassword.tsx index 0109f70a..94f91d4d 100644 --- a/frontend/src/screens/setup/SetupPassword.tsx +++ b/frontend/src/screens/setup/SetupPassword.tsx @@ -110,6 +110,7 @@ export function SetupPassword() {
setIsPasswordSecured2(!isPasswordSecured2) diff --git a/frontend/src/screens/wallet/WithdrawOnchainFunds.tsx b/frontend/src/screens/wallet/WithdrawOnchainFunds.tsx index e0cb8a6e..ae5bc780 100644 --- a/frontend/src/screens/wallet/WithdrawOnchainFunds.tsx +++ b/frontend/src/screens/wallet/WithdrawOnchainFunds.tsx @@ -139,7 +139,7 @@ export default function WithdrawOnchainFunds() { />
- {balances?.onchain.reserved && ( + {!!balances?.onchain.reserved && ( Channel Anchor Reserves will be depleted diff --git a/frontend/src/screens/wallet/index.tsx b/frontend/src/screens/wallet/index.tsx index 56706c22..ed68a252 100644 --- a/frontend/src/screens/wallet/index.tsx +++ b/frontend/src/screens/wallet/index.tsx @@ -8,28 +8,73 @@ import { Link } from "react-router-dom"; import AppHeader from "src/components/AppHeader"; import BreezRedeem from "src/components/BreezRedeem"; import ExternalLink from "src/components/ExternalLink"; +import { SelfCustodyIcon } from "src/components/icons/SelfCustodyIcon"; import Loading from "src/components/Loading"; import TransactionsList from "src/components/TransactionsList"; +import { TransferFundsButton } from "src/components/TransferFundsButton"; import { Alert, AlertDescription, AlertTitle, } from "src/components/ui/alert.tsx"; import { Button } from "src/components/ui/button"; +import { ALBY_HIDE_HOSTED_BALANCE_BELOW as ALBY_HIDE_HOSTED_BALANCE_LIMIT } from "src/constants.ts"; +import { useAlbyBalance } from "src/hooks/useAlbyBalance"; import { useBalances } from "src/hooks/useBalances"; +import { useChannels } from "src/hooks/useChannels"; import { useInfo } from "src/hooks/useInfo"; function Wallet() { const { data: info, hasChannelManagement } = useInfo(); const { data: balances } = useBalances(); + const { data: channels } = useChannels(); + const { data: albyBalance, mutate: reloadAlbyBalance } = useAlbyBalance(); if (!info || !balances) { return ; } + const showMigrateCard = + albyBalance && albyBalance.sats > ALBY_HIDE_HOSTED_BALANCE_LIMIT; + return ( <> + {showMigrateCard && ( +
+
+ +

+ Your funds ({new Intl.NumberFormat().format(albyBalance.sats)}{" "} + sats) are still hosted by Alby. +

+

+ {channels && channels.length > 0 + ? "Transfer funds from your Alby hosted balance to your self-custodial wallet." + : "Migrate funds from your Alby hosted balance to start using your self-custodial wallet."} +

+ {channels && channels.length > 0 ? ( + + Transfer Funds + + ) : ( + + + + )} +
+
+ )} {hasChannelManagement && !balances.lightning.totalSpendable && ( @@ -56,7 +101,7 @@ function Wallet() { )}
-
+
{new Intl.NumberFormat().format( Math.floor(balances.lightning.totalSpendable / 1000) )}{" "} diff --git a/frontend/src/screens/wallet/send/ConfirmPayment.tsx b/frontend/src/screens/wallet/send/ConfirmPayment.tsx index 262b0c9a..b67fbdf9 100644 --- a/frontend/src/screens/wallet/send/ConfirmPayment.tsx +++ b/frontend/src/screens/wallet/send/ConfirmPayment.tsx @@ -82,7 +82,7 @@ export default function ConfirmPayment() { )}
- + Confirm Payment diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 021f6880..b060cae9 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -124,6 +124,7 @@ export interface App { maxAmount: number; budgetUsage: number; budgetRenewal: BudgetRenewalType; + metadata?: AppMetadata; } export interface AppPermissions { @@ -151,6 +152,10 @@ export interface InfoResponse { export type Network = "bitcoin" | "testnet" | "signet"; +export type AppMetadata = + | Record + | { app_store_app_id?: string }; + export interface MnemonicResponse { mnemonic: string; } @@ -164,6 +169,7 @@ export interface CreateAppRequest { scopes: Scope[]; returnTo?: string; isolated?: boolean; + metadata?: AppMetadata; } export interface CreateAppResponse { diff --git a/go.mod b/go.mod index 99867516..e8f474bd 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/getAlby/ldk-node-go v0.0.0-20240815144818-6fa575b0a3f5 github.com/go-gormigrate/gormigrate/v2 v2.1.2 github.com/labstack/echo/v4 v4.12.0 - github.com/nbd-wtf/go-nostr v0.34.5 + github.com/nbd-wtf/go-nostr v0.34.10 github.com/nbd-wtf/ln-decodepay v1.12.1 github.com/orandin/lumberjackrus v1.0.1 github.com/stretchr/testify v1.9.0 @@ -18,7 +18,7 @@ require ( golang.org/x/crypto v0.26.0 golang.org/x/oauth2 v0.22.0 google.golang.org/grpc v1.65.0 - gopkg.in/DataDog/dd-trace-go.v1 v1.66.0 + gopkg.in/DataDog/dd-trace-go.v1 v1.67.0 gopkg.in/macaroon.v2 v2.1.0 gorm.io/driver/sqlite v1.5.6 gorm.io/gorm v1.25.11 diff --git a/go.sum b/go.sum index 1ad01923..09c76e91 100644 --- a/go.sum +++ b/go.sum @@ -9,16 +9,16 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg6 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/DataDog/appsec-internal-go v1.6.0 h1:QHvPOv/O0s2fSI/BraZJNpRDAtdlrRm5APJFZNBxjAw= -github.com/DataDog/appsec-internal-go v1.6.0/go.mod h1:pEp8gjfNLtEOmz+iZqC8bXhu0h4k7NUsW/qiQb34k1U= +github.com/DataDog/appsec-internal-go v1.7.0 h1:iKRNLih83dJeVya3IoUfK+6HLD/hQsIbyBlfvLmAeb0= +github.com/DataDog/appsec-internal-go v1.7.0/go.mod h1:wW0cRfWBo4C044jHGwYiyh5moQV2x0AhnwqMuiX7O/g= github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8= github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0/go.mod h1:HzySONXnAgSmIQfL6gOv9hWprKJkx8CicuXuUbmgWfo= github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 h1:5nE6N3JSs2IG3xzMthNFhXfOaXlrsdgqmJ73lndFf8c= github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1/go.mod h1:Vc+snp0Bey4MrrJyiV2tVxxJb6BmLomPvN1RgAvjGaQ= github.com/DataDog/datadog-go/v5 v5.3.0 h1:2q2qjFOb3RwAZNU+ez27ZVDwErJv5/VpbBPprz7Z+s8= github.com/DataDog/datadog-go/v5 v5.3.0/go.mod h1:XRDJk1pTc00gm+ZDiBKsjh7oOOtJfYfglVCmFb8C2+Q= -github.com/DataDog/go-libddwaf/v3 v3.2.1 h1:lZPc6UxCOwioHc++nsldKR50FpIrRh1uGnGLuryqnE8= -github.com/DataDog/go-libddwaf/v3 v3.2.1/go.mod h1:AP+7Atb8ftSsrha35wht7+K3R+xuzfVSQhabSO4w6CY= +github.com/DataDog/go-libddwaf/v3 v3.3.0 h1:jS72fuQpFgJZEdEJDmHJCPAgNTEMZoz1EUvimPUOiJ4= +github.com/DataDog/go-libddwaf/v3 v3.3.0/go.mod h1:Bz/0JkpGf689mzbUjKJeheJINqsyyhM8p9PDuHdK2Ec= github.com/DataDog/go-tuf v1.0.2-0.5.2 h1:EeZr937eKAWPxJ26IykAdWA4A0jQXJgkhUjqEI/w7+I= github.com/DataDog/go-tuf v1.0.2-0.5.2/go.mod h1:zBcq6f654iVqmkk8n2Cx81E1JnNTMOAx1UEO/wZR+P0= github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4= @@ -340,8 +340,8 @@ github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQ github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= -github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= -github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= @@ -493,8 +493,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/nbd-wtf/go-nostr v0.34.5 h1:vti8WqvGWbVoWAPniaz7li2TpCyC+7ZS62Gmy7ib/z0= -github.com/nbd-wtf/go-nostr v0.34.5/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= +github.com/nbd-wtf/go-nostr v0.34.10 h1:scJH45sFk5LOzHJNLw0EFTknCCKfKlo3tK+vdpTHz3Q= +github.com/nbd-wtf/go-nostr v0.34.10/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= github.com/nbd-wtf/ln-decodepay v1.12.1 h1:GDBIDZPm35DtRadhO9qBT+OebXgm33+8BpANq0QcwLA= github.com/nbd-wtf/ln-decodepay v1.12.1/go.mod h1:+VRpg00geUGDEaBx/9+P5nt2RVmyMCNsKnaFxErYUgo= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= @@ -892,8 +892,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= -gopkg.in/DataDog/dd-trace-go.v1 v1.66.0 h1:025+lLubGtpiDWrRmSOxoFBPIiVRVYRcqP9oLabVOeg= -gopkg.in/DataDog/dd-trace-go.v1 v1.66.0/go.mod h1:Av6AXGmQCQAbDnwNoPiuUz1k3GS8TwQjj+vEdwmEpmM= +gopkg.in/DataDog/dd-trace-go.v1 v1.67.0 h1:3Cb46zyKIlEWac21tvDF2O4KyMlOHQxrQkyiaUpdwM0= +gopkg.in/DataDog/dd-trace-go.v1 v1.67.0/go.mod h1:6DdiJPKOeJfZyd/IUGCAd5elY8qPGkztK6wbYYsMjag= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/scripts/linux-x86_64/phoenixd/README.md b/scripts/linux-x86_64/phoenixd/README.md index 79ecddc0..8ad41ba2 100644 --- a/scripts/linux-x86_64/phoenixd/README.md +++ b/scripts/linux-x86_64/phoenixd/README.md @@ -27,6 +27,7 @@ Make sure to backup the `albyhub-phoenixd` which is used as volume for albyhub a ### Installation (non-Docker) $ wget https://raw.githubusercontent.com/getAlby/hub/master/scripts/linux-x86_64/phoenixd/install.sh + $ chmod +x install.sh $ ./install.sh The install script will prompt you for a installation folder and will install phoenixd and Alby Hub there. diff --git a/service/service.go b/service/service.go index da15d77a..dfbd6704 100644 --- a/service/service.go +++ b/service/service.go @@ -89,10 +89,6 @@ func NewService(ctx context.Context) (*service, error) { eventPublisher := events.NewEventPublisher() - if err != nil { - logger.Logger.WithError(err).Error("Failed to create Alby OAuth service") - return nil, err - } keys := keys.NewKeys() diff --git a/service/start.go b/service/start.go index 6c90fc06..814f0dc6 100644 --- a/service/start.go +++ b/service/start.go @@ -41,8 +41,8 @@ func (svc *service) startNostr(ctx context.Context, encryptionKey string) error "npub": npub, "hex": svc.keys.GetNostrPublicKey(), }).Info("Starting Alby Hub") + svc.wg.Add(1) go func() { - svc.wg.Add(1) // ensure the relay is properly disconnected before exiting defer svc.wg.Done() //Start infinite loop which will be only broken by canceling ctx (SIGINT) @@ -154,9 +154,9 @@ func (svc *service) launchLNBackend(ctx context.Context, encryptionKey string) e return errors.New("LNClient already started") } + svc.wg.Add(1) go func() { // ensure the LNClient is stopped properly before exiting - svc.wg.Add(1) <-ctx.Done() svc.stopLNClient() }() diff --git a/wails/wails_handlers.go b/wails/wails_handlers.go index d6c90fc8..2660696f 100644 --- a/wails/wails_handlers.go +++ b/wails/wails_handlers.go @@ -3,6 +3,7 @@ package wails import ( "encoding/json" "fmt" + "net/url" "os" "regexp" "strconv" @@ -805,18 +806,21 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string if strings.HasPrefix(route, "/api/log/") { logType := strings.TrimPrefix(route, "/api/log/") + logType = strings.Split(logType, "?")[0] if logType != api.LogTypeNode && logType != api.LogTypeApp { return WailsRequestRouterResponse{Body: nil, Error: fmt.Sprintf("Invalid log type: '%s'", logType)} } - getLogOutputRequest := &api.GetLogOutputRequest{} - err := json.Unmarshal([]byte(body), getLogOutputRequest) + parsedUrl, err := url.Parse(route) 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: nil, Error: "Failed to parse route URL"} + } + queryParams := parsedUrl.Query() + getLogOutputRequest := &api.GetLogOutputRequest{} + if maxLen := queryParams.Get("maxLen"); maxLen != "" { + getLogOutputRequest.MaxLen, err = strconv.Atoi(maxLen) + if err != nil { + return WailsRequestRouterResponse{Body: nil, Error: "Invalid max length parameter"} + } } logOutputResponse, err := app.api.GetLogOutput(ctx, logType, getLogOutputRequest) if err != nil {