Skip to content

Commit

Permalink
fix: new app connection, edit app connection, deep linking
Browse files Browse the repository at this point in the history
  • Loading branch information
rolznz committed Jul 14, 2024
1 parent 0aa4a9c commit 9bea336
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 189 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ If the client creates the secret the client only needs to share the public key o
- `budget_renewal` (optional) reset the budget at the end of the given budget renewal. Can be `never` (default), `daily`, `weekly`, `monthly`, `yearly`
- `request_methods` (optional) url encoded, space separated list of request types that you need permission for: `pay_invoice` (default), `get_balance` (see NIP47). For example: `..&request_methods=pay_invoice%20get_balance`
- `notification_types` (optional) url encoded, space separated list of notification types that you need permission for: For example: `..&notification_types=payment_received%20payment_sent`
- `isolated` (optional) makes an isolated app connection with its own balance and only access to its own transaction list. e.g. `&isolated=true`
- `isolated` (optional) makes an isolated app connection with its own balance and only access to its own transaction list. e.g. `&isolated=true`. If using this option, you should not pass any custom request methods or notification types, nor set a budget or expiry.

Example:

Expand Down
12 changes: 8 additions & 4 deletions frontend/src/components/BudgetRenewalSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import { BudgetRenewalType, validBudgetRenewals } from "src/types";
interface BudgetRenewalProps {
value: BudgetRenewalType;
onChange: (value: BudgetRenewalType) => void;
onClose?: () => void;
}

const BudgetRenewalSelect: React.FC<BudgetRenewalProps> = ({
value,
onChange,
onClose,
}) => {
return (
<>
Expand All @@ -36,10 +38,12 @@ const BudgetRenewalSelect: React.FC<BudgetRenewalProps> = ({
</SelectItem>
))}
</SelectContent>
<XIcon
className="cursor-pointer w-4 text-muted-foreground"
onClick={() => onChange("never")}
/>
{onClose && (
<XIcon
className="cursor-pointer w-4 text-muted-foreground"
onClick={onClose}
/>
)}
</Select>
</div>
</>
Expand Down
24 changes: 14 additions & 10 deletions frontend/src/components/ExpirySelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { expiryOptions } from "src/types";

const daysFromNow = (date?: Date) => {
if (!date) {
return 0;
return undefined;
}
const now = dayjs();
const targetDate = dayjs(date);
Expand All @@ -26,11 +26,14 @@ interface ExpiryProps {

const ExpirySelect: React.FC<ExpiryProps> = ({ value, onChange }) => {
const [expiryDays, setExpiryDays] = React.useState(daysFromNow(value));
const [customExpiry, setCustomExpiry] = React.useState(
daysFromNow(value)
? !Object.values(expiryOptions).includes(daysFromNow(value))
: false
);
const [customExpiry, setCustomExpiry] = React.useState(() => {
const _daysFromNow = daysFromNow(value);
return _daysFromNow !== undefined
? !Object.values(expiryOptions)
.filter((value) => value !== 0)
.includes(_daysFromNow)
: false;
});
return (
<>
<p className="font-medium text-sm mb-2">Connection expiration</p>
Expand All @@ -41,11 +44,12 @@ const ExpirySelect: React.FC<ExpiryProps> = ({ value, onChange }) => {
key={expiry}
onClick={() => {
setCustomExpiry(false);
let date;
let date: Date | undefined;
if (expiryOptions[expiry]) {
date = new Date();
date.setDate(date.getUTCDate() + expiryOptions[expiry]);
date.setHours(23, 59, 59);
date = dayjs()
.add(expiryOptions[expiry], "day")
.endOf("day")
.toDate();
}
onChange(date);
setExpiryDays(expiryOptions[expiry]);
Expand Down
166 changes: 75 additions & 91 deletions frontend/src/components/Permissions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,73 +17,46 @@ import {

interface PermissionsProps {
capabilities: WalletCapabilities;
initialPermissions: AppPermissions;
onPermissionsChange: (permissions: AppPermissions) => void;
canEditPermissions?: boolean;
permissions: AppPermissions;
setPermissions: React.Dispatch<React.SetStateAction<AppPermissions>>;
readOnly?: boolean;
scopesReadOnly?: boolean;
budgetReadOnly?: boolean;
expiresAtReadOnly?: boolean;
budgetUsage?: number;
isNewConnection?: boolean;
isNewConnection: boolean;
}

const Permissions: React.FC<PermissionsProps> = ({
capabilities,
initialPermissions,
onPermissionsChange,
canEditPermissions,
permissions,
setPermissions,
isNewConnection,
budgetUsage,
readOnly,
scopesReadOnly,
budgetReadOnly,
expiresAtReadOnly,
}) => {
const [permissions, setPermissions] = React.useState(initialPermissions);

const [isScopesEditable, setScopesEditable] = React.useState(
isNewConnection ? !initialPermissions.scopes.size : canEditPermissions
);
const [isBudgetAmountEditable, setBudgetAmountEditable] = React.useState(
isNewConnection
? Number.isNaN(initialPermissions.maxAmount)
: canEditPermissions
);
const [isExpiryEditable, setExpiryEditable] = React.useState(
isNewConnection ? !initialPermissions.expiresAt : canEditPermissions
);

// triggered when changes are saved in show app
React.useEffect(() => {
if (isNewConnection || canEditPermissions) {
return;
}
setPermissions(initialPermissions);
}, [canEditPermissions, isNewConnection, initialPermissions]);

// triggered when edit mode is toggled in show app
React.useEffect(() => {
if (isNewConnection) {
return;
}
setScopesEditable(canEditPermissions);
setBudgetAmountEditable(canEditPermissions);
setExpiryEditable(canEditPermissions);
}, [canEditPermissions, isNewConnection]);

const [showBudgetOptions, setShowBudgetOptions] = React.useState(
//permissions.scopes.has("pay_invoice")
false
permissions.scopes.includes("pay_invoice") && permissions.maxAmount > 0
);
const [showExpiryOptions, setShowExpiryOptions] = React.useState(
false
// isNewConnection ? !!permissions.expiresAt : true
!!permissions.expiresAt
);

const handlePermissionsChange = React.useCallback(
(changedPermissions: Partial<AppPermissions>) => {
const updatedPermissions = { ...permissions, ...changedPermissions };
setPermissions(updatedPermissions);
onPermissionsChange(updatedPermissions);
setPermissions((currentPermissions) => ({
...currentPermissions,
...changedPermissions,
}));
},
[permissions, onPermissionsChange]
[setPermissions]
);

const onScopesChanged = React.useCallback(
(scopes: Set<Scope>, isolated: boolean) => {
(scopes: Scope[], isolated: boolean) => {
handlePermissionsChange({ scopes, isolated });
},
[handlePermissionsChange]
Expand All @@ -97,8 +70,8 @@ const Permissions: React.FC<PermissionsProps> = ({
);

const handleBudgetRenewalChange = React.useCallback(
(value: string) => {
handlePermissionsChange({ budgetRenewal: value as BudgetRenewalType });
(budgetRenewal: BudgetRenewalType) => {
handlePermissionsChange({ budgetRenewal });
},
[handlePermissionsChange]
);
Expand All @@ -112,13 +85,21 @@ const Permissions: React.FC<PermissionsProps> = ({

return (
<div className="max-w-lg">
{isScopesEditable ? (
{!readOnly && !scopesReadOnly ? (
<Scopes
capabilities={capabilities}
scopes={permissions.scopes}
isolated={permissions.isolated}
onScopesChanged={onScopesChanged}
isNewConnection={isNewConnection}
/>
) : permissions.isolated ? (
<p>
This app will be isolated from the rest of your wallet. This means it
will have an isolated balance and only has access to its own
transaction history. It will not be able to read your node info,
transactions, or sign messages.
</p>
) : (
<>
<p className="text-sm font-medium mb-2">Scopes</p>
Expand All @@ -141,44 +122,21 @@ const Permissions: React.FC<PermissionsProps> = ({
</div>
</>
)}
{!permissions.isolated && permissions.scopes.has("pay_invoice") && (

{!permissions.isolated && permissions.scopes.includes("pay_invoice") && (
<>
{!isBudgetAmountEditable ? (
<div className="pl-4 ml-2 border-l-2 border-l-primary mb-4">
<div className="flex flex-col gap-2 text-muted-foreground text-sm">
<p className="capitalize">
<span className="text-primary-foreground font-medium">
Budget Renewal:
</span>{" "}
{permissions.budgetRenewal || "Never"}
</p>
<p>
<span className="text-primary-foreground font-medium">
Budget Amount:
</span>{" "}
{permissions.maxAmount
? new Intl.NumberFormat().format(permissions.maxAmount)
: "∞"}
{" sats "}
{!isNewConnection &&
`(${new Intl.NumberFormat().format(budgetUsage || 0)} sats used)`}
</p>
</div>
</div>
) : (
{!readOnly && !budgetReadOnly ? (
<>
{!showBudgetOptions && (
<Button
type="button"
variant="secondary"
onClick={() => {
handleBudgetMaxAmountChange(100000);
handleBudgetRenewalChange("monthly");
handleBudgetMaxAmountChange(100_000);
setShowBudgetOptions(true);
}}
className={cn(
"mr-4",
(!isExpiryEditable || showExpiryOptions) && "mb-4"
)}
className={cn("mr-4", showExpiryOptions && "mb-4")}
>
<PlusCircle className="w-4 h-4 mr-2" />
Set budget
Expand All @@ -189,6 +147,11 @@ const Permissions: React.FC<PermissionsProps> = ({
<BudgetRenewalSelect
value={permissions.budgetRenewal}
onChange={handleBudgetRenewalChange}
onClose={() => {
handleBudgetRenewalChange("never");
handleBudgetMaxAmountChange(0);
setShowBudgetOptions(false);
}}
/>
<BudgetAmountSelect
value={permissions.maxAmount}
Expand All @@ -197,23 +160,35 @@ const Permissions: React.FC<PermissionsProps> = ({
</>
)}
</>
) : (
<div className="pl-4 ml-2 border-l-2 border-l-primary mb-4">
<div className="flex flex-col gap-2 text-muted-foreground text-sm">
<p className="capitalize">
<span className="text-primary font-medium">
Budget Renewal:
</span>{" "}
{permissions.budgetRenewal || "Never"}
</p>
<p>
<span className="text-primary font-medium">
Budget Amount:
</span>{" "}
{permissions.maxAmount
? new Intl.NumberFormat().format(permissions.maxAmount)
: "∞"}
{" sats "}
{!isNewConnection &&
`(${new Intl.NumberFormat().format(budgetUsage || 0)} sats used)`}
</p>
</div>
</div>
)}
</>
)}

{!permissions.isolated && (
<>
{!isExpiryEditable ? (
<>
<p className="text-sm font-medium mb-2">Connection expiry</p>
<p className="text-muted-foreground text-sm">
{permissions.expiresAt &&
new Date(permissions.expiresAt).getFullYear() !== 1
? new Date(permissions.expiresAt).toString()
: "This app will never expire"}
</p>
</>
) : (
{!readOnly && !expiresAtReadOnly ? (
<>
{!showExpiryOptions && (
<Button
Expand All @@ -233,6 +208,15 @@ const Permissions: React.FC<PermissionsProps> = ({
/>
)}
</>
) : (
<>
<p className="text-sm font-medium mb-2">Connection expiry</p>
<p className="text-muted-foreground text-sm">
{permissions.expiresAt
? new Date(permissions.expiresAt).toString()
: "This app will never expire"}
</p>
</>
)}
</>
)}
Expand Down
Loading

0 comments on commit 9bea336

Please sign in to comment.