Skip to content

Commit

Permalink
feat: add buzzpay internal app
Browse files Browse the repository at this point in the history
  • Loading branch information
rolznz committed Sep 2, 2024
1 parent 37e2fff commit 1d675dc
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 8 deletions.
15 changes: 15 additions & 0 deletions 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 @@ -141,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
1 change: 1 addition & 0 deletions api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,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 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.
8 changes: 8 additions & 0 deletions 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 Down Expand Up @@ -38,6 +39,13 @@ export const suggestedApps: SuggestedApp[] = [
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
9 changes: 7 additions & 2 deletions frontend/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ 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";
import { UncleJimApp } from "src/screens/internal-apps/UncleJimApp";
import { BuzzPay } from "src/screens/internal-apps/BuzzPay";
import { UncleJim } from "src/screens/internal-apps/UncleJim";
import { Success } from "src/screens/onboarding/Success";
import BuyBitcoin from "src/screens/onchain/BuyBitcoin";
import DepositBitcoin from "src/screens/onchain/DepositBitcoin";
Expand Down Expand Up @@ -210,7 +211,11 @@ const routes = [
children: [
{
path: "uncle-jim",
element: <UncleJimApp />,
element: <UncleJim />,
},
{
path: "buzzpay",
element: <BuzzPay />,
},
],
},
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/screens/apps/ShowApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { useDeleteApp } from "src/hooks/useDeleteApp";
import {
App,
AppPermissions,
BudgetRenewalType,
UpdateAppRequest,
WalletCapabilities,
} from "src/types";
Expand Down Expand Up @@ -95,7 +94,7 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) {
const [permissions, setPermissions] = React.useState<AppPermissions>({
scopes: app.scopes,
maxAmount: app.maxAmount,
budgetRenewal: app.budgetRenewal as BudgetRenewalType,
budgetRenewal: app.budgetRenewal,
expiresAt: app.expiresAt ? new Date(app.expiresAt) : undefined,
isolated: app.isolated,
});
Expand Down
145 changes: 145 additions & 0 deletions frontend/src/screens/internal-apps/BuzzPay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React from "react";
import AppHeader from "src/components/AppHeader";
import AppCard from "src/components/connections/AppCard";
import Loading from "src/components/Loading";
import { ExternalLinkButton } from "src/components/ui/button";
import { LoadingButton } from "src/components/ui/loading-button";
import { useToast } from "src/components/ui/use-toast";
import { useApps } from "src/hooks/useApps";
import {
App,
AppPermissions,
CreateAppRequest,
CreateAppResponse,
UpdateAppRequest,
} from "src/types";
import { handleRequestError } from "src/utils/handleRequestError";
import { request } from "src/utils/request";

export function BuzzPay() {
const { data: apps, mutate: reloadApps } = useApps();
const [creatingApp, setCreatingApp] = React.useState(false);
const { toast } = useToast();

if (!apps) {
return <Loading />;
}
const app = apps.find(
(app) =>
app.metadata?.app_store_app_id === "buzzpay" &&
app.metadata.connection_secret
);

function createApp() {
setCreatingApp(true);
(async () => {
try {
const name = "BuzzPay";
if (apps?.some((existingApp) => existingApp.name === name)) {
throw new Error("A connection with the same name already exists.");
}

const createAppRequest: CreateAppRequest = {
name,
scopes: ["get_info", "lookup_invoice", "make_invoice"],
isolated: false,
metadata: {
app_store_app_id: "buzzpay",
},
};

const createAppResponse = await request<CreateAppResponse>(
"/api/apps",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(createAppRequest),
}
);

if (!createAppResponse) {
throw new Error("no create app response received");
}

const app = await request<App>(
`/api/apps/${createAppResponse.pairingPublicKey}`
);

if (!app) {
throw new Error("failed to fetch buzzpay app");
}

const permissions: AppPermissions = {
scopes: app.scopes,
maxAmount: app.maxAmount,
budgetRenewal: app.budgetRenewal,
expiresAt: app.expiresAt ? new Date(app.expiresAt) : undefined,
isolated: app.isolated,
};

// TODO: should be able to partially update app rather than having to pass everything
// we are only updating the metadata
const updateAppRequest: UpdateAppRequest = {
name,
scopes: Array.from(permissions.scopes),
budgetRenewal: permissions.budgetRenewal,
expiresAt: permissions.expiresAt?.toISOString(),
maxAmount: permissions.maxAmount,
metadata: {
...app.metadata,
// read-only connection secret
connection_secret: createAppResponse.pairingUri,
},
};

await request(`/api/apps/${app.nostrPubkey}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(updateAppRequest),
});
await reloadApps();

toast({ title: "BuzzPay app created" });
} catch (error) {
handleRequestError(toast, "Failed to create app", error);
}
setCreatingApp(false);
})();
}

return (
<div className="grid gap-5">
<AppHeader
title="BuzzPay"
description="Receive-only PoS you can safely share with your employees"
/>
{app && (
<div>
<AppCard app={app} />
<ExternalLinkButton
className="mt-4"
to={`https://pos.albylabs.com/#/wallet/${encodeURIComponent(app.metadata?.connection_secret as string)}/new`}
>
Go to BuzzPay PoS
</ExternalLinkButton>
</div>
)}
{!app && (
<div className="max-w-lg flex flex-col gap-5">
<p className="text-muted-foreground">
By creating a new buzzpay app, a read-only wallet connection will be
created and you will receive a link you can share with your
employees, on any device.
</p>
<LoadingButton loading={creatingApp} onClick={createApp}>
Create BuzzPay App
</LoadingButton>
</div>
)}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { CreateAppRequest, CreateAppResponse } from "src/types";
import { handleRequestError } from "src/utils/handleRequestError";
import { request } from "src/utils/request";

export function UncleJimApp() {
export function UncleJim() {
const [name, setName] = React.useState("");
const [appPublicKey, setAppPublicKey] = React.useState("");
const [connectionSecret, setConnectionSecret] = React.useState("");
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,10 @@ export interface InfoResponse {

export type Network = "bitcoin" | "testnet" | "signet";

export type AppMetadata =
| Record<string, unknown>
| { app_store_app_id?: string };
export type AppMetadata = { app_store_app_id?: string } & Record<
string,
unknown
>;

export interface MnemonicResponse {
mnemonic: string;
Expand Down Expand Up @@ -187,6 +188,7 @@ export type UpdateAppRequest = {
budgetRenewal: string;
expiresAt: string | undefined;
scopes: Scope[];
metadata?: AppMetadata;
};

export type Channel = {
Expand Down

0 comments on commit 1d675dc

Please sign in to comment.