Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add uncle jim internal app #557

Merged
merged 9 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions alby/alby_oauth_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient.
nil,
scopes,
false,
nil,
)

if err != nil {
Expand Down
25 changes: 24 additions & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
22 changes: 15 additions & 7 deletions db/db_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package db

import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"slices"
Expand All @@ -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"
)

Expand All @@ -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
Expand All @@ -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
Expand Down
25 changes: 25 additions & 0 deletions db/migrations/202408291715_app_metadata.go
Original file line number Diff line number Diff line change
@@ -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
},
}
1 change: 1 addition & 0 deletions db/migrations/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion db/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type App struct {
CreatedAt time.Time
UpdatedAt time.Time
Isolated bool
Metadata datatypes.JSON
}

type AppPermission struct {
Expand Down Expand Up @@ -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 (
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Binary file added frontend/src/assets/suggested-apps/uncle-jim.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 29 additions & 8 deletions frontend/src/components/AppAvatar.tsx
Original file line number Diff line number Diff line change
@@ -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 <UserAvatar className={className} />;
}
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 (
<div
className={cn(
"rounded-lg border relative",
`avatar-gradient-${gradient}`,
"rounded-lg border relative overflow-hidden",
!image && `avatar-gradient-${gradient}`,
className
)}
>
<span className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-white text-xl font-medium capitalize">
{appName.charAt(0)}
</span>
{image && (
<img
src={image}
className={cn("absolute w-full h-full rounded-lg", className)}
/>
)}
{!image && (
<span className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-white text-xl font-medium capitalize">
{app.name.charAt(0)}
</span>
)}
</div>
);
}
1 change: 0 additions & 1 deletion frontend/src/components/CloseChannelDialogContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/components/SuggestedAppData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand All @@ -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",
Expand Down
48 changes: 39 additions & 9 deletions frontend/src/components/SuggestedApps.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -36,11 +36,13 @@ function SuggestedAppCard({
</CardContent>
<CardFooter className="flex flex-row justify-between">
<div className="flex flex-row gap-4">
<ExternalLink to={webLink}>
<Button variant="outline" size="icon">
<Globe className="w-4 h-4" />
</Button>
</ExternalLink>
{webLink && (
<ExternalLink to={webLink}>
<Button variant="outline" size="icon">
<Globe className="w-4 h-4" />
</Button>
</ExternalLink>
)}
{appleLink && (
<ExternalLink to={appleLink}>
<Button variant="outline" size="icon">
Expand All @@ -67,13 +69,41 @@ function SuggestedAppCard({
);
}

function InternalAppCard({ id, title, description, logo }: SuggestedApp) {
return (
<Card>
<CardContent className="pt-6">
<div className="flex gap-3 items-center">
<img src={logo} alt="logo" className="inline rounded-lg w-12 h-12" />
<div className="flex-grow">
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</div>
</div>
</CardContent>
<CardFooter className="flex flex-row justify-end">
<Link to={`/internal-apps/${id}`}>
<Button variant="outline">
<ExternalLinkIcon className="w-4 h-4 mr-2" />
Open
</Button>
</Link>
</CardFooter>
</Card>
);
}

export default function SuggestedApps() {
return (
<>
<div className="grid sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{suggestedApps.map((app) => (
<SuggestedAppCard key={app.id} {...app} />
))}
{suggestedApps.map((app) =>
app.internal ? (
<InternalAppCard key={app.id} {...app} />
) : (
<SuggestedAppCard key={app.id} {...app} />
)
)}
</div>
</>
);
Expand Down
Loading
Loading