diff --git a/alby/alby_oauth_service.go b/alby/alby_oauth_service.go index ad9c5a6b0..dc330ea19 100644 --- a/alby/alby_oauth_service.go +++ b/alby/alby_oauth_service.go @@ -253,9 +253,20 @@ func (svc *albyOAuthService) GetInfo(ctx context.Context) (*AlbyInfo, error) { LatestReleaseNotes string `json:"latest_release_notes"` } + type albyInfoIncident struct { + Name string `json:"name"` + Started string `json:"started"` + Status string `json:"status"` + Impact string `json:"impact"` + Url string `json:"url"` + } + type albyInfo struct { - Hub albyInfoHub `json:"hub"` - // TODO: consider getting healthcheck/incident info and showing in the hub + Hub albyInfoHub `json:"hub"` + Status string `json:"status"` + Healthy bool `json:"healthy"` + AccountAvailable bool `json:"account_available"` // false if country is blocked (can still use Alby Hub without an Alby Account) + Incidents []albyInfoIncident `json:"incidents"` } body, err := io.ReadAll(res.Body) @@ -279,11 +290,26 @@ func (svc *albyOAuthService) GetInfo(ctx context.Context) (*AlbyInfo, error) { return nil, err } + incidents := []AlbyInfoIncident{} + for _, incident := range info.Incidents { + incidents = append(incidents, AlbyInfoIncident{ + Name: incident.Name, + Started: incident.Started, + Status: incident.Status, + Impact: incident.Impact, + Url: incident.Url, + }) + } + return &AlbyInfo{ Hub: AlbyInfoHub{ LatestVersion: info.Hub.LatestVersion, LatestReleaseNotes: info.Hub.LatestReleaseNotes, }, + Status: info.Status, + Healthy: info.Healthy, + AccountAvailable: info.AccountAvailable, + Incidents: incidents, }, nil } @@ -702,8 +728,13 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve } if event.Event == "nwc_backup_channels" { - if err := svc.backupChannels(ctx, event); err != nil { - logger.Logger.WithError(err).Error("Failed to backup channels") + // if backup fails, try again (max 3 attempts) + for i := 0; i < 3; i++ { + if err := svc.backupChannels(ctx, event); err != nil { + logger.Logger.WithField("attempt", i).WithError(err).Error("Failed to backup channels") + continue + } + break } return } diff --git a/alby/models.go b/alby/models.go index a0bdfe946..5a50df000 100644 --- a/alby/models.go +++ b/alby/models.go @@ -55,9 +55,20 @@ type AlbyInfoHub struct { LatestReleaseNotes string `json:"latestReleaseNotes"` } +type AlbyInfoIncident struct { + Name string `json:"name"` + Started string `json:"started"` + Status string `json:"status"` + Impact string `json:"impact"` + Url string `json:"url"` +} + type AlbyInfo struct { - Hub AlbyInfoHub `json:"hub"` - // TODO: consider getting healthcheck/incident info and showing in the hub + Hub AlbyInfoHub `json:"hub"` + Status string `json:"status"` + Healthy bool `json:"healthy"` + AccountAvailable bool `json:"accountAvailable"` // false if country is blocked (can still use Alby Hub without an Alby Account) + Incidents []AlbyInfoIncident `json:"incidents"` } type AlbyMeHub struct { diff --git a/api/api.go b/api/api.go index 0e111fd52..414db536e 100644 --- a/api/api.go +++ b/api/api.go @@ -402,6 +402,7 @@ func (api *api) ListChannels(ctx context.Context) ([]Channel, error) { Id: channel.Id, RemotePubkey: channel.RemotePubkey, FundingTxId: channel.FundingTxId, + FundingTxVout: channel.FundingTxVout, Active: channel.Active, Public: channel.Public, InternalChannel: channel.InternalChannel, @@ -441,7 +442,15 @@ func (api *api) ChangeUnlockPassword(changeUnlockPasswordRequest *ChangeUnlockPa return errors.New("LNClient not started") } - err := api.cfg.ChangeUnlockPassword(changeUnlockPasswordRequest.CurrentUnlockPassword, changeUnlockPasswordRequest.NewUnlockPassword) + autoUnlockPassword, err := api.cfg.Get("AutoUnlockPassword", "") + if err != nil { + return err + } + if autoUnlockPassword != "" { + return errors.New("Please disable auto-unlock before using this feature") + } + + err = api.cfg.ChangeUnlockPassword(changeUnlockPasswordRequest.CurrentUnlockPassword, changeUnlockPasswordRequest.NewUnlockPassword) if err != nil { logger.Logger.WithError(err).Error("failed to change unlock password") @@ -453,6 +462,21 @@ func (api *api) ChangeUnlockPassword(changeUnlockPasswordRequest *ChangeUnlockPa return api.Stop() } +func (api *api) SetAutoUnlockPassword(unlockPassword string) error { + if api.svc.GetLNClient() == nil { + return errors.New("LNClient not started") + } + + err := api.cfg.SetAutoUnlockPassword(unlockPassword) + + if err != nil { + logger.Logger.WithError(err).Error("failed to set auto unlock password") + return err + } + + return nil +} + func (api *api) Stop() error { if !startMutex.TryLock() { // do not allow to stop twice in case this is somehow called twice @@ -686,6 +710,7 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { info := InfoResponse{} backendType, _ := api.cfg.Get("LNBackendType", "") ldkVssEnabled, _ := api.cfg.Get("LdkVssEnabled", "") + autoUnlockPassword, _ := api.cfg.Get("AutoUnlockPassword", "") info.SetupCompleted = api.cfg.SetupCompleted() if api.startupError != nil { info.StartupError = api.startupError.Error() @@ -699,6 +724,8 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { info.EnableAdvancedSetup = api.cfg.GetEnv().EnableAdvancedSetup info.LdkVssEnabled = ldkVssEnabled == "true" info.VssSupported = backendType == config.LDKBackendType && api.cfg.GetEnv().LDKVssUrl != "" + info.AutoUnlockPasswordEnabled = autoUnlockPassword != "" + info.AutoUnlockPasswordSupported = api.cfg.GetEnv().IsDefaultClientId() albyUserIdentifier, err := api.albyOAuthSvc.GetUserIdentifier() if err != nil { logger.Logger.WithError(err).Error("Failed to get alby user identifier") diff --git a/api/backup.go b/api/backup.go index 3532bddfe..f4adc04cb 100644 --- a/api/backup.go +++ b/api/backup.go @@ -30,6 +30,14 @@ func (api *api) CreateBackup(unlockPassword string, w io.Writer) error { return errors.New("invalid unlock password") } + autoUnlockPassword, err := api.cfg.Get("AutoUnlockPassword", "") + if err != nil { + return err + } + if autoUnlockPassword != "" { + return errors.New("Please disable auto-unlock before using this feature") + } + workDir, err := filepath.Abs(api.cfg.GetEnv().Workdir) if err != nil { return fmt.Errorf("failed to get absolute workdir: %w", err) diff --git a/api/models.go b/api/models.go index 401d360b7..a668696b8 100644 --- a/api/models.go +++ b/api/models.go @@ -21,6 +21,7 @@ type API interface { GetChannelPeerSuggestions(ctx context.Context) ([]alby.ChannelPeerSuggestion, error) ResetRouter(key string) error ChangeUnlockPassword(changeUnlockPasswordRequest *ChangeUnlockPasswordRequest) error + SetAutoUnlockPassword(unlockPassword string) error Stop() error GetNodeConnectionInfo(ctx context.Context) (*lnclient.NodeConnectionInfo, error) GetNodeStatus(ctx context.Context) (*lnclient.NodeStatus, error) @@ -71,7 +72,7 @@ type App struct { BudgetUsage uint64 `json:"budgetUsage"` BudgetRenewal string `json:"budgetRenewal"` Isolated bool `json:"isolated"` - Balance uint64 `json:"balance"` + Balance int64 `json:"balance"` Metadata Metadata `json:"metadata,omitempty"` } @@ -159,22 +160,24 @@ type User struct { } type InfoResponse struct { - BackendType string `json:"backendType"` - SetupCompleted bool `json:"setupCompleted"` - OAuthRedirect bool `json:"oauthRedirect"` - Running bool `json:"running"` - Unlocked bool `json:"unlocked"` - AlbyAuthUrl string `json:"albyAuthUrl"` - NextBackupReminder string `json:"nextBackupReminder"` - AlbyUserIdentifier string `json:"albyUserIdentifier"` - AlbyAccountConnected bool `json:"albyAccountConnected"` - Version string `json:"version"` - Network string `json:"network"` - EnableAdvancedSetup bool `json:"enableAdvancedSetup"` - LdkVssEnabled bool `json:"ldkVssEnabled"` - VssSupported bool `json:"vssSupported"` - StartupError string `json:"startupError"` - StartupErrorTime time.Time `json:"startupErrorTime"` + BackendType string `json:"backendType"` + SetupCompleted bool `json:"setupCompleted"` + OAuthRedirect bool `json:"oauthRedirect"` + Running bool `json:"running"` + Unlocked bool `json:"unlocked"` + AlbyAuthUrl string `json:"albyAuthUrl"` + NextBackupReminder string `json:"nextBackupReminder"` + AlbyUserIdentifier string `json:"albyUserIdentifier"` + AlbyAccountConnected bool `json:"albyAccountConnected"` + Version string `json:"version"` + Network string `json:"network"` + EnableAdvancedSetup bool `json:"enableAdvancedSetup"` + LdkVssEnabled bool `json:"ldkVssEnabled"` + VssSupported bool `json:"vssSupported"` + StartupError string `json:"startupError"` + StartupErrorTime time.Time `json:"startupErrorTime"` + AutoUnlockPasswordSupported bool `json:"autoUnlockPasswordSupported"` + AutoUnlockPasswordEnabled bool `json:"autoUnlockPasswordEnabled"` } type MnemonicRequest struct { @@ -189,6 +192,9 @@ type ChangeUnlockPasswordRequest struct { CurrentUnlockPassword string `json:"currentUnlockPassword"` NewUnlockPassword string `json:"newUnlockPassword"` } +type AutoUnlockRequest struct { + UnlockPassword string `json:"unlockPassword"` +} type ConnectPeerRequest = lnclient.ConnectPeerRequest type OpenChannelRequest = lnclient.OpenChannelRequest @@ -342,6 +348,7 @@ type Channel struct { Id string `json:"id"` RemotePubkey string `json:"remotePubkey"` FundingTxId string `json:"fundingTxId"` + FundingTxVout uint32 `json:"fundingTxVout"` Active bool `json:"active"` Public bool `json:"public"` InternalChannel interface{} `json:"internalChannel"` diff --git a/config/config.go b/config/config.go index 5739ba2f3..2d4c65eaf 100644 --- a/config/config.go +++ b/config/config.go @@ -248,6 +248,20 @@ func (cfg *config) ChangeUnlockPassword(currentUnlockPassword string, newUnlockP return nil } +func (cfg *config) SetAutoUnlockPassword(unlockPassword string) error { + if unlockPassword != "" && !cfg.CheckUnlockPassword(unlockPassword) { + return errors.New("incorrect password") + } + + err := cfg.SetUpdate("AutoUnlockPassword", unlockPassword, "") + if err != nil { + logger.Logger.WithError(err).Error("failed to update auto unlock password") + return err + } + + return nil +} + func (cfg *config) CheckUnlockPassword(encryptionKey string) bool { decryptedValue, err := cfg.Get("UnlockPasswordCheck", encryptionKey) diff --git a/config/models.go b/config/models.go index e3e167eff..4f6bdea90 100644 --- a/config/models.go +++ b/config/models.go @@ -14,37 +14,38 @@ const ( ) type AppConfig struct { - Relay string `envconfig:"RELAY" default:"wss://relay.getalby.com/v1"` - LNBackendType string `envconfig:"LN_BACKEND_TYPE"` - LNDAddress string `envconfig:"LND_ADDRESS"` - LNDCertFile string `envconfig:"LND_CERT_FILE"` - LNDMacaroonFile string `envconfig:"LND_MACAROON_FILE"` - Workdir string `envconfig:"WORK_DIR"` - Port string `envconfig:"PORT" default:"8080"` - DatabaseUri string `envconfig:"DATABASE_URI" default:"nwc.db"` - JWTSecret string `envconfig:"JWT_SECRET"` - LogLevel string `envconfig:"LOG_LEVEL" default:"4"` - LogToFile bool `envconfig:"LOG_TO_FILE" default:"true"` - LDKNetwork string `envconfig:"LDK_NETWORK" default:"bitcoin"` - LDKEsploraServer string `envconfig:"LDK_ESPLORA_SERVER" default:"https://electrs.getalbypro.com"` // TODO: remove LDK prefix - LDKGossipSource string `envconfig:"LDK_GOSSIP_SOURCE"` - LDKLogLevel string `envconfig:"LDK_LOG_LEVEL" default:"3"` - LDKVssUrl string `envconfig:"LDK_VSS_URL"` - LDKListeningAddresses string `envconfig:"LDK_LISTENING_ADDRESSES" default:"0.0.0.0:9735,[::]:9735"` - MempoolApi string `envconfig:"MEMPOOL_API" default:"https://mempool.space/api"` - AlbyClientId string `envconfig:"ALBY_OAUTH_CLIENT_ID" default:"J2PbXS1yOf"` - AlbyClientSecret string `envconfig:"ALBY_OAUTH_CLIENT_SECRET" default:"rABK2n16IWjLTZ9M1uKU"` - BaseUrl string `envconfig:"BASE_URL"` - FrontendUrl string `envconfig:"FRONTEND_URL"` - LogEvents bool `envconfig:"LOG_EVENTS" default:"true"` - AutoLinkAlbyAccount bool `envconfig:"AUTO_LINK_ALBY_ACCOUNT" default:"true"` - PhoenixdAddress string `envconfig:"PHOENIXD_ADDRESS"` - PhoenixdAuthorization string `envconfig:"PHOENIXD_AUTHORIZATION"` - GoProfilerAddr string `envconfig:"GO_PROFILER_ADDR"` - DdProfilerEnabled bool `envconfig:"DD_PROFILER_ENABLED" default:"false"` - EnableAdvancedSetup bool `envconfig:"ENABLE_ADVANCED_SETUP" default:"true"` - AutoUnlockPassword string `envconfig:"AUTO_UNLOCK_PASSWORD"` - LogDBQueries bool `envconfig:"LOG_DB_QUERIES" default:"false"` + Relay string `envconfig:"RELAY" default:"wss://relay.getalby.com/v1"` + LNBackendType string `envconfig:"LN_BACKEND_TYPE"` + LNDAddress string `envconfig:"LND_ADDRESS"` + LNDCertFile string `envconfig:"LND_CERT_FILE"` + LNDMacaroonFile string `envconfig:"LND_MACAROON_FILE"` + Workdir string `envconfig:"WORK_DIR"` + Port string `envconfig:"PORT" default:"8080"` + DatabaseUri string `envconfig:"DATABASE_URI" default:"nwc.db"` + JWTSecret string `envconfig:"JWT_SECRET"` + LogLevel string `envconfig:"LOG_LEVEL" default:"4"` + LogToFile bool `envconfig:"LOG_TO_FILE" default:"true"` + LDKNetwork string `envconfig:"LDK_NETWORK" default:"bitcoin"` + LDKEsploraServer string `envconfig:"LDK_ESPLORA_SERVER" default:"https://electrs.getalbypro.com"` // TODO: remove LDK prefix + LDKGossipSource string `envconfig:"LDK_GOSSIP_SOURCE"` + LDKLogLevel string `envconfig:"LDK_LOG_LEVEL" default:"3"` + LDKVssUrl string `envconfig:"LDK_VSS_URL"` + LDKListeningAddresses string `envconfig:"LDK_LISTENING_ADDRESSES" default:"0.0.0.0:9735,[::]:9735"` + LDKTransientNetworkGraph bool `envconfig:"LDK_TRANSIENT_NETWORK_GRAPH" default:"false"` + MempoolApi string `envconfig:"MEMPOOL_API" default:"https://mempool.space/api"` + AlbyClientId string `envconfig:"ALBY_OAUTH_CLIENT_ID" default:"J2PbXS1yOf"` + AlbyClientSecret string `envconfig:"ALBY_OAUTH_CLIENT_SECRET" default:"rABK2n16IWjLTZ9M1uKU"` + BaseUrl string `envconfig:"BASE_URL"` + FrontendUrl string `envconfig:"FRONTEND_URL"` + LogEvents bool `envconfig:"LOG_EVENTS" default:"true"` + AutoLinkAlbyAccount bool `envconfig:"AUTO_LINK_ALBY_ACCOUNT" default:"true"` + PhoenixdAddress string `envconfig:"PHOENIXD_ADDRESS"` + PhoenixdAuthorization string `envconfig:"PHOENIXD_AUTHORIZATION"` + GoProfilerAddr string `envconfig:"GO_PROFILER_ADDR"` + DdProfilerEnabled bool `envconfig:"DD_PROFILER_ENABLED" default:"false"` + EnableAdvancedSetup bool `envconfig:"ENABLE_ADVANCED_SETUP" default:"true"` + AutoUnlockPassword string `envconfig:"AUTO_UNLOCK_PASSWORD"` + LogDBQueries bool `envconfig:"LOG_DB_QUERIES" default:"false"` } func (c *AppConfig) IsDefaultClientId() bool { @@ -60,6 +61,7 @@ type Config interface { GetEnv() *AppConfig CheckUnlockPassword(password string) bool ChangeUnlockPassword(currentUnlockPassword string, newUnlockPassword string) error + SetAutoUnlockPassword(unlockPassword string) error SaveUnlockPasswordCheck(encryptionKey string) error SetupCompleted() bool } diff --git a/constants/constants.go b/constants/constants.go index 98a8afeed..620f19d9d 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -47,5 +47,6 @@ const ( ERROR_RESTRICTED = "RESTRICTED" ERROR_BAD_REQUEST = "BAD_REQUEST" ERROR_NOT_FOUND = "NOT_FOUND" + ERROR_UNSUPPORTED_VERSION = "UNSUPPORTED_VERSION" ERROR_OTHER = "OTHER" ) diff --git a/db/queries/get_isolated_balance.go b/db/queries/get_isolated_balance.go index bd9cef701..331b598e7 100644 --- a/db/queries/get_isolated_balance.go +++ b/db/queries/get_isolated_balance.go @@ -5,9 +5,9 @@ import ( "gorm.io/gorm" ) -func GetIsolatedBalance(tx *gorm.DB, appId uint) uint64 { +func GetIsolatedBalance(tx *gorm.DB, appId uint) int64 { var received struct { - Sum uint64 + Sum int64 } tx. Table("transactions"). @@ -15,7 +15,7 @@ func GetIsolatedBalance(tx *gorm.DB, appId uint) uint64 { Where("app_id = ? AND type = ? AND state = ?", appId, constants.TRANSACTION_TYPE_INCOMING, constants.TRANSACTION_STATE_SETTLED).Scan(&received) var spent struct { - Sum uint64 + Sum int64 } tx. diff --git a/db/queries/get_isolated_balance_test.go b/db/queries/get_isolated_balance_test.go new file mode 100644 index 000000000..773e5e0da --- /dev/null +++ b/db/queries/get_isolated_balance_test.go @@ -0,0 +1,69 @@ +package queries + +import ( + "testing" + + "github.com/getAlby/hub/constants" + "github.com/getAlby/hub/db" + "github.com/getAlby/hub/tests" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetIsolatedBalance_PendingNoOverflow(t *testing.T) { + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + require.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + app.Isolated = true + svc.DB.Save(&app) + + paymentAmount := uint64(1000) // 1 sat + + tx := db.Transaction{ + AppId: &app.ID, + RequestEventId: nil, + Type: constants.TRANSACTION_TYPE_OUTGOING, + State: constants.TRANSACTION_STATE_PENDING, + FeeReserveMsat: uint64(10000), + AmountMsat: paymentAmount, + PaymentRequest: tests.MockInvoice, + PaymentHash: tests.MockPaymentHash, + SelfPayment: true, + } + svc.DB.Save(&tx) + + balance := GetIsolatedBalance(svc.DB, app.ID) + assert.Equal(t, int64(-11000), balance) +} + +func TestGetIsolatedBalance_SettledNoOverflow(t *testing.T) { + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + require.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + app.Isolated = true + svc.DB.Save(&app) + + paymentAmount := uint64(1000) // 1 sat + + tx := db.Transaction{ + AppId: &app.ID, + RequestEventId: nil, + Type: constants.TRANSACTION_TYPE_OUTGOING, + State: constants.TRANSACTION_STATE_SETTLED, + FeeReserveMsat: uint64(0), + AmountMsat: paymentAmount, + PaymentRequest: tests.MockInvoice, + PaymentHash: tests.MockPaymentHash, + SelfPayment: true, + } + svc.DB.Save(&tx) + + balance := GetIsolatedBalance(svc.DB, app.ID) + assert.Equal(t, int64(-1000), balance) +} diff --git a/doc/logo.svg b/doc/logo.svg index 5f913d238..e351a3130 100644 --- a/doc/logo.svg +++ b/doc/logo.svg @@ -1,17 +1,62 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/suggested-apps/clams.png b/frontend/src/assets/suggested-apps/clams.png new file mode 100644 index 000000000..42c976b17 Binary files /dev/null and b/frontend/src/assets/suggested-apps/clams.png differ diff --git a/frontend/src/assets/suggested-apps/simple-boost.png b/frontend/src/assets/suggested-apps/simple-boost.png new file mode 100644 index 000000000..b06005e00 Binary files /dev/null and b/frontend/src/assets/suggested-apps/simple-boost.png differ diff --git a/frontend/src/assets/zapplanner/bitcoinbrink.png b/frontend/src/assets/zapplanner/bitcoinbrink.png new file mode 100644 index 000000000..c3c370e98 Binary files /dev/null and b/frontend/src/assets/zapplanner/bitcoinbrink.png differ diff --git a/frontend/src/assets/zapplanner/hrf.png b/frontend/src/assets/zapplanner/hrf.png new file mode 100644 index 000000000..6449fc83d Binary files /dev/null and b/frontend/src/assets/zapplanner/hrf.png differ diff --git a/frontend/src/assets/zapplanner/opensats.png b/frontend/src/assets/zapplanner/opensats.png new file mode 100644 index 000000000..b5a8a9a70 Binary files /dev/null and b/frontend/src/assets/zapplanner/opensats.png differ diff --git a/frontend/src/components/SuggestedAppData.tsx b/frontend/src/components/SuggestedAppData.tsx index c2747b4d9..51fd7e85d 100644 --- a/frontend/src/components/SuggestedAppData.tsx +++ b/frontend/src/components/SuggestedAppData.tsx @@ -4,6 +4,7 @@ import albyGo from "src/assets/suggested-apps/alby-go.png"; import alby from "src/assets/suggested-apps/alby.png"; import amethyst from "src/assets/suggested-apps/amethyst.png"; import buzzpay from "src/assets/suggested-apps/buzzpay.png"; +import clams from "src/assets/suggested-apps/clams.png"; import damus from "src/assets/suggested-apps/damus.png"; import hablanews from "src/assets/suggested-apps/habla-news.png"; import kiwi from "src/assets/suggested-apps/kiwi.png"; @@ -12,6 +13,7 @@ import nostrudel from "src/assets/suggested-apps/nostrudel.png"; 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 simpleboost from "src/assets/suggested-apps/simple-boost.png"; import snort from "src/assets/suggested-apps/snort.png"; import stackernews from "src/assets/suggested-apps/stacker-news.png"; import uncleJim from "src/assets/suggested-apps/uncle-jim.png"; @@ -61,6 +63,13 @@ export const suggestedApps: SuggestedApp[] = [ internal: true, logo: buzzpay, }, + { + id: "simpleboost", + title: "SimpleBoost", + description: "Donation widget for your website", + internal: true, + logo: simpleboost, + }, { id: "alby-extension", title: "Alby Extension", @@ -520,6 +529,56 @@ export const suggestedApps: SuggestedApp[] = [ ), }, + { + id: "clams", + title: "Clams", + description: "Multi wallet accounting tool", + webLink: "https://clams.tech/", + logo: clams, + guide: ( + <> +
+

In Clams

+ +
+
+

In Alby Hub

+ +
+
+

In Clams

+ +
+ + ), + }, { id: "nostrudel", title: "noStrudel", @@ -647,65 +706,7 @@ export const suggestedApps: SuggestedApp[] = [ description: "Schedule payments", webLink: "https://zapplanner.albylabs.com/", logo: zapplanner, - guide: ( - <> -
-

In ZapPlanner

- -
-
-

In Alby Hub

- -
-
-

In ZapPlanner

- -
- - ), + internal: true, }, { id: "zapplepay", @@ -1212,7 +1213,7 @@ export const suggestedApps: SuggestedApp[] = [ playLink: "https://play.google.com/store/apps/details?id=com.getalby.mobile", appleLink: "https://apps.apple.com/us/app/alby-go/id6471335774", - zapStoreLink: "https://zapstore.dev", + zapStoreLink: "https://zapstore.dev/download/", logo: albyGo, guide: ( <> diff --git a/frontend/src/components/TransactionItem.tsx b/frontend/src/components/TransactionItem.tsx index 310fd9f08..f55e35857 100644 --- a/frontend/src/components/TransactionItem.tsx +++ b/frontend/src/components/TransactionItem.tsx @@ -72,7 +72,7 @@ function TransactionItem({ tx }: Props) { tx.state === "failed" ? "bg-red-100 dark:bg-red-950" : tx.state === "pending" - ? "bg-gray-100 dark:bg-gray-950" + ? "bg-blue-100 dark:bg-blue-900" : type === "outgoing" ? "bg-orange-100 dark:bg-orange-950" : "bg-green-100 dark:bg-emerald-950" @@ -85,7 +85,7 @@ function TransactionItem({ tx }: Props) { tx.state === "failed" ? "stroke-rose-400 dark:stroke-red-600" : tx.state === "pending" - ? "stroke-gray-400 dark:stroke-gray-600" + ? "stroke-blue-500" : type === "outgoing" ? "stroke-orange-400 dark:stroke-amber-600" : "stroke-green-400 dark:stroke-emerald-500" diff --git a/frontend/src/components/channels/ChannelDropdownMenu.tsx b/frontend/src/components/channels/ChannelDropdownMenu.tsx index e5aa65a88..436e7f24b 100644 --- a/frontend/src/components/channels/ChannelDropdownMenu.tsx +++ b/frontend/src/components/channels/ChannelDropdownMenu.tsx @@ -49,7 +49,7 @@ export function ChannelDropdownMenu({ diff --git a/frontend/src/components/connections/AppCard.tsx b/frontend/src/components/connections/AppCard.tsx index 75335da71..0fe3a5ffd 100644 --- a/frontend/src/components/connections/AppCard.tsx +++ b/frontend/src/components/connections/AppCard.tsx @@ -16,9 +16,10 @@ dayjs.extend(relativeTime); type Props = { app: App; + actions?: React.ReactNode; }; -export default function AppCard({ app }: Props) { +export default function AppCard({ app, actions }: Props) { const navigate = useNavigate(); return ( @@ -34,6 +35,10 @@ export default function AppCard({ app }: Props) {
{app.name}
+ {!!actions && ( + // stop the above navigation click handler +
e.stopPropagation()}>{actions}
+ )} diff --git a/frontend/src/components/icons/AlbyHubLogo.tsx b/frontend/src/components/icons/AlbyHubLogo.tsx index 48a1783c8..ac8a02056 100644 --- a/frontend/src/components/icons/AlbyHubLogo.tsx +++ b/frontend/src/components/icons/AlbyHubLogo.tsx @@ -3,64 +3,67 @@ import { SVGAttributes } from "react"; export function AlbyHubLogo(props: SVGAttributes) { return ( - - - - - - - - + + + + + + + + + + - - - - - + ); } diff --git a/frontend/src/components/layouts/SettingsLayout.tsx b/frontend/src/components/layouts/SettingsLayout.tsx index 4d69eb17b..94b267296 100644 --- a/frontend/src/components/layouts/SettingsLayout.tsx +++ b/frontend/src/components/layouts/SettingsLayout.tsx @@ -98,6 +98,9 @@ export default function SettingsLayout() {