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 buzzpay internal app #580

Merged
merged 13 commits into from
Sep 3, 2024
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
40 changes: 39 additions & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

"github.com/sirupsen/logrus"
"gorm.io/datatypes"
"gorm.io/gorm"

"github.com/getAlby/hub/alby"
Expand Down Expand Up @@ -75,7 +76,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 @@ -140,6 +142,20 @@ func (api *api) UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) e
}
}

if updateAppRequest.Metadata != nil {
var metadataBytes []byte
var err error
metadataBytes, err = json.Marshal(updateAppRequest.Metadata)
if err != nil {
logger.Logger.WithError(err).Error("Failed to serialize metadata")
return err
}
err = tx.Model(&db.App{}).Where("id", userApp.ID).Update("metadata", datatypes.JSON(metadataBytes)).Error
if err != nil {
return err
}
}

// Update existing permissions with new budget and expiry
err := tx.Model(&db.AppPermission{}).Where("app_id", userApp.ID).Updates(map[string]interface{}{
"ExpiresAt": expiresAt,
Expand Down Expand Up @@ -220,6 +236,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 +259,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 +327,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
3 changes: 3 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 @@ -82,6 +83,7 @@ type UpdateAppRequest struct {
BudgetRenewal string `json:"budgetRenewal"`
ExpiresAt string `json:"expiresAt"`
Scopes []string `json:"scopes"`
Metadata Metadata `json:"metadata,omitempty"`
}

type CreateAppRequest struct {
Expand All @@ -93,6 +95,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/buzzpay.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
19 changes: 18 additions & 1 deletion frontend/src/components/SuggestedAppData.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
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 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";
Expand All @@ -10,6 +11,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 +22,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 +32,20 @@ export type SuggestedApp = {
};

export const suggestedApps: SuggestedApp[] = [
{
id: "uncle-jim",
title: "Friends & Family",
description: "Subaccounts for your friends and family powered by your Hub",
internal: true,
logo: uncleJim,
},
{
id: "buzzpay",
title: "BuzzPay PoS",
description: "Receive-only PoS you can safely share with your employees",
internal: true,
logo: buzzpay,
},
{
id: "alby-extension",
title: "Alby Extension",
Expand Down
Loading
Loading