Files
zap.stream/src/element/login-signup.tsx
2024-02-27 19:16:50 +00:00

340 lines
11 KiB
TypeScript

import "./login-signup.css";
import LoginHeader from "../login-start.jpg";
import LoginHeader2x from "../login-start@2x.jpg";
import LoginVault from "../login-vault.jpg";
import LoginVault2x from "../login-vault@2x.jpg";
import LoginProfile from "../login-profile.jpg";
import LoginProfile2x from "../login-profile@2x.jpg";
import LoginKey from "../login-key.jpg";
import LoginKey2x from "../login-key@2x.jpg";
import LoginWallet from "../login-wallet.jpg";
import LoginWallet2x from "../login-wallet@2x.jpg";
import { CSSProperties, useContext, useState } from "react";
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
import { EventPublisher, UserMetadata } from "@snort/system";
import { schnorr } from "@noble/curves/secp256k1";
import { bytesToHex } from "@noble/curves/abstract/utils";
import { LNURL, bech32ToHex, getPublicKey, hexToBech32 } from "@snort/shared";
import { VoidApi } from "@void-cat/api";
import { SnortContext } from "@snort/system-react";
import { Login } from "@/index";
import { Icon } from "./icon";
import Copy from "./copy";
import { openFile } from "@/utils";
import { LoginType } from "@/login";
import { DefaultProvider, StreamProviderInfo } from "@/providers";
import { NostrStreamProvider } from "@/providers/zsz";
import { DefaultButton, Layer1Button } from "./buttons";
enum Stage {
Login = 0,
LoginInput = 1,
Details = 2,
LnAddress = 3,
SaveKey = 4,
}
export function LoginSignup({ close }: { close: () => void }) {
const system = useContext(SnortContext);
const [error, setError] = useState("");
const [stage, setStage] = useState(Stage.Login);
const [username, setUsername] = useState("");
const [lnAddress, setLnAddress] = useState("");
const [providerInfo, setProviderInfo] = useState<StreamProviderInfo>();
const [avatar, setAvatar] = useState("");
const [key, setNewKey] = useState("");
const { formatMessage } = useIntl();
const hasNostrExtension = "nostr" in window && window.nostr;
function doLoginNsec() {
try {
const hexKey = key.startsWith("nsec") ? bech32ToHex(key) : key;
Login.loginWithPrivateKey(hexKey);
close();
} catch (e) {
console.error(e);
if (e instanceof Error) {
setError(e.message);
} else {
setError(e as string);
}
}
}
async function loginNip7() {
try {
const nip7 = await EventPublisher.nip7();
if (nip7) {
Login.loginWithPubkey(nip7.pubKey, LoginType.Nip7);
}
} catch (e) {
if (e instanceof Error) {
setError(e.message);
} else {
setError(e as string);
}
}
}
function createAccount() {
const newKey = bytesToHex(schnorr.utils.randomPrivateKey());
setNewKey(newKey);
setLnAddress(`${getPublicKey(newKey)}@${window.location.host}`);
setStage(Stage.Details);
}
function loginWithKey() {
Login.loginWithPrivateKey(key);
close();
}
async function uploadAvatar() {
const file = await openFile();
if (file) {
const VoidCatHost = "https://void.cat";
const api = new VoidApi(VoidCatHost);
const uploader = api.getUploader(file);
const result = await uploader.upload({
"V-Strip-Metadata": "true",
});
if (result.ok) {
const resultUrl = result.file?.metadata?.url ?? `${VoidCatHost}/d/${result.file?.id}`;
setAvatar(resultUrl);
} else {
setError(result.errorMessage ?? "Upload failed");
}
}
}
async function setupProfile() {
const px = new NostrStreamProvider(DefaultProvider.name, DefaultProvider.url, EventPublisher.privateKey(key));
const info = await px.info();
setProviderInfo(info);
setStage(Stage.LnAddress);
}
async function saveProfile() {
try {
// validate LN addreess
try {
const lnurl = new LNURL(lnAddress);
await lnurl.load();
} catch {
throw new Error(
formatMessage({
defaultMessage: "Hmm, your lightning address looks wrong",
id: "4l69eO",
})
);
}
const pub = EventPublisher.privateKey(key);
const profile = {
name: username,
picture: avatar,
lud16: lnAddress,
} as UserMetadata;
const ev = await pub.metadata(profile);
console.debug(ev);
system.BroadcastEvent(ev);
setStage(Stage.SaveKey);
} catch (e) {
if (e instanceof Error) {
setError(e.message);
} else {
setError(e as string);
}
}
}
switch (stage) {
case Stage.Login: {
return (
<>
<img src={LoginHeader as string} srcSet={`${LoginHeader2x} 2x`} className="header-image" />
<div className="content-inner">
<h2>
<FormattedMessage defaultMessage="Create an Account" id="u6uD94" />
</h2>
<h3>
<FormattedMessage defaultMessage="No emails, just awesomeness!" id="+AcVD+" />
</h3>
<DefaultButton onClick={createAccount}>
<FormattedMessage defaultMessage="Create Account" id="5JcXdV" />
</DefaultButton>
<div className="or-divider">
<hr />
<FormattedMessage defaultMessage="OR" id="INlWvJ" />
<hr />
</div>
{hasNostrExtension && (
<>
<DefaultButton onClick={loginNip7}>
<FormattedMessage defaultMessage="Nostr Extension" id="ebmhes" />
</DefaultButton>
</>
)}
<DefaultButton onClick={() => setStage(Stage.LoginInput)}>
<FormattedMessage defaultMessage="Login with Private Key (insecure)" id="feZ/kG" />
</DefaultButton>
{error && <b className="error">{error}</b>}
</div>
</>
);
}
case Stage.LoginInput: {
return (
<>
<img src={LoginVault as string} srcSet={`${LoginVault2x} 2x`} className="header-image" />
<div className="content-inner">
<h2>
<FormattedMessage defaultMessage="Login with private key" id="3df560" />
</h2>
<p>
<FormattedMessage
defaultMessage="This method is insecure. We recommend using a {nostrlink}"
id="Z8ZOEY"
values={{
nostrlink: (
<a href="">
<FormattedMessage defaultMessage="nostr signer extension" id="/EvlqN" />
</a>
),
}}
/>
</p>
<input
type="text"
value={key}
onChange={e => setNewKey(e.target.value)}
placeholder={formatMessage({ defaultMessage: "eg. nsec1xyz", id: "yzKwBQ" })}
/>
<div className="flex justify-between">
<div></div>
<div className="flex gap-1">
<Layer1Button
onClick={() => {
setNewKey("");
setStage(Stage.Login);
}}>
<FormattedMessage defaultMessage="Cancel" id="47FYwb" />
</Layer1Button>
<DefaultButton onClick={doLoginNsec}>
<FormattedMessage defaultMessage="Log In" id="r2Jjms" />
</DefaultButton>
</div>
</div>
{error && <b className="error">{error}</b>}
</div>
</>
);
}
case Stage.Details: {
return (
<>
<img src={LoginProfile as string} srcSet={`${LoginProfile2x} 2x`} className="header-image" />
<div className="content-inner">
<h2>
<FormattedMessage defaultMessage="Setup Profile" id="nOaArs" />
</h2>
<div className="flex items-center">
<div
className="avatar-input"
onClick={uploadAvatar}
style={
{
"--img": `url(${avatar})`,
} as CSSProperties
}>
<Icon name="camera-plus" />
</div>
</div>
<div className="username">
<input type="text" placeholder="Username" value={username} onChange={e => setUsername(e.target.value)} />
<small>
<FormattedMessage defaultMessage="You can change this later" id="ZmqxZs" />
</small>
</div>
<DefaultButton onClick={setupProfile}>
<FormattedMessage defaultMessage="Save" id="jvo0vs" />
</DefaultButton>
</div>
</>
);
}
case Stage.LnAddress: {
return (
<>
<img src={LoginWallet as string} srcSet={`${LoginWallet2x} 2x`} className="header-image" />
<div className="content-inner">
<h2>
<FormattedMessage defaultMessage="Get paid by viewers" id="Fodi9+" />
</h2>
<p>
<FormattedMessage
defaultMessage="We hooked you up with a lightning wallet so you can get paid by viewers right away!"
id="Oxqtyf"
/>
</p>
{providerInfo?.balance && (
<p>
<FormattedMessage
defaultMessage="Oh, and you have {n} sats of free streaming on us! 💜"
id="f6biFA"
values={{
n: <FormattedNumber value={providerInfo.balance} />,
}}
/>
</p>
)}
<div className="username">
<input
type="text"
placeholder={formatMessage({ defaultMessage: "eg. name@wallet.com", id: "1qsXCO" })}
value={lnAddress}
onChange={e => setLnAddress(e.target.value)}
/>
<small>
<FormattedMessage defaultMessage="You can always replace it with your own address later." id="FjDlus" />
</small>
</div>
{error && <b className="error">{error}</b>}
<DefaultButton onClick={saveProfile}>
<FormattedMessage defaultMessage="Amazing! Continue.." id="tM6fNW" />
</DefaultButton>
</div>
</>
);
}
case Stage.SaveKey: {
return (
<>
<img src={LoginKey as string} srcSet={`${LoginKey2x} 2x`} className="header-image" />
<div className="content-inner">
<h2>
<FormattedMessage defaultMessage="Save Key" id="04lmFi" />
</h2>
<p>
<FormattedMessage
defaultMessage="Save this and keep it safe! If you lose this key, you won't be able to access your account ever again. Yep, it's that serious!"
id="H/bNs9"
/>
</p>
<div className="bg-layer-1 rounded-xl px-3 py-2">
<Copy text={hexToBech32("nsec", key)} />
</div>
<DefaultButton onClick={loginWithKey}>
<FormattedMessage defaultMessage="Ok, it's safe" id="My6HwN" />
</DefaultButton>
</div>
</>
);
}
}
}