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