From e5dc19ae68a240f4efdca14e34af08cea3f02281 Mon Sep 17 00:00:00 2001
From: Roland <33993199+rolznz@users.noreply.github.com>
Date: Wed, 10 Jun 2026 13:17:13 +0700
Subject: [PATCH] feat: add readonly option for app store apps (#2415)
---
frontend/src/components/Scopes.tsx | 18 +++++++---------
.../connections/SuggestedAppData.tsx | 21 +++++++++----------
frontend/src/screens/apps/NewApp.tsx | 11 ++++++++++
frontend/src/types.ts | 11 ++++++++++
4 files changed, 39 insertions(+), 22 deletions(-)
diff --git a/frontend/src/components/Scopes.tsx b/frontend/src/components/Scopes.tsx
index 7982b0bf..b325a265 100644
--- a/frontend/src/components/Scopes.tsx
+++ b/frontend/src/components/Scopes.tsx
@@ -19,7 +19,12 @@ import {
SheetTitle,
} from "src/components/ui/sheet";
import { cn } from "src/lib/utils";
-import { Scope, WalletCapabilities, scopeDescriptions } from "src/types";
+import {
+ READ_ONLY_SCOPES,
+ Scope,
+ WalletCapabilities,
+ scopeDescriptions,
+} from "src/types";
const scopeGroups = ["full_access", "read_only", "isolated", "custom"] as const;
type ScopeGroup = (typeof scopeGroups)[number];
@@ -65,17 +70,8 @@ const Scopes: React.FC = ({
}, [capabilities.scopes]);
const readOnlyScopes: Scope[] = React.useMemo(() => {
- const readOnlyScopes: Scope[] = [
- "get_balance",
- "get_info",
- "make_invoice",
- "lookup_invoice",
- "list_transactions",
- "notifications",
- ];
-
return capabilities.scopes.filter((scope) =>
- readOnlyScopes.includes(scope)
+ READ_ONLY_SCOPES.includes(scope)
);
}, [capabilities.scopes]);
diff --git a/frontend/src/components/connections/SuggestedAppData.tsx b/frontend/src/components/connections/SuggestedAppData.tsx
index 75def3bd..f514d129 100644
--- a/frontend/src/components/connections/SuggestedAppData.tsx
+++ b/frontend/src/components/connections/SuggestedAppData.tsx
@@ -84,6 +84,9 @@ export type AppStoreApp = {
hideConnectionQr?: boolean;
internal?: boolean;
superuser?: boolean;
+ // Receive-only apps (e.g. merchant payment receivers) default to read-only
+ // permissions in the new connection flow.
+ readonly?: boolean;
addedDate?: string;
};
@@ -796,6 +799,7 @@ export const appStoreApps: AppStoreApp[] = (
},
{
id: "sat-sorter",
+ readonly: true,
title: "Sat Sorter",
description: "A Bitcoin Budgeting App",
webLink: "https://satsorter.com",
@@ -895,6 +899,7 @@ export const appStoreApps: AppStoreApp[] = (
},
{
id: "bitrequest",
+ readonly: true,
title: "Bitrequest",
description: "Non-custodial payment requests",
webLink: "https://www.bitrequest.io",
@@ -917,17 +922,6 @@ export const appStoreApps: AppStoreApp[] = (
{" "}
in your browser, or download the app on iOS or Android
-
- In the next step, set wallet permissions to{" "}
- Custom and
- enable:
-
-
- - Read your node info
- - Create invoices
- - Lookup status of invoices
- - Read transaction history
-
>
),
@@ -967,6 +961,7 @@ export const appStoreApps: AppStoreApp[] = (
},
{
id: "btcpay",
+ readonly: true,
title: "BTCPay Server",
description: "Bitcoin payment processor",
webLink: "https://btcpayserver.org/",
@@ -1455,6 +1450,7 @@ export const appStoreApps: AppStoreApp[] = (
},
{
id: "clams",
+ readonly: true,
title: "Clams",
description: "Multi wallet accounting tool",
webLink: "https://clams.tech/",
@@ -1491,6 +1487,7 @@ export const appStoreApps: AppStoreApp[] = (
},
{
id: "nostrcheck-server",
+ readonly: true,
title: "Nostrcheck Server",
description: "Sovereign Nostr services",
webLink: "https://github.com/quentintaranpino/nostrcheck-server",
@@ -1798,6 +1795,7 @@ export const appStoreApps: AppStoreApp[] = (
},
{
id: "nakapay",
+ readonly: true,
title: "NakaPay",
description: "Non-custodial Lightning payments for businesses via NWC",
webLink: "https://www.nakapay.app",
@@ -2338,6 +2336,7 @@ export const appStoreApps: AppStoreApp[] = (
},
{
id: "takemysats",
+ readonly: true,
title: "Take My Sats",
description: "Create your online store and accept Bitcoin payments",
webLink: "https://www.takemysats.com",
diff --git a/frontend/src/screens/apps/NewApp.tsx b/frontend/src/screens/apps/NewApp.tsx
index baac7814..d0db8162 100644
--- a/frontend/src/screens/apps/NewApp.tsx
+++ b/frontend/src/screens/apps/NewApp.tsx
@@ -31,6 +31,7 @@ import {
CreateAppResponse,
Nip47NotificationType,
Nip47RequestMethod,
+ READ_ONLY_SCOPES,
Scope,
WalletCapabilities,
validBudgetRenewals,
@@ -105,6 +106,14 @@ const NewAppInternal = ({ capabilities }: NewAppInternalProps) => {
/* eslint-disable react-hooks/preserve-manual-memoization */
const initialScopes: Scope[] = React.useMemo(() => {
+ // Receive-only app store apps (e.g. merchant payment receivers) default to
+ // read-only permissions, unless the deep link explicitly requests methods.
+ if (appStoreApp?.readonly && !reqMethodsParam) {
+ return capabilities.scopes.filter((scope) =>
+ READ_ONLY_SCOPES.includes(scope)
+ );
+ }
+
const methods = reqMethodsParam
? reqMethodsParam.split(" ")
: capabilities.methods;
@@ -185,8 +194,10 @@ const NewAppInternal = ({ capabilities }: NewAppInternalProps) => {
return scopes;
}, [
+ appStoreApp?.readonly,
capabilities.methods,
capabilities.notificationTypes,
+ capabilities.scopes,
isolatedParam,
notificationTypesParam,
reqMethodsParam,
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index cf0c9a88..bbda4156 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -80,6 +80,17 @@ export const validBudgetRenewals: BudgetRenewalType[] = [
"never",
];
+// Scopes granted to a read-only connection: receive payments and view
+// balance/history, but never send/spend.
+export const READ_ONLY_SCOPES: Scope[] = [
+ "get_balance",
+ "get_info",
+ "make_invoice",
+ "lookup_invoice",
+ "list_transactions",
+ "notifications",
+];
+
export const scopeDescriptions: Record = {
get_balance: "Read your balance",
get_info: "Read your node info",