feat: add readonly option for app store apps (#2415)

This commit is contained in:
Roland 2026-06-10 13:17:13 +07:00 committed by GitHub
parent 98e7d987bb
commit e5dc19ae68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 39 additions and 22 deletions

View file

@ -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<ScopesProps> = ({
}, [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]);

View file

@ -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[] = (
</ExternalLink>{" "}
in your browser, or download the app on iOS or Android
</p>
<p className="text-muted-foreground mt-4">
In the next step, set wallet permissions to{" "}
<span className="font-medium text-foreground">Custom</span> and
enable:
</p>
<ul className="list-inside list-disc text-muted-foreground mt-1">
<li>Read your node info</li>
<li>Create invoices</li>
<li>Lookup status of invoices</li>
<li>Read transaction history</li>
</ul>
</div>
</>
),
@ -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",

View file

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

View file

@ -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<Scope, string> = {
get_balance: "Read your balance",
get_info: "Read your node info",