New login flow

This commit is contained in:
2023-09-01 12:39:04 +01:00
parent e5a67a24c5
commit 857226e2a5
15 changed files with 310 additions and 122 deletions

View File

@ -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,12 +101,30 @@ 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() {
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 pub = EventPublisher.privateKey(key);
const profile = { const profile = {
name: username, name: username,
picture: avatar, picture: avatar,
lud16: `${pub.pubKey}@zap.stream`, lud16: lnAddress,
} as UserMetadata; } as UserMetadata;
const ev = await pub.metadata(profile); const ev = await pub.metadata(profile);
@ -91,12 +132,21 @@ export function LoginSignup({ close }: { close: () => void }) {
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 (
<> <>
<img src={LoginHeader} className="header-image" />
<div className="content-inner">
<h2> <h2>
<FormattedMessage defaultMessage="Create an Account" /> <FormattedMessage defaultMessage="Create an Account" />
</h2> </h2>
@ -106,21 +156,67 @@ export function LoginSignup({ close }: { close: () => void }) {
<button type="button" className="btn btn-primary btn-block" onClick={createAccount}> <button type="button" className="btn btn-primary btn-block" onClick={createAccount}>
<FormattedMessage defaultMessage="Create Account" /> <FormattedMessage defaultMessage="Create Account" />
</button> </button>
<div className="or-divider"> <div className="or-divider">
<hr /> <hr />
<FormattedMessage defaultMessage="OR" /> <FormattedMessage defaultMessage="OR" />
<hr /> <hr />
</div> </div>
<button type="button" className="btn btn-primary btn-block" onClick={doLoginNsec}> {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)" /> <FormattedMessage defaultMessage="Login with Private Key (insecure)" />
</button> </button>
{error && <b className="error">{error}</b>} {error && <b className="error">{error}</b>}
</div>
</> </>
); );
} }
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 (
<> <>
<img src={LoginProfile} className="header-image" />
<div className="content-inner">
<h2> <h2>
<FormattedMessage defaultMessage="Setup Profile" /> <FormattedMessage defaultMessage="Setup Profile" />
</h2> </h2>
@ -144,20 +240,55 @@ export function LoginSignup({ close }: { close: () => void }) {
<FormattedMessage defaultMessage="You can change this later" /> <FormattedMessage defaultMessage="You can change this later" />
</small> </small>
</div> </div>
<AsyncButton type="button" className="btn btn-primary" onClick={saveProfile}> <AsyncButton type="button" className="btn btn-primary" onClick={setupProfile}>
<FormattedMessage defaultMessage="Save" /> <FormattedMessage defaultMessage="Save" />
</AsyncButton> </AsyncButton>
</div>
</> </>
); );
} }
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 (
<> <>
<img src={LoginKey} className="header-image" />
<div className="content-inner">
<h2> <h2>
<FormattedMessage defaultMessage="Save Key" /> <FormattedMessage defaultMessage="Save Key" />
</h2> </h2>
<p> <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!" /> <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> </p>
<div className="paper"> <div className="paper">
<Copy text={hexToBech32("nsec", key)} /> <Copy text={hexToBech32("nsec", key)} />
@ -165,6 +296,7 @@ export function LoginSignup({ close }: { close: () => void }) {
<button type="button" className="btn btn-primary" onClick={loginWithKey}> <button type="button" className="btn btn-primary" onClick={loginWithKey}>
<FormattedMessage defaultMessage="Ok, it's safe" /> <FormattedMessage defaultMessage="Ok, it's safe" />
</button> </button>
</div>
</> </>
); );
} }

View File

@ -101,9 +101,11 @@ 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="content-inner">
<div className="new-stream"> <div className="new-stream">
<NewStream {...props} onFinish={() => setOpen(false)} /> <NewStream {...props} onFinish={() => setOpen(false)} />
</div> </div>
</div>
</Dialog.Content> </Dialog.Content>
</Dialog.Portal> </Dialog.Portal>
</Dialog.Root> </Dialog.Root>

View File

@ -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;

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 KiB

BIN
src/login-vault.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 KiB

BIN
src/login-wallet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 KiB

View File

@ -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" />
<div className="content-inner">
<LoginSignup close={() => setShowLogin(false)} /> <LoginSignup close={() => setShowLogin(false)} />
</div>
</Dialog.Content> </Dialog.Content>
</Dialog.Portal> </Dialog.Portal>
</Dialog.Root> </Dialog.Root>

View File

@ -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" />

View File

@ -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() {

View File

@ -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 pub = (() => {
if (this.#publisher) {
return this.#publisher;
} else {
const login = Login.snapshot(); const login = Login.snapshot();
const pub = login && getPublisher(login); 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();

View File

@ -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"
} }