Skip to content

Commit b64149f

Browse files
authored
Merge pull request #1801 from getAlby/feature/nip04-and-permissions
Feature/nip04 and permissions
2 parents 6181142 + 4ec55d3 commit b64149f

File tree

18 files changed

+495
-55
lines changed

18 files changed

+495
-55
lines changed

src/app/router/Prompt/Prompt.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ import LNURLChannel from "@screens/LNURLChannel";
99
import LNURLPay from "@screens/LNURLPay";
1010
import LNURLWithdraw from "@screens/LNURLWithdraw";
1111
import MakeInvoice from "@screens/MakeInvoice";
12+
import NostrConfirm from "@screens/Nostr/Confirm";
13+
import NostrConfirmGetPublicKey from "@screens/Nostr/ConfirmGetPublicKey";
14+
import NostrConfirmSignMessage from "@screens/Nostr/ConfirmSignMessage";
1215
import Unlock from "@screens/Unlock";
1316
import { HashRouter, Outlet, Route, Routes, Navigate } from "react-router-dom";
1417
import { ToastContainer } from "react-toastify";
1518
import Providers from "~/app/context/Providers";
1619
import RequireAuth from "~/app/router/RequireAuth";
17-
import NostrConfirmGetPublicKey from "~/app/screens/Nostr/ConfirmGetPublicKey";
18-
import NostrConfirmSignMessage from "~/app/screens/Nostr/ConfirmSignMessage";
1920
import type { NavigationState, OriginData } from "~/types";
2021

2122
// Parse out the parameters from the querystring.
@@ -72,6 +73,7 @@ function Prompt() {
7273
path="public/nostr/enable"
7374
element={<Enable origin={navigationState.origin as OriginData} />} // prompt will always have an `origin` set, just the type is optional to support usage via PopUp
7475
/>
76+
<Route path="public/nostr/confirm" element={<NostrConfirm />} />
7577
<Route
7678
path="public/nostr/confirmGetPublicKey"
7779
element={<NostrConfirmGetPublicKey />}

src/app/screens/Nostr/Confirm.tsx

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { CheckIcon } from "@bitcoin-design/bitcoin-icons-react/filled";
2+
import ConfirmOrCancel from "@components/ConfirmOrCancel";
3+
import Container from "@components/Container";
4+
import PublisherCard from "@components/PublisherCard";
5+
import Checkbox from "@components/form/Checkbox";
6+
import { useState } from "react";
7+
import { useTranslation } from "react-i18next";
8+
import ScreenHeader from "~/app/components/ScreenHeader";
9+
import { useNavigationState } from "~/app/hooks/useNavigationState";
10+
import { USER_REJECTED_ERROR } from "~/common/constants";
11+
import msg from "~/common/lib/msg";
12+
import utils from "~/common/lib/utils";
13+
import { OriginData } from "~/types";
14+
15+
function NostrConfirm() {
16+
const { t } = useTranslation("translation", {
17+
keyPrefix: "nostr",
18+
});
19+
const { t: tCommon } = useTranslation("common");
20+
const navState = useNavigationState();
21+
const origin = navState.origin as OriginData;
22+
const description = navState.args?.description;
23+
const details = navState.args?.details;
24+
const [loading, setLoading] = useState(false);
25+
const [rememberPermission, setRememberPermission] = useState(false);
26+
27+
function confirm() {
28+
setLoading(true);
29+
msg.reply({
30+
confirm: true,
31+
rememberPermission,
32+
});
33+
setLoading(false);
34+
}
35+
36+
function reject(event: React.MouseEvent<HTMLAnchorElement>) {
37+
event.preventDefault();
38+
msg.error(USER_REJECTED_ERROR);
39+
}
40+
41+
async function block(event: React.MouseEvent<HTMLAnchorElement>) {
42+
event.preventDefault();
43+
await utils.call("addBlocklist", {
44+
domain: origin.domain,
45+
host: origin.host,
46+
});
47+
msg.error(USER_REJECTED_ERROR);
48+
}
49+
50+
return (
51+
<div className="h-full flex flex-col overflow-y-auto no-scrollbar">
52+
<ScreenHeader title={t("title")} />
53+
<Container justifyBetween maxWidth="sm">
54+
<div>
55+
<PublisherCard
56+
title={origin.name}
57+
image={origin.icon}
58+
url={origin.host}
59+
isSmall={false}
60+
/>
61+
<div className="dark:text-white pt-6 mb-4">
62+
<p className="mb-2">{t("allow", { host: origin.host })}</p>
63+
<p className="dark:text-white">
64+
<CheckIcon className="w-5 h-5 mr-2 inline" />
65+
{description}
66+
{details && (
67+
<>
68+
<br />
69+
<i className="ml-7">{details}</i>
70+
</>
71+
)}
72+
</p>
73+
</div>
74+
75+
<div className="flex items-center">
76+
<Checkbox
77+
id="remember_permission"
78+
name="remember_permission"
79+
checked={rememberPermission}
80+
onChange={(event) => {
81+
setRememberPermission(event.target.checked);
82+
}}
83+
/>
84+
<label
85+
htmlFor="remember_permission"
86+
className="cursor-pointer ml-2 block text-sm text-gray-900 font-medium dark:text-white"
87+
>
88+
{t("confirm_sign_message.remember.label")}
89+
</label>
90+
</div>
91+
</div>
92+
<div className="mb-4 text-center flex flex-col">
93+
<ConfirmOrCancel
94+
disabled={loading}
95+
loading={loading}
96+
label={tCommon("actions.confirm")}
97+
onConfirm={confirm}
98+
onCancel={reject}
99+
/>
100+
<a
101+
className="underline text-sm text-gray-400 mx-4 overflow-hidden text-ellipsis whitespace-nowrap"
102+
href="#"
103+
onClick={block}
104+
>
105+
{t("block_and_ignore", { host: origin.host })}
106+
</a>
107+
</div>
108+
</Container>
109+
</div>
110+
);
111+
}
112+
113+
export default NostrConfirm;

src/app/screens/Nostr/ConfirmGetPublicKey.tsx

+30-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { CheckIcon } from "@bitcoin-design/bitcoin-icons-react/filled";
22
import ConfirmOrCancel from "@components/ConfirmOrCancel";
33
import Container from "@components/Container";
44
import PublisherCard from "@components/PublisherCard";
5+
import Checkbox from "@components/form/Checkbox";
6+
import { useState } from "react";
57
import { useTranslation } from "react-i18next";
68
import ScreenHeader from "~/app/components/ScreenHeader";
79
import { useNavigationState } from "~/app/hooks/useNavigationState";
@@ -17,11 +19,16 @@ function NostrConfirmGetPublicKey() {
1719
const { t: tCommon } = useTranslation("common");
1820
const navState = useNavigationState();
1921
const origin = navState.origin as OriginData;
22+
const [loading, setLoading] = useState(false);
23+
const [rememberPermission, setRememberPermission] = useState(false);
2024

21-
function enable() {
25+
function confirm() {
26+
setLoading(true);
2227
msg.reply({
2328
confirm: true,
29+
rememberPermission,
2430
});
31+
setLoading(false);
2532
}
2633

2734
function reject(event: React.MouseEvent<HTMLAnchorElement>) {
@@ -58,10 +65,30 @@ function NostrConfirmGetPublicKey() {
5865
</div>
5966
</div>
6067
</div>
68+
69+
<div className="flex items-center">
70+
<Checkbox
71+
id="remember_permission"
72+
name="remember_permission"
73+
checked={rememberPermission}
74+
onChange={(event) => {
75+
setRememberPermission(event.target.checked);
76+
}}
77+
/>
78+
<label
79+
htmlFor="remember_permission"
80+
className="cursor-pointer ml-2 block text-sm text-gray-900 font-medium dark:text-white"
81+
>
82+
{t("confirm_sign_message.remember.label")}
83+
</label>
84+
</div>
85+
6186
<div className="mb-4 text-center flex flex-col">
6287
<ConfirmOrCancel
63-
label={tCommon("actions.connect")}
64-
onConfirm={enable}
88+
disabled={loading}
89+
loading={loading}
90+
label={tCommon("actions.confirm")}
91+
onConfirm={confirm}
6592
onCancel={reject}
6693
/>
6794
<a

src/app/screens/Nostr/ConfirmSignMessage.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ function ConfirmSignMessage() {
7272
url={origin.host}
7373
/>
7474
<ContentMessage
75-
heading={t("content", { host: origin.host })}
75+
heading={t("permissions.allow_sign", { host: origin.host })}
7676
content={event.content}
7777
/>
7878
<div className="flex items-center">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import utils from "~/common/lib/utils";
2+
import state from "~/extension/background-script/state";
3+
import i18n from "~/i18n/i18nConfig";
4+
import { MessageDecryptGet, PermissionMethodNostr } from "~/types";
5+
6+
import { hasPermissionFor, addPermissionFor } from "./helpers";
7+
8+
const decryptOrPrompt = async (message: MessageDecryptGet) => {
9+
if (!("host" in message.origin)) {
10+
console.error("error", message.origin);
11+
return;
12+
}
13+
14+
try {
15+
const hasPermission = await hasPermissionFor(
16+
PermissionMethodNostr["NOSTR_NIP04DECRYPT"],
17+
message.origin.host
18+
);
19+
20+
if (hasPermission) {
21+
const response = state
22+
.getState()
23+
.getNostr()
24+
.decrypt(message.args.peer, message.args.ciphertext);
25+
26+
return { data: response };
27+
} else {
28+
const promptResponse = await utils.openPrompt<{
29+
confirm: boolean;
30+
rememberPermission: boolean;
31+
}>({
32+
...message,
33+
action: "public/nostr/confirm",
34+
args: {
35+
description: i18n.t("nostr.permissions.decrypt"),
36+
},
37+
});
38+
39+
// add permission to db only if user decided to always allow this request
40+
if (promptResponse.data.rememberPermission) {
41+
await addPermissionFor(
42+
PermissionMethodNostr["NOSTR_NIP04DECRYPT"],
43+
message.origin.host
44+
);
45+
}
46+
if (promptResponse.data.confirm) {
47+
const response = state
48+
.getState()
49+
.getNostr()
50+
.decrypt(message.args.peer, message.args.ciphertext);
51+
52+
return { data: response };
53+
} else {
54+
return { error: "User rejected" };
55+
}
56+
}
57+
} catch (e) {
58+
console.error("decrypt failed", e);
59+
if (e instanceof Error) {
60+
return { error: e.message };
61+
}
62+
}
63+
};
64+
65+
export default decryptOrPrompt;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import utils from "~/common/lib/utils";
2+
import state from "~/extension/background-script/state";
3+
import i18n from "~/i18n/i18nConfig";
4+
import { MessageEncryptGet, PermissionMethodNostr } from "~/types";
5+
6+
import { hasPermissionFor, addPermissionFor } from "./helpers";
7+
8+
const encryptOrPrompt = async (message: MessageEncryptGet) => {
9+
if (!("host" in message.origin)) {
10+
console.error("error", message.origin);
11+
return;
12+
}
13+
14+
try {
15+
const hasPermission = await hasPermissionFor(
16+
PermissionMethodNostr["NOSTR_NIP04ENCRYPT"],
17+
message.origin.host
18+
);
19+
20+
if (hasPermission) {
21+
const response = state
22+
.getState()
23+
.getNostr()
24+
.encrypt(message.args.peer, message.args.plaintext);
25+
26+
return { data: response };
27+
} else {
28+
const promptResponse = await utils.openPrompt<{
29+
confirm: boolean;
30+
rememberPermission: boolean;
31+
}>({
32+
...message,
33+
action: "public/nostr/confirm",
34+
args: {
35+
description: i18n.t("nostr.permissions.encrypt"),
36+
},
37+
});
38+
39+
// add permission to db only if user decided to always allow this request
40+
if (promptResponse.data.rememberPermission) {
41+
await addPermissionFor(
42+
PermissionMethodNostr["NOSTR_NIP04ENCRYPT"],
43+
message.origin.host
44+
);
45+
}
46+
if (promptResponse.data.confirm) {
47+
const response = state
48+
.getState()
49+
.getNostr()
50+
.encrypt(message.args.peer, message.args.plaintext);
51+
52+
return { data: response };
53+
} else {
54+
return { error: "User rejected" };
55+
}
56+
}
57+
} catch (e) {
58+
console.error("encrypt failed", e);
59+
if (e instanceof Error) {
60+
return { error: e.message };
61+
}
62+
}
63+
};
64+
65+
export default encryptOrPrompt;

0 commit comments

Comments
 (0)