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/db/db_service.go b/db/db_service.go
index 9cca9c72..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,14 +28,10 @@ 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) {
- if isolated && (slices.Contains(scopes, constants.GET_INFO_SCOPE)) {
- // cannot return node info because the isolated app is a custodial subaccount
- return nil, "", errors.New("Isolated app cannot have get_info scope")
- }
+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")
+ return nil, "", errors.New("isolated app cannot have sign_message scope")
}
var pairingPublicKey string
@@ -51,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/frontend/package.json b/frontend/package.json
index 4c6c0cd8..6f384963 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -21,6 +21,7 @@
},
"dependencies": {
"@getalby/bitcoin-connect-react": "^3.6.2",
+ "@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
diff --git a/frontend/src/assets/suggested-apps/uncle-jim.png b/frontend/src/assets/suggested-apps/uncle-jim.png
new file mode 100644
index 00000000..9238fca0
Binary files /dev/null and b/frontend/src/assets/suggested-apps/uncle-jim.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/CloseChannelDialogContent.tsx b/frontend/src/components/CloseChannelDialogContent.tsx
index 7c16fa9f..a8afe5c5 100644
--- a/frontend/src/components/CloseChannelDialogContent.tsx
+++ b/frontend/src/components/CloseChannelDialogContent.tsx
@@ -38,7 +38,6 @@ export function CloseChannelDialogContent({ alias, channel }: Props) {
const copy = (text: string) => {
copyToClipboard(text, toast);
- toast({ title: "Copied to clipboard." });
};
async function closeChannel() {
diff --git a/frontend/src/components/SuggestedAppData.tsx b/frontend/src/components/SuggestedAppData.tsx
index 886fde12..6b133f84 100644
--- a/frontend/src/components/SuggestedAppData.tsx
+++ b/frontend/src/components/SuggestedAppData.tsx
@@ -10,6 +10,7 @@ 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/stacker-news.png";
+import uncleJim from "src/assets/suggested-apps/uncle-jim.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";
@@ -20,7 +21,8 @@ import zappybird from "src/assets/suggested-apps/zappy-bird.png";
export type SuggestedApp = {
id: string;
- webLink: string;
+ webLink?: string;
+ internal?: boolean;
playLink?: string;
appleLink?: string;
title: string;
@@ -29,6 +31,13 @@ export type SuggestedApp = {
};
export const suggestedApps: SuggestedApp[] = [
+ {
+ id: "uncle-jim",
+ title: "Friends & Family",
+ description: "Subaccounts powered by your Hub",
+ internal: true,
+ logo: uncleJim,
+ },
{
id: "alby-extension",
title: "Alby Extension",
diff --git a/frontend/src/components/SuggestedApps.tsx b/frontend/src/components/SuggestedApps.tsx
index 1a6b80f9..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";
@@ -36,11 +36,13 @@ function SuggestedAppCard({
-
-
-
+ {webLink && (
+
+
+
+ )}
{appleLink && (