New login flow
This commit is contained in:
@ -1,43 +1,50 @@
|
|||||||
import "./login-signup.css";
|
import "./login-signup.css";
|
||||||
|
import LoginHeader from "../login-start.png";
|
||||||
|
import LoginVault from "../login-vault.png";
|
||||||
|
import LoginProfile from "../login-profile.png";
|
||||||
|
import LoginKey from "../login-key.png";
|
||||||
|
import LoginWallet from "../login-wallet.png";
|
||||||
|
|
||||||
import { CSSProperties, useState } from "react";
|
import { CSSProperties, useState } from "react";
|
||||||
|
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
|
||||||
import { EventPublisher, UserMetadata } from "@snort/system";
|
import { EventPublisher, UserMetadata } from "@snort/system";
|
||||||
import { schnorr } from "@noble/curves/secp256k1";
|
import { schnorr } from "@noble/curves/secp256k1";
|
||||||
import { bytesToHex } from "@noble/curves/abstract/utils";
|
import { bytesToHex } from "@noble/curves/abstract/utils";
|
||||||
|
import { LNURL, bech32ToHex, getPublicKey } from "@snort/shared";
|
||||||
|
import { VoidApi } from "@void-cat/api";
|
||||||
|
|
||||||
import AsyncButton from "./async-button";
|
import AsyncButton from "./async-button";
|
||||||
import { Login, System } from "index";
|
import { Login, System } from "index";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
import Copy from "./copy";
|
import Copy from "./copy";
|
||||||
import { hexToBech32, openFile } from "utils";
|
import { hexToBech32, openFile } from "utils";
|
||||||
import { VoidApi } from "@void-cat/api";
|
import { LoginType } from "login";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { DefaultProvider, StreamProviderInfo } from "providers";
|
||||||
import { bech32 } from "@scure/base";
|
import { Nip103StreamProvider } from "providers/zsz";
|
||||||
|
|
||||||
enum Stage {
|
enum Stage {
|
||||||
Login = 0,
|
Login = 0,
|
||||||
Details = 1,
|
LoginInput = 1,
|
||||||
SaveKey = 2,
|
Details = 2,
|
||||||
|
LnAddress = 3,
|
||||||
|
SaveKey = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LoginSignup({ close }: { close: () => void }) {
|
export function LoginSignup({ close }: { close: () => void }) {
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [stage, setStage] = useState(Stage.Login);
|
const [stage, setStage] = useState(Stage.Login);
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
|
const [lnAddress, setLnAddress] = useState("");
|
||||||
|
const [providerInfo, setProviderInfo] = useState<StreamProviderInfo>();
|
||||||
const [avatar, setAvatar] = useState("");
|
const [avatar, setAvatar] = useState("");
|
||||||
const [key, setNewKey] = useState("");
|
const [key, setNewKey] = useState("");
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const hasNostrExtension = "nostr" in window && window.nostr;
|
||||||
|
|
||||||
function doLoginNsec() {
|
function doLoginNsec() {
|
||||||
try {
|
try {
|
||||||
let nsec = prompt("Enter your nsec\nWARNING: THIS IS NOT RECOMMENDED. DO NOT IMPORT ANY KEYS YOU CARE ABOUT");
|
const hexKey = key.startsWith("nsec") ? bech32ToHex(key) : key;
|
||||||
if (!nsec) {
|
Login.loginWithPrivateKey(hexKey);
|
||||||
throw new Error("no nsec provided");
|
|
||||||
}
|
|
||||||
if (nsec.startsWith("nsec")) {
|
|
||||||
const { words } = bech32.decode(nsec, 5000);
|
|
||||||
const data = new Uint8Array(bech32.fromWords(words));
|
|
||||||
nsec = bytesToHex(data);
|
|
||||||
}
|
|
||||||
Login.loginWithPrivateKey(nsec);
|
|
||||||
close();
|
close();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@ -49,9 +56,25 @@ export function LoginSignup({ close }: { close: () => void }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
function createAccount() {
|
||||||
const newKey = bytesToHex(schnorr.utils.randomPrivateKey());
|
const newKey = bytesToHex(schnorr.utils.randomPrivateKey());
|
||||||
setNewKey(newKey);
|
setNewKey(newKey);
|
||||||
|
setLnAddress(`${getPublicKey(newKey)}@zap.stream`)
|
||||||
setStage(Stage.Details);
|
setStage(Stage.Details);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,93 +101,202 @@ export function LoginSignup({ close }: { close: () => void }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setupProfile() {
|
||||||
|
const px = new Nip103StreamProvider(DefaultProvider.name, DefaultProvider.url, EventPublisher.privateKey(key));
|
||||||
|
const info = await px.info();
|
||||||
|
setProviderInfo(info);
|
||||||
|
|
||||||
|
setStage(Stage.LnAddress)
|
||||||
|
}
|
||||||
|
|
||||||
async function saveProfile() {
|
async function saveProfile() {
|
||||||
const pub = EventPublisher.privateKey(key);
|
try {
|
||||||
const profile = {
|
// validate LN addreess
|
||||||
name: username,
|
try {
|
||||||
picture: avatar,
|
const lnurl = new LNURL(lnAddress);
|
||||||
lud16: `${pub.pubKey}@zap.stream`,
|
await lnurl.load();
|
||||||
} as UserMetadata;
|
} catch {
|
||||||
|
throw new Error(formatMessage({
|
||||||
|
defaultMessage: "Hmm, your lightning address looks wrong"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
const pub = EventPublisher.privateKey(key);
|
||||||
|
const profile = {
|
||||||
|
name: username,
|
||||||
|
picture: avatar,
|
||||||
|
lud16: lnAddress,
|
||||||
|
} as UserMetadata;
|
||||||
|
|
||||||
const ev = await pub.metadata(profile);
|
const ev = await pub.metadata(profile);
|
||||||
console.debug(ev);
|
console.debug(ev);
|
||||||
System.BroadcastEvent(ev);
|
System.BroadcastEvent(ev);
|
||||||
|
|
||||||
setStage(Stage.SaveKey);
|
setStage(Stage.SaveKey);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
setError(e.message);
|
||||||
|
} else {
|
||||||
|
setError(e as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (stage) {
|
switch (stage) {
|
||||||
case Stage.Login: {
|
case Stage.Login: {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h2>
|
<img src={LoginHeader} className="header-image" />
|
||||||
<FormattedMessage defaultMessage="Create an Account" />
|
<div className="content-inner">
|
||||||
</h2>
|
<h2>
|
||||||
<h3>
|
<FormattedMessage defaultMessage="Create an Account" />
|
||||||
<FormattedMessage defaultMessage="No emails, just awesomeness!" />
|
</h2>
|
||||||
</h3>
|
<h3>
|
||||||
<button type="button" className="btn btn-primary btn-block" onClick={createAccount}>
|
<FormattedMessage defaultMessage="No emails, just awesomeness!" />
|
||||||
<FormattedMessage defaultMessage="Create Account" />
|
</h3>
|
||||||
</button>
|
<button type="button" className="btn btn-primary btn-block" onClick={createAccount}>
|
||||||
<div className="or-divider">
|
<FormattedMessage defaultMessage="Create Account" />
|
||||||
<hr />
|
</button>
|
||||||
<FormattedMessage defaultMessage="OR" />
|
|
||||||
<hr />
|
<div className="or-divider">
|
||||||
|
<hr />
|
||||||
|
<FormattedMessage defaultMessage="OR" />
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
{hasNostrExtension && <>
|
||||||
|
<AsyncButton type="button" className="btn btn-primary btn-block" onClick={loginNip7}>
|
||||||
|
<FormattedMessage defaultMessage="Nostr Extension" />
|
||||||
|
</AsyncButton>
|
||||||
|
</>}
|
||||||
|
<button type="button" className="btn btn-primary btn-block" onClick={() => setStage(Stage.LoginInput)}>
|
||||||
|
<FormattedMessage defaultMessage="Login with Private Key (insecure)" />
|
||||||
|
</button>
|
||||||
|
{error && <b className="error">{error}</b>}
|
||||||
</div>
|
</div>
|
||||||
<button type="button" className="btn btn-primary btn-block" onClick={doLoginNsec}>
|
|
||||||
<FormattedMessage defaultMessage="Login with Private Key (insecure)" />
|
|
||||||
</button>
|
|
||||||
{error && <b className="error">{error}</b>}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case Stage.LoginInput: {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<img src={LoginVault} className="header-image" />
|
||||||
|
<div className="content-inner">
|
||||||
|
<h2>
|
||||||
|
<FormattedMessage defaultMessage="Login with private key" />
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="This method is insecure. We recommend using a {nostrlink}" values={{
|
||||||
|
nostrlink: <a href="">
|
||||||
|
<FormattedMessage defaultMessage="nostr signer extension" />
|
||||||
|
</a>
|
||||||
|
}} />
|
||||||
|
</p>
|
||||||
|
<div className="paper">
|
||||||
|
<input type="text" value={key} onChange={e => setNewKey(e.target.value)} placeholder={formatMessage({ defaultMessage: "eg. nsec1xyz" })} />
|
||||||
|
</div>
|
||||||
|
<div className="flex f-space">
|
||||||
|
<div></div>
|
||||||
|
<div className="flex g8">
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={() => {
|
||||||
|
setNewKey("");
|
||||||
|
setStage(Stage.Login)
|
||||||
|
}}>
|
||||||
|
<FormattedMessage defaultMessage="Cancel" />
|
||||||
|
</button>
|
||||||
|
<AsyncButton onClick={doLoginNsec} className="btn btn-primary">
|
||||||
|
<FormattedMessage defaultMessage="Log In" />
|
||||||
|
</AsyncButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{error && <b className="error">{error}</b>}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
case Stage.Details: {
|
case Stage.Details: {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h2>
|
<img src={LoginProfile} className="header-image" />
|
||||||
<FormattedMessage defaultMessage="Setup Profile" />
|
<div className="content-inner">
|
||||||
</h2>
|
<h2>
|
||||||
<div className="flex f-center">
|
<FormattedMessage defaultMessage="Setup Profile" />
|
||||||
<div
|
</h2>
|
||||||
className="avatar-input"
|
<div className="flex f-center">
|
||||||
onClick={uploadAvatar}
|
<div
|
||||||
style={
|
className="avatar-input"
|
||||||
{
|
onClick={uploadAvatar}
|
||||||
"--img": `url(${avatar})`,
|
style={
|
||||||
} as CSSProperties
|
{
|
||||||
}>
|
"--img": `url(${avatar})`,
|
||||||
<Icon name="camera-plus" />
|
} as CSSProperties
|
||||||
|
}>
|
||||||
|
<Icon name="camera-plus" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="username">
|
||||||
<div className="username">
|
<div className="paper">
|
||||||
<div className="paper">
|
<input type="text" placeholder="Username" value={username} onChange={e => setUsername(e.target.value)} />
|
||||||
<input type="text" placeholder="Username" value={username} onChange={e => setUsername(e.target.value)} />
|
</div>
|
||||||
|
<small>
|
||||||
|
<FormattedMessage defaultMessage="You can change this later" />
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<small>
|
<AsyncButton type="button" className="btn btn-primary" onClick={setupProfile}>
|
||||||
<FormattedMessage defaultMessage="You can change this later" />
|
<FormattedMessage defaultMessage="Save" />
|
||||||
</small>
|
</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
<AsyncButton type="button" className="btn btn-primary" onClick={saveProfile}>
|
|
||||||
<FormattedMessage defaultMessage="Save" />
|
|
||||||
</AsyncButton>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case Stage.LnAddress: {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<img src={LoginWallet} className="header-image" />
|
||||||
|
<div className="content-inner">
|
||||||
|
<h2>
|
||||||
|
<FormattedMessage defaultMessage="Get paid by viewers" />
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="We hooked you up with a lightning wallet so you can get paid by viewers right away!" />
|
||||||
|
</p>
|
||||||
|
{providerInfo?.balance && <p>
|
||||||
|
<FormattedMessage defaultMessage="Oh, and you have {n} sats of free streaming on us! 💜" values={{
|
||||||
|
n: <FormattedNumber value={providerInfo.balance} />
|
||||||
|
}} />
|
||||||
|
</p>}
|
||||||
|
<div className="username">
|
||||||
|
<div className="paper">
|
||||||
|
<input type="text" placeholder={formatMessage({ defaultMessage: "eg. name@wallet.com" })} value={lnAddress} onChange={e => setLnAddress(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<small>
|
||||||
|
<FormattedMessage defaultMessage="You can always replace it with your own address later." />
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
{error && <b className="error">{error}</b>}
|
||||||
|
<AsyncButton type="button" className="btn btn-primary" onClick={saveProfile}>
|
||||||
|
<FormattedMessage defaultMessage="Amazing! Continue.." />
|
||||||
|
</AsyncButton>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
case Stage.SaveKey: {
|
case Stage.SaveKey: {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h2>
|
<img src={LoginKey} className="header-image" />
|
||||||
<FormattedMessage defaultMessage="Save Key" />
|
<div className="content-inner">
|
||||||
</h2>
|
<h2>
|
||||||
<p>
|
<FormattedMessage defaultMessage="Save Key" />
|
||||||
<FormattedMessage defaultMessage="Nostr uses private keys, please save yours, if you lose this key you wont be able to login to your account anymore!" />
|
</h2>
|
||||||
</p>
|
<p>
|
||||||
<div className="paper">
|
<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!" />
|
||||||
<Copy text={hexToBech32("nsec", key)} />
|
</p>
|
||||||
|
<div className="paper">
|
||||||
|
<Copy text={hexToBech32("nsec", key)} />
|
||||||
|
</div>
|
||||||
|
<button type="button" className="btn btn-primary" onClick={loginWithKey}>
|
||||||
|
<FormattedMessage defaultMessage="Ok, it's safe" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" className="btn btn-primary" onClick={loginWithKey}>
|
|
||||||
<FormattedMessage defaultMessage="Ok, it's safe" />
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -101,8 +101,10 @@ export function NewStreamDialog(props: NewStreamDialogProps & StreamEditorProps)
|
|||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay className="dialog-overlay" />
|
<Dialog.Overlay className="dialog-overlay" />
|
||||||
<Dialog.Content className="dialog-content">
|
<Dialog.Content className="dialog-content">
|
||||||
<div className="new-stream">
|
<div className="content-inner">
|
||||||
<NewStream {...props} onFinish={() => setOpen(false)} />
|
<div className="new-stream">
|
||||||
|
<NewStream {...props} onFinish={() => setOpen(false)} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Portal>
|
</Dialog.Portal>
|
||||||
|
@ -61,6 +61,10 @@ a {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.f-space {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.pill {
|
.pill {
|
||||||
background: #171717;
|
background: #171717;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
@ -108,7 +112,6 @@ a {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
height: 44px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-block {
|
.btn-block {
|
||||||
@ -232,7 +235,6 @@ div.paper {
|
|||||||
.dialog-content {
|
.dialog-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
background-color: #171717;
|
background-color: #171717;
|
||||||
@ -242,7 +244,7 @@ div.paper {
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
width: 90vw;
|
width: 90vw;
|
||||||
max-width: 430px;
|
max-width: 450px;
|
||||||
max-height: 85vh;
|
max-height: 85vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
@ -254,7 +256,6 @@ div.paper {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
|
||||||
padding: 25px;
|
padding: 25px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
@ -2,12 +2,18 @@
|
|||||||
"+0zv6g": {
|
"+0zv6g": {
|
||||||
"defaultMessage": "Image"
|
"defaultMessage": "Image"
|
||||||
},
|
},
|
||||||
|
"+AcVD+": {
|
||||||
|
"defaultMessage": "No emails, just awesomeness!"
|
||||||
|
},
|
||||||
"+vVZ/G": {
|
"+vVZ/G": {
|
||||||
"defaultMessage": "Connect"
|
"defaultMessage": "Connect"
|
||||||
},
|
},
|
||||||
"/0TOL5": {
|
"/0TOL5": {
|
||||||
"defaultMessage": "Amount"
|
"defaultMessage": "Amount"
|
||||||
},
|
},
|
||||||
|
"/EvlqN": {
|
||||||
|
"defaultMessage": "nostr signer extension"
|
||||||
|
},
|
||||||
"/GCoTA": {
|
"/GCoTA": {
|
||||||
"defaultMessage": "Clear"
|
"defaultMessage": "Clear"
|
||||||
},
|
},
|
||||||
@ -20,6 +26,9 @@
|
|||||||
"1EYCdR": {
|
"1EYCdR": {
|
||||||
"defaultMessage": "Tags"
|
"defaultMessage": "Tags"
|
||||||
},
|
},
|
||||||
|
"1qsXCO": {
|
||||||
|
"defaultMessage": "eg. name@wallet.com"
|
||||||
|
},
|
||||||
"2/2yg+": {
|
"2/2yg+": {
|
||||||
"defaultMessage": "Add"
|
"defaultMessage": "Add"
|
||||||
},
|
},
|
||||||
@ -32,9 +41,15 @@
|
|||||||
"3adEeb": {
|
"3adEeb": {
|
||||||
"defaultMessage": "{n} viewers"
|
"defaultMessage": "{n} viewers"
|
||||||
},
|
},
|
||||||
|
"3df560": {
|
||||||
|
"defaultMessage": "Login with private key"
|
||||||
|
},
|
||||||
"47FYwb": {
|
"47FYwb": {
|
||||||
"defaultMessage": "Cancel"
|
"defaultMessage": "Cancel"
|
||||||
},
|
},
|
||||||
|
"4l69eO": {
|
||||||
|
"defaultMessage": "Hmm, your lightning address looks wrong"
|
||||||
|
},
|
||||||
"4l6vz1": {
|
"4l6vz1": {
|
||||||
"defaultMessage": "Copy"
|
"defaultMessage": "Copy"
|
||||||
},
|
},
|
||||||
@ -83,12 +98,21 @@
|
|||||||
"ESyhzp": {
|
"ESyhzp": {
|
||||||
"defaultMessage": "Your comment for {name}"
|
"defaultMessage": "Your comment for {name}"
|
||||||
},
|
},
|
||||||
|
"FjDlus": {
|
||||||
|
"defaultMessage": "You can always replace it with your own address later."
|
||||||
|
},
|
||||||
|
"Fodi9+": {
|
||||||
|
"defaultMessage": "Get paid by viewers"
|
||||||
|
},
|
||||||
"G/yZLu": {
|
"G/yZLu": {
|
||||||
"defaultMessage": "Remove"
|
"defaultMessage": "Remove"
|
||||||
},
|
},
|
||||||
"Gq6x9o": {
|
"Gq6x9o": {
|
||||||
"defaultMessage": "Cover Image"
|
"defaultMessage": "Cover Image"
|
||||||
},
|
},
|
||||||
|
"H/bNs9": {
|
||||||
|
"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!"
|
||||||
|
},
|
||||||
"H5+NAX": {
|
"H5+NAX": {
|
||||||
"defaultMessage": "Balance"
|
"defaultMessage": "Balance"
|
||||||
},
|
},
|
||||||
@ -101,6 +125,9 @@
|
|||||||
"IJDKz3": {
|
"IJDKz3": {
|
||||||
"defaultMessage": "Zap amount in {currency}"
|
"defaultMessage": "Zap amount in {currency}"
|
||||||
},
|
},
|
||||||
|
"INlWvJ": {
|
||||||
|
"defaultMessage": "OR"
|
||||||
|
},
|
||||||
"JEsxDw": {
|
"JEsxDw": {
|
||||||
"defaultMessage": "Uploading..."
|
"defaultMessage": "Uploading..."
|
||||||
},
|
},
|
||||||
@ -119,9 +146,6 @@
|
|||||||
"KkIL3s": {
|
"KkIL3s": {
|
||||||
"defaultMessage": "No, I am under 18"
|
"defaultMessage": "No, I am under 18"
|
||||||
},
|
},
|
||||||
"Ld5LAE": {
|
|
||||||
"defaultMessage": "Nostr uses private keys, please save yours, if you lose this key you wont be able to login to your account anymore!"
|
|
||||||
},
|
|
||||||
"LknBsU": {
|
"LknBsU": {
|
||||||
"defaultMessage": "Stream Key"
|
"defaultMessage": "Stream Key"
|
||||||
},
|
},
|
||||||
@ -137,6 +161,9 @@
|
|||||||
"OWgHbg": {
|
"OWgHbg": {
|
||||||
"defaultMessage": "Edit card"
|
"defaultMessage": "Edit card"
|
||||||
},
|
},
|
||||||
|
"Oxqtyf": {
|
||||||
|
"defaultMessage": "We hooked you up with a lightning wallet so you can get paid by viewers right away!"
|
||||||
|
},
|
||||||
"Q3au2v": {
|
"Q3au2v": {
|
||||||
"defaultMessage": "About {estimate}"
|
"defaultMessage": "About {estimate}"
|
||||||
},
|
},
|
||||||
@ -182,6 +209,9 @@
|
|||||||
"X2PZ7D": {
|
"X2PZ7D": {
|
||||||
"defaultMessage": "Create Goal"
|
"defaultMessage": "Create Goal"
|
||||||
},
|
},
|
||||||
|
"Z8ZOEY": {
|
||||||
|
"defaultMessage": "This method is insecure. We recommend using a {nostrlink}"
|
||||||
|
},
|
||||||
"ZmqxZs": {
|
"ZmqxZs": {
|
||||||
"defaultMessage": "You can change this later"
|
"defaultMessage": "You can change this later"
|
||||||
},
|
},
|
||||||
@ -203,12 +233,18 @@
|
|||||||
"ebmhes": {
|
"ebmhes": {
|
||||||
"defaultMessage": "Nostr Extension"
|
"defaultMessage": "Nostr Extension"
|
||||||
},
|
},
|
||||||
|
"f6biFA": {
|
||||||
|
"defaultMessage": "Oh, and you have {n} sats of free streaming on us! 💜"
|
||||||
|
},
|
||||||
"fBI91o": {
|
"fBI91o": {
|
||||||
"defaultMessage": "Zap"
|
"defaultMessage": "Zap"
|
||||||
},
|
},
|
||||||
"fc2iho": {
|
"fc2iho": {
|
||||||
"defaultMessage": "Add File"
|
"defaultMessage": "Add File"
|
||||||
},
|
},
|
||||||
|
"feZ/kG": {
|
||||||
|
"defaultMessage": "Login with Private Key (insecure)"
|
||||||
|
},
|
||||||
"hGQqkW": {
|
"hGQqkW": {
|
||||||
"defaultMessage": "Schedule"
|
"defaultMessage": "Schedule"
|
||||||
},
|
},
|
||||||
@ -263,6 +299,9 @@
|
|||||||
"pO/lPX": {
|
"pO/lPX": {
|
||||||
"defaultMessage": "Scheduled for {date}"
|
"defaultMessage": "Scheduled for {date}"
|
||||||
},
|
},
|
||||||
|
"r2Jjms": {
|
||||||
|
"defaultMessage": "Log In"
|
||||||
|
},
|
||||||
"rWBFZA": {
|
"rWBFZA": {
|
||||||
"defaultMessage": "Sexually explicit material ahead!"
|
"defaultMessage": "Sexually explicit material ahead!"
|
||||||
},
|
},
|
||||||
@ -284,12 +323,18 @@
|
|||||||
"tG1ST3": {
|
"tG1ST3": {
|
||||||
"defaultMessage": "Incoming Zap"
|
"defaultMessage": "Incoming Zap"
|
||||||
},
|
},
|
||||||
|
"tM6fNW": {
|
||||||
|
"defaultMessage": "Amazing! Continue.."
|
||||||
|
},
|
||||||
"thsiMl": {
|
"thsiMl": {
|
||||||
"defaultMessage": "terms and conditions"
|
"defaultMessage": "terms and conditions"
|
||||||
},
|
},
|
||||||
"tzMNF3": {
|
"tzMNF3": {
|
||||||
"defaultMessage": "Status"
|
"defaultMessage": "Status"
|
||||||
},
|
},
|
||||||
|
"u6uD94": {
|
||||||
|
"defaultMessage": "Create an Account"
|
||||||
|
},
|
||||||
"uYw2LD": {
|
"uYw2LD": {
|
||||||
"defaultMessage": "Stream"
|
"defaultMessage": "Stream"
|
||||||
},
|
},
|
||||||
@ -314,6 +359,9 @@
|
|||||||
"x82IOl": {
|
"x82IOl": {
|
||||||
"defaultMessage": "Mute"
|
"defaultMessage": "Mute"
|
||||||
},
|
},
|
||||||
|
"yzKwBQ": {
|
||||||
|
"defaultMessage": "eg. nsec1xyz"
|
||||||
|
},
|
||||||
"zVDHAu": {
|
"zVDHAu": {
|
||||||
"defaultMessage": "Zap Alert"
|
"defaultMessage": "Zap Alert"
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 179 KiB |
BIN
src/login-key.png
Normal file
BIN
src/login-key.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 934 KiB |
BIN
src/login-profile.png
Normal file
BIN
src/login-profile.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 MiB |
BIN
src/login-start.png
Normal file
BIN
src/login-start.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 680 KiB |
BIN
src/login-vault.png
Normal file
BIN
src/login-vault.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 520 KiB |
BIN
src/login-wallet.png
Normal file
BIN
src/login-wallet.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 699 KiB |
@ -13,9 +13,6 @@ import { Menu, MenuItem } from "@szhsin/react-menu";
|
|||||||
import { hexToBech32 } from "@snort/shared";
|
import { hexToBech32 } from "@snort/shared";
|
||||||
import { Login } from "index";
|
import { Login } from "index";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { EventPublisher } from "@snort/system";
|
|
||||||
import { LoginType } from "login";
|
|
||||||
import LoginHeader from "../login-header.png";
|
|
||||||
|
|
||||||
export function LayoutPage() {
|
export function LayoutPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -65,16 +62,7 @@ export function LayoutPage() {
|
|||||||
function loggedOut() {
|
function loggedOut() {
|
||||||
if (login) return;
|
if (login) return;
|
||||||
|
|
||||||
async function handleLogin() {
|
function handleLogin() {
|
||||||
try {
|
|
||||||
const pub = await EventPublisher.nip7();
|
|
||||||
if (pub) {
|
|
||||||
Login.loginWithPubkey(pub.pubKey, LoginType.Nip7);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
setShowLogin(true);
|
setShowLogin(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,10 +75,7 @@ export function LayoutPage() {
|
|||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay className="dialog-overlay" />
|
<Dialog.Overlay className="dialog-overlay" />
|
||||||
<Dialog.Content className="dialog-content">
|
<Dialog.Content className="dialog-content">
|
||||||
<img src={LoginHeader} className="header-image" />
|
<LoginSignup close={() => setShowLogin(false)} />
|
||||||
<div className="content-inner">
|
|
||||||
<LoginSignup close={() => setShowLogin(false)} />
|
|
||||||
</div>
|
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Portal>
|
</Dialog.Portal>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
|
@ -15,7 +15,7 @@ export function ConfigureNostrType() {
|
|||||||
|
|
||||||
async function tryConnect() {
|
async function tryConnect() {
|
||||||
try {
|
try {
|
||||||
const api = new Nip103StreamProvider(url);
|
const api = new Nip103StreamProvider(new URL(url).host, url);
|
||||||
const inf = await api.info();
|
const inf = await api.info();
|
||||||
setInfo(inf);
|
setInfo(inf);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -58,7 +58,7 @@ export function ConfigureNostrType() {
|
|||||||
<button
|
<button
|
||||||
className="btn btn-border"
|
className="btn btn-border"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
StreamProviderStore.add(new Nip103StreamProvider(url));
|
StreamProviderStore.add(new Nip103StreamProvider(new URL(url).host, url));
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}}>
|
}}>
|
||||||
<FormattedMessage defaultMessage="Save" />
|
<FormattedMessage defaultMessage="Save" />
|
||||||
|
@ -73,6 +73,8 @@ export interface StreamProviderStreamInfo {
|
|||||||
content_warning: string;
|
content_warning: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DefaultProvider = new Nip103StreamProvider("zap.stream", "https://api.zap.stream/api/nostr/");
|
||||||
|
|
||||||
export class ProviderStore extends ExternalStore<Array<StreamProvider>> {
|
export class ProviderStore extends ExternalStore<Array<StreamProvider>> {
|
||||||
#providers: Array<StreamProvider> = [];
|
#providers: Array<StreamProvider> = [];
|
||||||
|
|
||||||
@ -88,7 +90,7 @@ export class ProviderStore extends ExternalStore<Array<StreamProvider>> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case StreamProviders.NostrType: {
|
case StreamProviders.NostrType: {
|
||||||
this.#providers.push(new Nip103StreamProvider(c.url as string));
|
this.#providers.push(new Nip103StreamProvider(new URL(c.url as string).host, c.url as string));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case StreamProviders.Owncast: {
|
case StreamProviders.Owncast: {
|
||||||
@ -107,8 +109,7 @@ export class ProviderStore extends ExternalStore<Array<StreamProvider>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
takeSnapshot() {
|
takeSnapshot() {
|
||||||
const defaultProvider = new Nip103StreamProvider("https://api.zap.stream/api/nostr/");
|
return [DefaultProvider, new ManualProvider(), ...this.#providers];
|
||||||
return [defaultProvider, new ManualProvider(), ...this.#providers];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#save() {
|
#save() {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { base64 } from "@scure/base";
|
||||||
import {
|
import {
|
||||||
StreamProvider,
|
StreamProvider,
|
||||||
StreamProviderEndpoint,
|
StreamProviderEndpoint,
|
||||||
@ -5,20 +6,16 @@ import {
|
|||||||
StreamProviderStreamInfo,
|
StreamProviderStreamInfo,
|
||||||
StreamProviders,
|
StreamProviders,
|
||||||
} from ".";
|
} from ".";
|
||||||
import { EventKind, NostrEvent } from "@snort/system";
|
import { EventKind, EventPublisher, NostrEvent } from "@snort/system";
|
||||||
import { Login, StreamState } from "index";
|
import { Login, StreamState } from "index";
|
||||||
import { getPublisher } from "login";
|
import { getPublisher } from "login";
|
||||||
import { findTag } from "utils";
|
import { findTag } from "utils";
|
||||||
|
|
||||||
export class Nip103StreamProvider implements StreamProvider {
|
export class Nip103StreamProvider implements StreamProvider {
|
||||||
#url: string;
|
#publisher?: EventPublisher;
|
||||||
|
|
||||||
constructor(url: string) {
|
constructor(readonly name: string, readonly url: string, pub?: EventPublisher) {
|
||||||
this.#url = url;
|
this.#publisher = pub;
|
||||||
}
|
|
||||||
|
|
||||||
get name() {
|
|
||||||
return new URL(this.#url).host;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get type() {
|
get type() {
|
||||||
@ -52,7 +49,7 @@ export class Nip103StreamProvider implements StreamProvider {
|
|||||||
createConfig() {
|
createConfig() {
|
||||||
return {
|
return {
|
||||||
type: StreamProviders.NostrType,
|
type: StreamProviders.NostrType,
|
||||||
url: this.#url,
|
url: this.url,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,11 +80,17 @@ export class Nip103StreamProvider implements StreamProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async #getJson<T>(method: "GET" | "POST" | "PATCH", path: string, body?: unknown): Promise<T> {
|
async #getJson<T>(method: "GET" | "POST" | "PATCH", path: string, body?: unknown): Promise<T> {
|
||||||
const login = Login.snapshot();
|
const pub = (() => {
|
||||||
const pub = login && getPublisher(login);
|
if (this.#publisher) {
|
||||||
|
return this.#publisher;
|
||||||
|
} else {
|
||||||
|
const login = Login.snapshot();
|
||||||
|
return login && getPublisher(login);
|
||||||
|
}
|
||||||
|
})();
|
||||||
if (!pub) throw new Error("No signer");
|
if (!pub) throw new Error("No signer");
|
||||||
|
|
||||||
const u = `${this.#url}${path}`;
|
const u = `${this.url}${path}`;
|
||||||
const token = await pub.generic(eb => {
|
const token = await pub.generic(eb => {
|
||||||
return eb.kind(EventKind.HttpAuthentication).content("").tag(["u", u]).tag(["method", method]);
|
return eb.kind(EventKind.HttpAuthentication).content("").tag(["u", u]).tag(["method", method]);
|
||||||
});
|
});
|
||||||
@ -96,7 +99,7 @@ export class Nip103StreamProvider implements StreamProvider {
|
|||||||
body: body ? JSON.stringify(body) : undefined,
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
authorization: `Nostr ${btoa(JSON.stringify(token))}`,
|
authorization: `Nostr ${base64.encode(new TextEncoder().encode(JSON.stringify(token)))}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const json = await rsp.text();
|
const json = await rsp.text();
|
||||||
|
@ -1,16 +1,21 @@
|
|||||||
{
|
{
|
||||||
"+0zv6g": "Image",
|
"+0zv6g": "Image",
|
||||||
|
"+AcVD+": "No emails, just awesomeness!",
|
||||||
"+vVZ/G": "Connect",
|
"+vVZ/G": "Connect",
|
||||||
"/0TOL5": "Amount",
|
"/0TOL5": "Amount",
|
||||||
|
"/EvlqN": "nostr signer extension",
|
||||||
"/GCoTA": "Clear",
|
"/GCoTA": "Clear",
|
||||||
"04lmFi": "Save Key",
|
"04lmFi": "Save Key",
|
||||||
"0GfNiL": "Stream Zap Goals",
|
"0GfNiL": "Stream Zap Goals",
|
||||||
"1EYCdR": "Tags",
|
"1EYCdR": "Tags",
|
||||||
|
"1qsXCO": "eg. name@wallet.com",
|
||||||
"2/2yg+": "Add",
|
"2/2yg+": "Add",
|
||||||
"2CGh/0": "live",
|
"2CGh/0": "live",
|
||||||
"3HwrQo": "Zap!",
|
"3HwrQo": "Zap!",
|
||||||
"3adEeb": "{n} viewers",
|
"3adEeb": "{n} viewers",
|
||||||
|
"3df560": "Login with private key",
|
||||||
"47FYwb": "Cancel",
|
"47FYwb": "Cancel",
|
||||||
|
"4l69eO": "Hmm, your lightning address looks wrong",
|
||||||
"4l6vz1": "Copy",
|
"4l6vz1": "Copy",
|
||||||
"4uI538": "Resolutions",
|
"4uI538": "Resolutions",
|
||||||
"5JcXdV": "Create Account",
|
"5JcXdV": "Create Account",
|
||||||
@ -27,24 +32,28 @@
|
|||||||
"C81/uG": "Logout",
|
"C81/uG": "Logout",
|
||||||
"Dn82AL": "Live",
|
"Dn82AL": "Live",
|
||||||
"ESyhzp": "Your comment for {name}",
|
"ESyhzp": "Your comment for {name}",
|
||||||
|
"FjDlus": "You can always replace it with your own address later.",
|
||||||
|
"Fodi9+": "Get paid by viewers",
|
||||||
"G/yZLu": "Remove",
|
"G/yZLu": "Remove",
|
||||||
"Gq6x9o": "Cover Image",
|
"Gq6x9o": "Cover Image",
|
||||||
|
"H/bNs9": "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!",
|
||||||
"H5+NAX": "Balance",
|
"H5+NAX": "Balance",
|
||||||
"HAlOn1": "Name",
|
"HAlOn1": "Name",
|
||||||
"I1kjHI": "Supports {markdown}",
|
"I1kjHI": "Supports {markdown}",
|
||||||
"IJDKz3": "Zap amount in {currency}",
|
"IJDKz3": "Zap amount in {currency}",
|
||||||
|
"INlWvJ": "OR",
|
||||||
"JEsxDw": "Uploading...",
|
"JEsxDw": "Uploading...",
|
||||||
"Jq3FDz": "Content",
|
"Jq3FDz": "Content",
|
||||||
"K3r6DQ": "Delete",
|
"K3r6DQ": "Delete",
|
||||||
"K3uH1C": "offline",
|
"K3uH1C": "offline",
|
||||||
"K7AkdL": "Show",
|
"K7AkdL": "Show",
|
||||||
"KkIL3s": "No, I am under 18",
|
"KkIL3s": "No, I am under 18",
|
||||||
"Ld5LAE": "Nostr uses private keys, please save yours, if you lose this key you wont be able to login to your account anymore!",
|
|
||||||
"LknBsU": "Stream Key",
|
"LknBsU": "Stream Key",
|
||||||
"My6HwN": "Ok, it's safe",
|
"My6HwN": "Ok, it's safe",
|
||||||
"O2Cy6m": "Yes, I am over 18",
|
"O2Cy6m": "Yes, I am over 18",
|
||||||
"OKhRC6": "Share",
|
"OKhRC6": "Share",
|
||||||
"OWgHbg": "Edit card",
|
"OWgHbg": "Edit card",
|
||||||
|
"Oxqtyf": "We hooked you up with a lightning wallet so you can get paid by viewers right away!",
|
||||||
"Q3au2v": "About {estimate}",
|
"Q3au2v": "About {estimate}",
|
||||||
"QRHNuF": "What are we steaming today?",
|
"QRHNuF": "What are we steaming today?",
|
||||||
"QRRCp0": "Stream URL",
|
"QRRCp0": "Stream URL",
|
||||||
@ -60,6 +69,7 @@
|
|||||||
"VA/Z1S": "Hide",
|
"VA/Z1S": "Hide",
|
||||||
"W9355R": "Unmute",
|
"W9355R": "Unmute",
|
||||||
"X2PZ7D": "Create Goal",
|
"X2PZ7D": "Create Goal",
|
||||||
|
"Z8ZOEY": "This method is insecure. We recommend using a {nostrlink}",
|
||||||
"ZmqxZs": "You can change this later",
|
"ZmqxZs": "You can change this later",
|
||||||
"acrOoz": "Continue",
|
"acrOoz": "Continue",
|
||||||
"cPIKU2": "Following",
|
"cPIKU2": "Following",
|
||||||
@ -67,8 +77,10 @@
|
|||||||
"cyR7Kh": "Back",
|
"cyR7Kh": "Back",
|
||||||
"dVD/AR": "Top Zappers",
|
"dVD/AR": "Top Zappers",
|
||||||
"ebmhes": "Nostr Extension",
|
"ebmhes": "Nostr Extension",
|
||||||
|
"f6biFA": "Oh, and you have {n} sats of free streaming on us! 💜",
|
||||||
"fBI91o": "Zap",
|
"fBI91o": "Zap",
|
||||||
"fc2iho": "Add File",
|
"fc2iho": "Add File",
|
||||||
|
"feZ/kG": "Login with Private Key (insecure)",
|
||||||
"hGQqkW": "Schedule",
|
"hGQqkW": "Schedule",
|
||||||
"hpl4BP": "Chat Widget",
|
"hpl4BP": "Chat Widget",
|
||||||
"ieGrWo": "Follow",
|
"ieGrWo": "Follow",
|
||||||
@ -87,6 +99,7 @@
|
|||||||
"oHPB8Q": "Zap {name}",
|
"oHPB8Q": "Zap {name}",
|
||||||
"oZrFyI": "Stream type should be HLS",
|
"oZrFyI": "Stream type should be HLS",
|
||||||
"pO/lPX": "Scheduled for {date}",
|
"pO/lPX": "Scheduled for {date}",
|
||||||
|
"r2Jjms": "Log In",
|
||||||
"rWBFZA": "Sexually explicit material ahead!",
|
"rWBFZA": "Sexually explicit material ahead!",
|
||||||
"rbrahO": "Close",
|
"rbrahO": "Close",
|
||||||
"rfC1Zq": "Save card",
|
"rfC1Zq": "Save card",
|
||||||
@ -94,8 +107,10 @@
|
|||||||
"s5ksS7": "Image Link",
|
"s5ksS7": "Image Link",
|
||||||
"s7V+5p": "Confirm your age",
|
"s7V+5p": "Confirm your age",
|
||||||
"tG1ST3": "Incoming Zap",
|
"tG1ST3": "Incoming Zap",
|
||||||
|
"tM6fNW": "Amazing! Continue..",
|
||||||
"thsiMl": "terms and conditions",
|
"thsiMl": "terms and conditions",
|
||||||
"tzMNF3": "Status",
|
"tzMNF3": "Status",
|
||||||
|
"u6uD94": "Create an Account",
|
||||||
"uYw2LD": "Stream",
|
"uYw2LD": "Stream",
|
||||||
"vrTOHJ": "{amount} sats",
|
"vrTOHJ": "{amount} sats",
|
||||||
"w0Xm2F": "Start typing",
|
"w0Xm2F": "Start typing",
|
||||||
@ -104,5 +119,6 @@
|
|||||||
"wOy57k": "Add stream goal",
|
"wOy57k": "Add stream goal",
|
||||||
"wzWWzV": "Top zappers",
|
"wzWWzV": "Top zappers",
|
||||||
"x82IOl": "Mute",
|
"x82IOl": "Mute",
|
||||||
|
"yzKwBQ": "eg. nsec1xyz",
|
||||||
"zVDHAu": "Zap Alert"
|
"zVDHAu": "Zap Alert"
|
||||||
}
|
}
|
Reference in New Issue
Block a user