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: -

- ), @@ -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",