snort/packages/app/src/Pages/onboarding/start.tsx

208 lines
6.6 KiB
TypeScript

import { unwrap } from "@snort/shared";
import { NotEncrypted } from "@snort/system";
import { SnortContext } from "@snort/system-react";
import classNames from "classnames";
import { FormEvent, useContext, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { Link, useNavigate } from "react-router-dom";
import AsyncButton from "@/Components/Button/AsyncButton";
import Icon from "@/Components/Icons/Icon";
import useLoginHandler from "@/Hooks/useLoginHandler";
import { trackEvent } from "@/Utils";
import { generateNewLogin, LoginSessionType, LoginStore } from "@/Utils/Login";
import { NewUserState } from ".";
const NSEC_NPUB_REGEX = /(nsec1|npub1)[a-zA-Z0-9]{20,65}/gi;
export function SignIn() {
const navigate = useNavigate();
const { formatMessage } = useIntl();
const [key, setKey] = useState("");
const [error, setError] = useState("");
const [useKey, setUseKey] = useState(false);
const loginHandler = useLoginHandler();
const hasNip7 = "nostr" in window;
async function doNip07Login() {
/*const relays =
"getRelays" in unwrap(window.nostr) ? await unwrap(window.nostr?.getRelays).call(window.nostr) : undefined;*/
const pubKey = await unwrap(window.nostr).getPublicKey();
LoginStore.loginWithPubkey(pubKey, LoginSessionType.Nip7);
trackEvent("Login", { type: "NIP7" });
navigate("/");
}
async function onSubmit(e) {
e.preventDefault();
doLogin(key);
}
async function doLogin(key: string) {
setError("");
try {
await loginHandler.doLogin(key, key => Promise.resolve(new NotEncrypted(key)));
trackEvent("Login", { type: "Key" });
navigate("/");
} catch (e) {
if (e instanceof Error) {
setError(e.message);
} else {
setError(
formatMessage({
defaultMessage: "Unknown login error",
id: "OLEm6z",
}),
);
}
console.error(e);
}
}
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
if (val.match(NSEC_NPUB_REGEX)) {
doLogin(val);
} else {
setKey(val);
}
};
const nip7Login = hasNip7 && !useKey;
return (
<div className="flex flex-col g24">
<img src={CONFIG.icon} width={48} height={48} className="br mr-auto ml-auto" />
<div className="flex flex-col g16 items-center">
<h1>
<FormattedMessage defaultMessage="Sign In" />
</h1>
{nip7Login && <FormattedMessage defaultMessage="Use a nostr signer extension to sign in" />}
</div>
<div className={classNames("flex flex-col g16", { "items-center": nip7Login })}>
{hasNip7 && !useKey && (
<>
<AsyncButton onClick={doNip07Login}>
<div className="circle bg-warning p12 text-white">
<Icon name="key" />
</div>
<FormattedMessage defaultMessage="Sign in with Nostr Extension" />
</AsyncButton>
<Link to="" className="highlight">
<FormattedMessage defaultMessage="Supported Extensions" />
</Link>
<AsyncButton onClick={() => setUseKey(true)}>
<FormattedMessage defaultMessage="Sign in with key" />
</AsyncButton>
</>
)}
{(!hasNip7 || useKey) && (
<form onSubmit={onSubmit} className="flex flex-col gap-4">
<input
type="text"
placeholder={formatMessage({
defaultMessage: "nsec, npub, nip-05, hex, mnemonic",
id: "X7xU8J",
})}
value={key}
onChange={onChange}
className="new-username"
/>
{error && <b className="error">{error}</b>}
<div className="flex justify-center">
<AsyncButton onClick={onSubmit} className="primary">
<FormattedMessage defaultMessage="Login" />
</AsyncButton>
</div>
</form>
)}
</div>
<div className="flex flex-col g16 items-center">
<Link to={"/login/sign-up"}>
<FormattedMessage defaultMessage="Don't have an account?" />
</Link>
<AsyncButton className="secondary" onClick={() => navigate("/login/sign-up")}>
<FormattedMessage defaultMessage="Sign Up" />
</AsyncButton>
</div>
</div>
);
}
export function SignUp() {
const { formatMessage } = useIntl();
const navigate = useNavigate();
const [name, setName] = useState("");
const system = useContext(SnortContext);
const onSubmit = async (e: FormEvent) => {
e.preventDefault();
if (CONFIG.signUp.quickStart) {
return generateNewLogin(system, key => Promise.resolve(new NotEncrypted(key)), {
name,
}).then(() => {
trackEvent("Login", { newAccount: true });
navigate("/trending/notes");
});
}
navigate("/login/sign-up/profile", {
state: {
name: name,
} as NewUserState,
});
};
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
if (val.match(NSEC_NPUB_REGEX)) {
e.preventDefault();
} else {
setName(val);
}
};
return (
<div className="flex flex-col g24">
<img src={CONFIG.icon} width={48} height={48} className="br mr-auto ml-auto" />
<div className="flex flex-col g16 items-center">
<h1>
<FormattedMessage defaultMessage="Sign Up" />
</h1>
<FormattedMessage defaultMessage="What should we call you?" />
</div>
<form onSubmit={onSubmit} className="flex flex-col g16">
<input
type="text"
autoFocus={true}
placeholder={formatMessage({
defaultMessage: "Name or nym",
id: "aHje0o",
})}
value={name}
onChange={onChange}
className="new-username"
/>
<AsyncButton className="primary" disabled={name.length === 0} onClick={onSubmit}>
{CONFIG.signUp.quickStart ? (
<FormattedMessage
description="Button text after entering username in quick signup"
defaultMessage="Go"
id="0zASjL"
/>
) : (
<FormattedMessage defaultMessage="Next" />
)}
</AsyncButton>
</form>
<div className="flex flex-col g16 items-center">
<Link to={"/login"}>
<FormattedMessage defaultMessage="Already have an account?" />
</Link>
<AsyncButton className="secondary" onClick={() => navigate("/login")}>
<FormattedMessage defaultMessage="Sign In" />
</AsyncButton>
</div>
</div>
);
}