Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/nip04 and permissions #1801

Merged
merged 14 commits into from
Dec 2, 2022
6 changes: 4 additions & 2 deletions src/app/router/Prompt/Prompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import LNURLChannel from "@screens/LNURLChannel";
import LNURLPay from "@screens/LNURLPay";
import LNURLWithdraw from "@screens/LNURLWithdraw";
import MakeInvoice from "@screens/MakeInvoice";
import NostrConfirm from "@screens/Nostr/Confirm";
import NostrConfirmGetPublicKey from "@screens/Nostr/ConfirmGetPublicKey";
import NostrConfirmSignMessage from "@screens/Nostr/ConfirmSignMessage";
import Unlock from "@screens/Unlock";
import { HashRouter, Outlet, Route, Routes, Navigate } from "react-router-dom";
import { ToastContainer } from "react-toastify";
import Providers from "~/app/context/Providers";
import RequireAuth from "~/app/router/RequireAuth";
import NostrConfirmGetPublicKey from "~/app/screens/Nostr/ConfirmGetPublicKey";
import NostrConfirmSignMessage from "~/app/screens/Nostr/ConfirmSignMessage";
import type { NavigationState, OriginData } from "~/types";

// Parse out the parameters from the querystring.
Expand Down Expand Up @@ -72,6 +73,7 @@ function Prompt() {
path="public/nostr/enable"
element={<Enable origin={navigationState.origin as OriginData} />} // prompt will always have an `origin` set, just the type is optional to support usage via PopUp
/>
<Route path="public/nostr/confirm" element={<NostrConfirm />} />
<Route
path="public/nostr/confirmGetPublicKey"
element={<NostrConfirmGetPublicKey />}
Expand Down
113 changes: 113 additions & 0 deletions src/app/screens/Nostr/Confirm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { CheckIcon } from "@bitcoin-design/bitcoin-icons-react/filled";
import ConfirmOrCancel from "@components/ConfirmOrCancel";
import Container from "@components/Container";
import PublisherCard from "@components/PublisherCard";
import Checkbox from "@components/form/Checkbox";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import ScreenHeader from "~/app/components/ScreenHeader";
import { useNavigationState } from "~/app/hooks/useNavigationState";
import { USER_REJECTED_ERROR } from "~/common/constants";
import msg from "~/common/lib/msg";
import utils from "~/common/lib/utils";
import { OriginData } from "~/types";

function NostrConfirm() {
const { t } = useTranslation("translation", {
keyPrefix: "nostr",
});
const { t: tCommon } = useTranslation("common");
const navState = useNavigationState();
const origin = navState.origin as OriginData;
const description = navState.args?.description;
const details = navState.args?.details;
const [loading, setLoading] = useState(false);
const [rememberPermission, setRememberPermission] = useState(false);

function confirm() {
setLoading(true);
msg.reply({
confirm: true,
rememberPermission,
});
setLoading(false);
}

function reject(event: React.MouseEvent<HTMLAnchorElement>) {
event.preventDefault();
msg.error(USER_REJECTED_ERROR);
}

async function block(event: React.MouseEvent<HTMLAnchorElement>) {
event.preventDefault();
await utils.call("addBlocklist", {
domain: origin.domain,
host: origin.host,
});
msg.error(USER_REJECTED_ERROR);
}

return (
<div className="h-full flex flex-col overflow-y-auto no-scrollbar">
<ScreenHeader title={t("title")} />
<Container justifyBetween maxWidth="sm">
<div>
<PublisherCard
title={origin.name}
image={origin.icon}
url={origin.host}
isSmall={false}
/>
<div className="dark:text-white pt-6 mb-4">
<p className="mb-2">{t("allow", { host: origin.host })}</p>
<p className="dark:text-white">
<CheckIcon className="w-5 h-5 mr-2 inline" />
{description}
{details && (
<>
<br />
<i className="ml-7">{details}</i>
</>
)}
</p>
</div>

<div className="flex items-center">
<Checkbox
id="remember_permission"
name="remember_permission"
checked={rememberPermission}
onChange={(event) => {
setRememberPermission(event.target.checked);
}}
/>
<label
htmlFor="remember_permission"
className="cursor-pointer ml-2 block text-sm text-gray-900 font-medium dark:text-white"
>
{t("confirm_sign_message.remember.label")}
</label>
</div>
</div>
<div className="mb-4 text-center flex flex-col">
<ConfirmOrCancel
disabled={loading}
loading={loading}
label={tCommon("actions.confirm")}
onConfirm={confirm}
onCancel={reject}
/>
<a
className="underline text-sm text-gray-400 mx-4 overflow-hidden text-ellipsis whitespace-nowrap"
href="#"
onClick={block}
>
{t("block_and_ignore", { host: origin.host })}
</a>
</div>
</Container>
</div>
);
}

export default NostrConfirm;
33 changes: 30 additions & 3 deletions src/app/screens/Nostr/ConfirmGetPublicKey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { CheckIcon } from "@bitcoin-design/bitcoin-icons-react/filled";
import ConfirmOrCancel from "@components/ConfirmOrCancel";
import Container from "@components/Container";
import PublisherCard from "@components/PublisherCard";
import Checkbox from "@components/form/Checkbox";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import ScreenHeader from "~/app/components/ScreenHeader";
import { useNavigationState } from "~/app/hooks/useNavigationState";
Expand All @@ -17,11 +19,16 @@ function NostrConfirmGetPublicKey() {
const { t: tCommon } = useTranslation("common");
const navState = useNavigationState();
const origin = navState.origin as OriginData;
const [loading, setLoading] = useState(false);
const [rememberPermission, setRememberPermission] = useState(false);

function enable() {
function confirm() {
setLoading(true);
msg.reply({
confirm: true,
rememberPermission,
});
setLoading(false);
}

function reject(event: React.MouseEvent<HTMLAnchorElement>) {
Expand Down Expand Up @@ -58,10 +65,30 @@ function NostrConfirmGetPublicKey() {
</div>
</div>
</div>

<div className="flex items-center">
<Checkbox
id="remember_permission"
name="remember_permission"
checked={rememberPermission}
onChange={(event) => {
setRememberPermission(event.target.checked);
}}
/>
<label
htmlFor="remember_permission"
className="cursor-pointer ml-2 block text-sm text-gray-900 font-medium dark:text-white"
>
{t("confirm_sign_message.remember.label")}
</label>
</div>

<div className="mb-4 text-center flex flex-col">
<ConfirmOrCancel
label={tCommon("actions.connect")}
onConfirm={enable}
disabled={loading}
loading={loading}
label={tCommon("actions.confirm")}
onConfirm={confirm}
onCancel={reject}
/>
<a
Expand Down
2 changes: 1 addition & 1 deletion src/app/screens/Nostr/ConfirmSignMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function ConfirmSignMessage() {
url={origin.host}
/>
<ContentMessage
heading={t("content", { host: origin.host })}
heading={t("permissions.allow_sign", { host: origin.host })}
content={event.content}
/>
<div className="flex items-center">
Expand Down
65 changes: 65 additions & 0 deletions src/extension/background-script/actions/nostr/decryptOrPrompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import utils from "~/common/lib/utils";
import state from "~/extension/background-script/state";
import i18n from "~/i18n/i18nConfig";
import { MessageDecryptGet, PermissionMethodNostr } from "~/types";

import { hasPermissionFor, addPermissionFor } from "./helpers";

const decryptOrPrompt = async (message: MessageDecryptGet) => {
if (!("host" in message.origin)) {
console.error("error", message.origin);
return;
}

try {
const hasPermission = await hasPermissionFor(
PermissionMethodNostr["NOSTR_NIP04DECRYPT"],
message.origin.host
);

if (hasPermission) {
const response = state
.getState()
.getNostr()
.decrypt(message.args.peer, message.args.ciphertext);

return { data: response };
} else {
const promptResponse = await utils.openPrompt<{
confirm: boolean;
rememberPermission: boolean;
}>({
...message,
action: "public/nostr/confirm",
args: {
description: i18n.t("nostr.permissions.decrypt"),
},
});

// add permission to db only if user decided to always allow this request
if (promptResponse.data.rememberPermission) {
await addPermissionFor(
PermissionMethodNostr["NOSTR_NIP04DECRYPT"],
message.origin.host
);
}
if (promptResponse.data.confirm) {
const response = state
.getState()
.getNostr()
.decrypt(message.args.peer, message.args.ciphertext);

return { data: response };
} else {
return { error: "User rejected" };
}
}
} catch (e) {
console.error("decrypt failed", e);
if (e instanceof Error) {
return { error: e.message };
}
}
};

export default decryptOrPrompt;
65 changes: 65 additions & 0 deletions src/extension/background-script/actions/nostr/encryptOrPrompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import utils from "~/common/lib/utils";
import state from "~/extension/background-script/state";
import i18n from "~/i18n/i18nConfig";
import { MessageEncryptGet, PermissionMethodNostr } from "~/types";

import { hasPermissionFor, addPermissionFor } from "./helpers";

const encryptOrPrompt = async (message: MessageEncryptGet) => {
if (!("host" in message.origin)) {
console.error("error", message.origin);
return;
}

try {
const hasPermission = await hasPermissionFor(
PermissionMethodNostr["NOSTR_NIP04ENCRYPT"],
message.origin.host
);

if (hasPermission) {
const response = state
.getState()
.getNostr()
.encrypt(message.args.peer, message.args.plaintext);

return { data: response };
} else {
const promptResponse = await utils.openPrompt<{
confirm: boolean;
rememberPermission: boolean;
}>({
...message,
action: "public/nostr/confirm",
args: {
description: i18n.t("nostr.permissions.encrypt"),
},
});

// add permission to db only if user decided to always allow this request
if (promptResponse.data.rememberPermission) {
await addPermissionFor(
PermissionMethodNostr["NOSTR_NIP04ENCRYPT"],
message.origin.host
);
}
if (promptResponse.data.confirm) {
const response = state
.getState()
.getNostr()
.encrypt(message.args.peer, message.args.plaintext);

return { data: response };
} else {
return { error: "User rejected" };
}
}
} catch (e) {
console.error("encrypt failed", e);
if (e instanceof Error) {
return { error: e.message };
}
}
};

export default encryptOrPrompt;
Loading