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

feat: revamp-nostr-settings #3147

Merged
merged 13 commits into from
May 23, 2024
Merged
4 changes: 2 additions & 2 deletions src/app/screens/Accounts/Detail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ function AccountDetail() {
</div>

<div className="flex items-center gap-2">
<p className="text-gray-600 text-sm dark:text-neutral-400">
<p className="text-gray-600 text-sm dark:text-neutral-400 text-ellipsis whitespace-nowrap overflow-hidden">
{nostrPublicKey}
</p>
{nostrPublicKey && (
Expand Down Expand Up @@ -496,7 +496,7 @@ function AccountDetail() {
components={[
// eslint-disable-next-line react/jsx-key
<Link
to="secret-key/new"
to="secret-key/generate"
relative="path"
className="underline"
/>,
Expand Down
316 changes: 221 additions & 95 deletions src/app/screens/Accounts/NostrSettings/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import Container from "@components/Container";
import Loading from "@components/Loading";
import {
PopiconsCircleExclamationLine,
PopiconsExpandLine,
} from "@popicons/react";
import { FormEvent, useCallback, useEffect, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { Link, useNavigate, useParams } from "react-router-dom";
import { useNavigate, useParams } from "react-router-dom";
import Alert from "~/app/components/Alert";
import Button from "~/app/components/Button";
import { ContentBox } from "~/app/components/ContentBox";
import TextField from "~/app/components/form/TextField";
import InputCopyButton from "~/app/components/InputCopyButton";
import MenuDivider from "~/app/components/Menu/MenuDivider";
import PasswordViewAdornment from "~/app/components/PasswordViewAdornment";
import toast from "~/app/components/Toast";
import TextField from "~/app/components/form/TextField";
import { isAlbyOAuthAccount } from "~/app/utils";
import api, { GetAccountRes } from "~/common/lib/api";
import { default as nostr } from "~/common/lib/nostr";

Expand All @@ -27,10 +32,20 @@ function NostrSettings() {
const [hasImportedNostrKey, setHasImportedNostrKey] = useState(false);
const [account, setAccount] = useState<GetAccountRes>();
const { id } = useParams() as { id: string };
const [NIP05Key, setNIP05Key] = useState("");
const [lightningAddress, setLightningAddress] = useState("");

const fetchData = useCallback(async () => {
if (id) {
const priv = await api.nostr.getPrivateKey(id);
const account = await api.getAccountInfo();
if (account.info.nostr_pubkey) {
setNIP05Key(account.info.nostr_pubkey);
}

if (account.info.lightning_address) {
setLightningAddress(account.info.lightning_address);
}
if (priv) {
setCurrentPrivateKey(priv);
const nsec = nostr.hexToNip19(priv);
Expand Down Expand Up @@ -61,11 +76,6 @@ function NostrSettings() {
}
}, [nostrPrivateKey, t]);

function onCancel() {
// go to account settings
navigate(`/accounts/${id}`);
}

function handleDeleteKeys() {
setNostrPrivateKey("");
}
Expand Down Expand Up @@ -120,107 +130,223 @@ function NostrSettings() {
<Loading />
</div>
) : (
<div>
<form
onSubmit={(e: FormEvent) => {
e.preventDefault();
handleSaveNostrPrivateKey();
}}
>
<Container maxWidth="sm">
<ContentBox>
<div>
<h1 className="font-bold text-2xl dark:text-white">
{t("nostr.settings.title")}
</h1>
<p className="text-gray-500 dark:text-neutral-500">
{t("nostr.settings.description")}
<Container>
<div className="flex flex-col gap-12 mt-12">
<div className="flex flex-col gap-6">
<h2 className="text-2xl font-bold dark:text-white leading-8">
{t("nostr.settings.title")}
</h2>

<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<h2 className="text-xl font-bold dark:text-white leading-7">
{t("nostr.settings.nostr_keys.title")}
</h2>

<p className="text-gray-600 dark:text-neutral-400 text-sm leading-6">
{t("nostr.settings.nostr_keys.description")}
</p>
</div>
<div className="shadow bg-white rounded-md sm:overflow-hidden p-6 dark:bg-surface-01dp flex flex-col gap-4">
{hasMnemonic && currentPrivateKey ? (
hasImportedNostrKey ? (
<Alert type="warn">
<div className="flex items-center gap-2">
<div className="shrink-0">
<PopiconsCircleExclamationLine className="w-5 h-5" />
</div>
<span className="text-sm">
{t("nostr.settings.imported_key_warning")}
</span>
</div>
</Alert>
) : (
<Alert type="info">
<div className="flex items-center gap-2">
<div className="shrink-0">
<PopiconsCircleExclamationLine className="w-5 h-5" />
</div>
<span className="text-sm">
{t("nostr.settings.can_restore")}
</span>
</div>
</Alert>
)
) : null}
{nostrPublicKey && (
<>
<div className="flex flex-col sm:flex-row justify-between items-center">
<div className="sm:w-9/12 w-full">
<p className="text-gray-800 dark:text-white font-medium">
{t("nostr.public_key.label")}
</p>

{!hasMnemonic && !nostrPrivateKey && (
<Alert type="info">
<Trans
i18nKey={"nostr.settings.no_secret_key"}
t={t}
components={[
// eslint-disable-next-line react/jsx-key
<Link
to="../../secret-key/new"
relative="path"
className="underline"
/>,
]}
/>
</Alert>
)}

{hasMnemonic && currentPrivateKey ? (
hasImportedNostrKey ? (
<Alert type="warn">
{t("nostr.settings.imported_key_warning")}
</Alert>
) : (
<Alert type="info">{t("nostr.settings.can_restore")}</Alert>
)
) : null}

<div>
<TextField
id="nostrPrivateKey"
label={t("nostr.private_key.label")}
autoComplete="new-password"
type={nostrPrivateKeyVisible ? "text" : "password"}
value={nostrPrivateKey}
onChange={(event) => {
setNostrPrivateKey(event.target.value.trim());
<div className="flex items-center gap-2">
<p className="text-gray-600 text-sm dark:text-neutral-400 text-ellipsis overflow-hidden whitespace-nowrap">
{nostrPublicKey}
</p>

<InputCopyButton
value={nostrPublicKey}
className="w-5 h-5"
/>
</div>
</div>
</div>
<MenuDivider />
</>
)}

<form
onSubmit={(e: FormEvent) => {
e.preventDefault();
handleSaveNostrPrivateKey();
}}
endAdornment={
<div className="flex items-center gap-1 px-2">
<PasswordViewAdornment
onChange={(passwordView) => {
setNostrPrivateKeyVisible(passwordView);
}}
className="flex flex-col sm:flex-row justify-between items-center gap-4"
>
<div className="sm:w-7/12 w-full">
<TextField
id="nostrPrivateKey"
label={t("nostr.private_key.label")}
placeholder="Enter private key"
autoComplete="new-password"
type={nostrPrivateKeyVisible ? "text" : "password"}
value={nostrPrivateKey}
onChange={(event) => {
setNostrPrivateKey(event.target.value.trim());
}}
endAdornment={
<div className="flex items-center gap-1 px-4">
<PasswordViewAdornment
onChange={(passwordView) => {
setNostrPrivateKeyVisible(passwordView);
}}
/>
<InputCopyButton
value={nostrPrivateKey}
className="w-6"
/>
</div>
}
/>
</div>
<div className="flex flex-col sm:flex-row w-full justify-end mt-0 sm:mt-6">
{hasImportedNostrKey && hasMnemonic && (
<div className="flex-none sm:w-64 w-full pt-4 sm:pt-0 mr-4">
<Button
outline
label={t("nostr.settings.derive")}
onClick={handleDeriveNostrKeyFromSecretKey}
fullWidth
/>
</div>
)}

<div className="flex-none sm:w-64 w-full pt-4 sm:pt-0">
<Button
type="submit"
label={tCommon("actions.save")}
primary
fullWidth
/>
<InputCopyButton value={nostrPrivateKey} className="w-6" />
</div>
}
/>
</div>
</form>

{nostrPrivateKey && (
<>
<MenuDivider />
<form
onSubmit={(e: FormEvent) => {
e.preventDefault();
handleSaveNostrPrivateKey();
}}
className="flex flex-col sm:flex-row justify-between items-center"
>
<div className="sm:w-7/12 w-full">
<p className="text-gray-800 dark:text-white font-medium">
{t("nostr.settings.remove_keys.title")}
</p>

<p className="text-gray-600 text-sm dark:text-neutral-400">
{t("nostr.settings.remove_keys.description")}
</p>
</div>

<div className="flex-none sm:w-64 w-full pt-4 sm:pt-0">
<Button
destructive
label={t("nostr.settings.remove")}
onClick={handleDeleteKeys}
fullWidth
/>
</div>
</form>
</>
)}
</div>
</div>
</div>
{isAlbyOAuthAccount(account.connectorType) && (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<h2 className="text-xl font-bold dark:text-white leading-7">
{t("nostr.settings.nostr_address.title")}
</h2>

<div>
<TextField
id="nostrPublicKey"
label={t("nostr.public_key.label")}
value={nostrPublicKey}
disabled
endAdornment={<InputCopyButton value={nostrPublicKey} />}
/>
<div className="mt-4 flex gap-4 items-center justify-center">
{nostrPrivateKey && (
<Button
destructive
label={t("nostr.settings.remove")}
onClick={handleDeleteKeys}
<p className="text-gray-600 dark:text-neutral-400 text-sm leading-6">
{t("nostr.settings.nostr_address.description")}
</p>
</div>
<div className="shadow bg-white rounded-md sm:overflow-hidden p-6 dark:bg-surface-01dp flex flex-col sm:flex-row gap-4">
<div className="sm:w-9/12 w-full">
<p className="text-gray-800 dark:text-white font-medium">
{t("nostr.settings.nostr_address.manage_nostr_address.title")}
</p>
<p className="text-gray-600 text-sm dark:text-neutral-400 text-ellipsis whitespace-nowrap overflow-hidden">
<Trans
i18nKey={
NIP05Key === ""
? "nostr.settings.nostr_address.manage_nostr_address.description_alternate"
: "nostr.settings.nostr_address.manage_nostr_address.description"
}
t={t}
values={{
lnaddress: lightningAddress,
npub:
NIP05Key.substring(0, 11) +
"..." +
NIP05Key.substring(NIP05Key.length - 11),
}}
// eslint-disable-next-line react/jsx-key
components={[<b></b>]}
/>
)}
{hasImportedNostrKey && hasMnemonic && (
</p>
</div>

<div className="flex-none sm:w-64 w-full pt-4 sm:pt-0">
<div className="flex flex-row gap-2">
<Button
outline
label={t("nostr.settings.derive")}
onClick={handleDeriveNostrKeyFromSecretKey}
label={t(
"nostr.settings.nostr_address.manage_nostr_address.set_nip05"
)}
iconRight={<PopiconsExpandLine className="w-5 h-5" />}
fullWidth
primary
onClick={() =>
window.open(
"https://getalby.com/settings/nostr",
"_blank"
)
}
/>
)}
</div>
</div>
</div>
</ContentBox>
<div className="flex justify-center my-6 gap-4">
<Button label={tCommon("actions.cancel")} onClick={onCancel} />
<Button type="submit" label={tCommon("actions.save")} primary />
</div>
</Container>
</form>
</div>
)}
</div>
</Container>
);
}

Expand Down
1 change: 1 addition & 0 deletions src/common/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface AccountInfoRes {
balance: { balance: string | number; currency: ACCOUNT_CURRENCIES };
currentAccountId: string;
info: {
nostr_pubkey?: string;
alias: string;
pubkey?: string;
lightning_address?: string;
Expand Down
Loading
Loading