Permissions.tsx raw
1 import { AlertTriangleIcon, CoinsIcon, TimerIcon } from "lucide-react";
2 import React from "react";
3 import BudgetAmountSelect from "src/components/BudgetAmountSelect";
4 import BudgetRenewalSelect from "src/components/BudgetRenewalSelect";
5 import ExpirySelect from "src/components/ExpirySelect";
6 import Scopes from "src/components/Scopes";
7 import { Badge } from "src/components/ui/badge";
8 import { Switch } from "src/components/ui/switch";
9 import {
10 DEFAULT_APP_BUDGET_RENEWAL,
11 DEFAULT_APP_BUDGET_SATS,
12 } from "src/constants";
13 import { cn } from "src/lib/utils";
14 import {
15 AppPermissions,
16 BudgetRenewalType,
17 Scope,
18 WalletCapabilities,
19 scopeDescriptions,
20 scopeIconMap,
21 } from "src/types";
22
23 interface PermissionsProps {
24 capabilities: WalletCapabilities;
25 permissions: AppPermissions;
26 setPermissions?: React.Dispatch<React.SetStateAction<AppPermissions>>;
27 readOnly?: boolean;
28 }
29
30 const Permissions: React.FC<PermissionsProps> = ({
31 capabilities,
32 permissions,
33 setPermissions,
34 readOnly,
35 }) => {
36 const [showBudgetOptions, setShowBudgetOptions] = React.useState(
37 permissions.scopes.includes("pay_invoice") && permissions.maxAmountSat > 0
38 );
39 const [showExpiryOptions, setShowExpiryOptions] = React.useState(
40 !!permissions.expiresAt
41 );
42
43 const handlePermissionsChange = React.useCallback(
44 (changedPermissions: Partial<AppPermissions>) => {
45 setPermissions?.((currentPermissions) => ({
46 ...currentPermissions,
47 ...changedPermissions,
48 }));
49 },
50 [setPermissions]
51 );
52
53 const onScopesChanged = React.useCallback(
54 (scopes: Scope[], isolated: boolean) => {
55 handlePermissionsChange({ scopes, isolated });
56 },
57 [handlePermissionsChange]
58 );
59
60 const handleBudgetMaxAmountSatChange = React.useCallback(
61 (amountSat: number) => {
62 handlePermissionsChange({ maxAmountSat: amountSat });
63 },
64 [handlePermissionsChange]
65 );
66
67 const handleBudgetRenewalChange = React.useCallback(
68 (budgetRenewal: BudgetRenewalType) => {
69 handlePermissionsChange({ budgetRenewal });
70 },
71 [handlePermissionsChange]
72 );
73
74 const handleExpiryChange = React.useCallback(
75 (expiryDate?: Date) => {
76 handlePermissionsChange({ expiresAt: expiryDate });
77 },
78 [handlePermissionsChange]
79 );
80
81 return (
82 <div className={cn("space-y-4", !readOnly && "max-w-lg")}>
83 {!readOnly ? (
84 <Scopes
85 capabilities={capabilities}
86 scopes={permissions.scopes}
87 isolated={permissions.isolated}
88 onScopesChanged={onScopesChanged}
89 />
90 ) : (
91 <>
92 <p className="text-sm font-medium mb-2">This app is authorized to:</p>
93 <div className="flex flex-wrap gap-2 mb-4">
94 {[...permissions.scopes].map((scope) => {
95 const PermissionIcon = scopeIconMap[scope];
96 return (
97 <Badge
98 variant="secondary"
99 key={scope}
100 className={cn(
101 "flex items-center font-normal py-1 rounded-full px-3"
102 )}
103 >
104 <PermissionIcon className="mr-1 size-4" />
105 <p className="text-sm">{scopeDescriptions[scope]}</p>
106 </Badge>
107 );
108 })}
109 </div>
110 </>
111 )}
112
113 {/* We skip read only component here as budget is shown in AppUsage */}
114 {permissions.scopes.includes("pay_invoice") && !readOnly && (
115 <div className="rounded-lg border p-4">
116 <div className="flex items-center justify-between">
117 <label
118 htmlFor="budget-toggle"
119 className="flex items-center gap-2 cursor-pointer"
120 >
121 <CoinsIcon className="size-5 text-muted-foreground" />
122 <div className="flex flex-col gap-0.5">
123 <p className="text-sm font-medium">Budget</p>
124 <p className="text-xs text-muted-foreground">
125 Limit how much this app can spend
126 </p>
127 </div>
128 </label>
129 <Switch
130 id="budget-toggle"
131 checked={showBudgetOptions}
132 onCheckedChange={(checked) => {
133 if (checked) {
134 handleBudgetRenewalChange(DEFAULT_APP_BUDGET_RENEWAL);
135 handleBudgetMaxAmountSatChange(DEFAULT_APP_BUDGET_SATS);
136 } else {
137 handleBudgetRenewalChange("never");
138 handleBudgetMaxAmountSatChange(0);
139 }
140 setShowBudgetOptions(checked);
141 }}
142 />
143 </div>
144 {showBudgetOptions && (
145 <div className="mt-4">
146 <BudgetRenewalSelect
147 value={permissions.budgetRenewal}
148 onChange={handleBudgetRenewalChange}
149 />
150 <BudgetAmountSelect
151 valueSat={permissions.maxAmountSat}
152 onChange={handleBudgetMaxAmountSatChange}
153 />
154 </div>
155 )}
156 </div>
157 )}
158
159 {!readOnly ? (
160 <div className="rounded-lg border p-4">
161 <div className="flex items-center justify-between">
162 <label
163 htmlFor="expiry-toggle"
164 className="flex items-center gap-2 cursor-pointer"
165 >
166 <TimerIcon className="size-5 text-muted-foreground" />
167 <div className="flex flex-col gap-0.5">
168 <p className="text-sm font-medium">Expiration</p>
169 <p className="text-xs text-muted-foreground">
170 Automatically expire this connection
171 </p>
172 </div>
173 </label>
174 <Switch
175 id="expiry-toggle"
176 checked={showExpiryOptions}
177 onCheckedChange={(checked) => {
178 if (checked) {
179 const defaultExpiry = new Date();
180 defaultExpiry.setFullYear(defaultExpiry.getFullYear() + 1);
181 defaultExpiry.setHours(23, 59, 59);
182 handleExpiryChange(defaultExpiry);
183 } else {
184 handleExpiryChange(undefined);
185 }
186 setShowExpiryOptions(checked);
187 }}
188 />
189 </div>
190 {showExpiryOptions && (
191 <div className="mt-4">
192 <ExpirySelect
193 value={permissions.expiresAt}
194 onChange={handleExpiryChange}
195 />
196 </div>
197 )}
198 </div>
199 ) : (
200 <div>
201 <p className="text-sm font-medium mb-2">Connection expiry</p>
202 <p className="text-muted-foreground text-sm">
203 {permissions.expiresAt
204 ? new Date(permissions.expiresAt).toString()
205 : "This app will never expire"}
206 </p>
207 </div>
208 )}
209
210 {permissions.scopes.includes("superuser") && (
211 <>
212 <div className="flex items-center gap-2 mt-4">
213 <AlertTriangleIcon className="size-4" />
214 <p className="text-sm font-medium">
215 This app can create other app connections
216 </p>
217 </div>
218 <p className="text-muted-foreground text-sm">
219 Make sure to set budgets on connections created by this app.
220 </p>
221 </>
222 )}
223 </div>
224 );
225 };
226
227 export default Permissions;
228