From ecb0f0e78a07b9372f4ffc31086373e5c11977fd Mon Sep 17 00:00:00 2001 From: Martti Malmi Date: Wed, 27 Sep 2023 11:41:26 +0300 Subject: [PATCH 1/7] iris account registration --- packages/app/src/Const.ts | 5 + .../src/Element/IrisAccount/AccountName.tsx | 31 ++ .../src/Element/IrisAccount/ActiveAccount.tsx | 73 +++++ .../src/Element/IrisAccount/IrisAccount.tsx | 304 ++++++++++++++++++ .../Element/IrisAccount/ReservedAccount.tsx | 22 ++ .../app/src/Pages/FreeNostrAddressPage.tsx | 38 +++ packages/app/src/Pages/settings/Profile.tsx | 2 +- packages/app/src/index.tsx | 5 + 8 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 packages/app/src/Element/IrisAccount/AccountName.tsx create mode 100644 packages/app/src/Element/IrisAccount/ActiveAccount.tsx create mode 100644 packages/app/src/Element/IrisAccount/IrisAccount.tsx create mode 100644 packages/app/src/Element/IrisAccount/ReservedAccount.tsx create mode 100644 packages/app/src/Pages/FreeNostrAddressPage.tsx diff --git a/packages/app/src/Const.ts b/packages/app/src/Const.ts index 2eefd906..0af7a091 100644 --- a/packages/app/src/Const.ts +++ b/packages/app/src/Const.ts @@ -15,6 +15,11 @@ export const Day = Hour * 24; */ export const ApiHost = "https://api.snort.social"; +/** + * Iris api for free nip05 names + */ +export const IrisHost = "https://api.iris.to"; + /** * LibreTranslate endpoint */ diff --git a/packages/app/src/Element/IrisAccount/AccountName.tsx b/packages/app/src/Element/IrisAccount/AccountName.tsx new file mode 100644 index 00000000..77d494ac --- /dev/null +++ b/packages/app/src/Element/IrisAccount/AccountName.tsx @@ -0,0 +1,31 @@ +import {useNavigate} from "react-router-dom"; + +export default function AccountName({ name = '', link = true }) { + const navigate = useNavigate(); + return ( + <> +
+ Username: {name} +
+
+ Short link:{' '} + {link ? ( + { + e.preventDefault(); + navigate(`/${name}`); + }} + > + iris.to/{name} + + ) : ( + <>iris.to/{name} + )} +
+
+ Nostr address (nip05): {name}@iris.to +
+ + ); +} diff --git a/packages/app/src/Element/IrisAccount/ActiveAccount.tsx b/packages/app/src/Element/IrisAccount/ActiveAccount.tsx new file mode 100644 index 00000000..f237f86b --- /dev/null +++ b/packages/app/src/Element/IrisAccount/ActiveAccount.tsx @@ -0,0 +1,73 @@ +import AccountName from './AccountName'; +import useLogin from "../../Hooks/useLogin"; +import {useUserProfile} from "@snort/system-react"; +import {System} from "../../index"; +import {UserCache} from "../../Cache"; +import useEventPublisher from "../../Hooks/useEventPublisher"; +import {mapEventToProfile} from "@snort/system"; + +export default function ActiveAccount({ name = '', setAsPrimary = () => {} }) { + const { publicKey, readonly } = useLogin(s => ({ + publicKey: s.publicKey, + readonly: s.readonly, + })); + const profile = useUserProfile(publicKey); + const publisher = useEventPublisher(); + + async function saveProfile(nip05: string) { + if (readonly) { + return; + } + // copy user object and delete internal fields + const userCopy = { + ...profile, + nip05, + } as Record; + delete userCopy["loaded"]; + delete userCopy["created"]; + delete userCopy["pubkey"]; + delete userCopy["npub"]; + delete userCopy["deleted"]; + delete userCopy["zapService"]; + delete userCopy["isNostrAddressValid"]; + console.debug(userCopy); + + if (publisher) { + const ev = await publisher.metadata(userCopy); + System.BroadcastEvent(ev); + + const newProfile = mapEventToProfile(ev); + if (newProfile) { + await UserCache.update(newProfile); + } + } + } + + const onClick = () => { + const newNip = name + '@iris.to'; + const timeout = setTimeout(() => { + saveProfile(newNip); + }, 2000); + if (profile) { + clearTimeout(timeout); + if (profile.nip05 !== newNip) { + saveProfile(newNip); + setAsPrimary(); + } + } + }; + + return ( +
+
+ You have an active iris.to account: + +
+

+ +

+
+ ); +} diff --git a/packages/app/src/Element/IrisAccount/IrisAccount.tsx b/packages/app/src/Element/IrisAccount/IrisAccount.tsx new file mode 100644 index 00000000..1ea18a2f --- /dev/null +++ b/packages/app/src/Element/IrisAccount/IrisAccount.tsx @@ -0,0 +1,304 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Component, FormEvent} from 'react'; +import { LoginStore } from "Login"; + +import AccountName from './AccountName'; +import ActiveAccount from './ActiveAccount'; +import ReservedAccount from './ReservedAccount'; +//import {ProfileLoader} from "../../index"; + +declare global { + interface Window { + cf_turnstile_callback: any; + } +} + +// TODO split into smaller components +export default class IrisAccount extends Component { + state = { + irisToActive: false, + existing: null as any, + profile: null as any, + newUserName: '', + newUserNameValid: false, + error: null as any, + showChallenge: false, + invalidUsernameMessage: null as any, + }; + + render() { + let view: any; + + if (this.state.irisToActive) { + const username = this.state.profile.nip05.split('@')[0]; + view = ; + } else if (this.state.existing && this.state.existing.confirmed) { + view = ( + this.setState({irisToActive: true})} + /> + ); + } else if (this.state.existing) { + view = ( + this.enableReserved()} + declineReserved={() => this.declineReserved()} + /> + ); + } else if (this.state.error) { + view =
Error: {this.state.error}
; + } else if (this.state.showChallenge) { + window.cf_turnstile_callback = (token: any) => this.register(token); + view = ( + <> +
+ + ); + } else { + view = ( +
+

Register an Iris username (iris.to/username)

+
this.showChallenge(e)}> +
+ this.onNewUserNameChange(e)} + /> + +
+
+ {this.state.newUserNameValid ? ( + <> + Username is available + + + ) : ( + {this.state.invalidUsernameMessage} + )} +
+
+
+ ); + } + + return ( + <> +

Iris.to account

+ {view} +

+ FAQ +

+ + ); + } + + async onNewUserNameChange(e: any) { + const newUserName = e.target.value; + if (newUserName.length === 0) { + this.setState({ + newUserName, + newUserNameValid: false, + invalidUsernameMessage: '', + }); + return; + } + if (newUserName.length < 8 || newUserName.length > 15) { + this.setState({ + newUserName, + newUserNameValid: false, + invalidUsernameMessage: 'Username must be between 8 and 15 characters', + }); + return; + } + if (!newUserName.match(/^[a-z0-9_.]+$/)) { + this.setState({ + newUserName, + newUserNameValid: false, + invalidUsernameMessage: 'Username must only contain lowercase letters and numbers', + }); + return; + } + this.setState({ + newUserName, + invalidUsernameMessage: '', + }); + this.checkAvailabilityFromAPI(newUserName); + } + + checkAvailabilityFromAPI = async (name: string) => { + const res = await fetch(`https://api.iris.to/user/available?name=${encodeURIComponent(name)}`); + if (name !== this.state.newUserName) { + return; + } + if (res.status < 500) { + const json = await res.json(); + if (json.available) { + this.setState({newUserNameValid: true}); + } else { + this.setState({ + newUserNameValid: false, + invalidUsernameMessage: json.message, + }); + } + } else { + this.setState({ + newUserNameValid: false, + invalidUsernameMessage: 'Error checking username availability', + }); + } + } + + showChallenge(e: FormEvent) { + e.preventDefault(); + if (!this.state.newUserNameValid) { + return; + } + this.setState({ showChallenge: true }, () => { + // Dynamically injecting Cloudflare script + if (!document.querySelector('script[src="https://challenges.cloudflare.com/turnstile/v0/api.js"]')) { + const script = document.createElement("script"); + script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js"; + script.async = true; + script.defer = true; + document.body.appendChild(script); + } + }); + } + + async register(cfToken: any) { + console.log('register', cfToken); + const login = LoginStore.snapshot(); + const publisher = LoginStore.getPublisher(login.id) + const event = await publisher?.note(`iris.to/${this.state.newUserName}`); + // post signed event as request body to https://api.iris.to/user/confirm_user + const res = await fetch('https://api.iris.to/user/signup', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ event, cfToken }), + }); + if (res.status === 200) { + this.setState({ + error: null, + existing: { + confirmed: true, + name: this.state.newUserName, + }, + }); + delete window.cf_turnstile_callback; + } else { + res + .json() + .then((json) => { + this.setState({ error: json.message || 'error' }); + }) + .catch(() => { + this.setState({ error: 'error' }); + }); + } + } + + async enableReserved() { + const login = LoginStore.snapshot(); + const publisher = LoginStore.getPublisher(login.id) + const event = await publisher?.note(`iris.to/${this.state.newUserName}`); + // post signed event as request body to https://api.iris.to/user/confirm_user + const res = await fetch('https://api.iris.to/user/confirm_user', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(event), + }); + if (res.status === 200) { + this.setState({ + error: null, + existing: { confirmed: true, name: this.state.existing.name }, + }); + } else { + res + .json() + .then((json) => { + this.setState({ error: json.message || 'error' }); + }) + .catch(() => { + this.setState({ error: 'error' }); + }); + } + } + + async declineReserved() { + if (!confirm(`Are you sure you want to decline iris.to/${name}?`)) { + return; + } + const login = LoginStore.snapshot(); + const publisher = LoginStore.getPublisher(login.id) + const event = await publisher?.note(`decline iris.to/${this.state.newUserName}`); + // post signed event as request body to https://api.iris.to/user/confirm_user + const res = await fetch('https://api.iris.to/user/decline_user', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(event), + }); + if (res.status === 200) { + this.setState({ confirmSuccess: false, error: null, existing: null }); + } else { + res + .json() + .then((json) => { + this.setState({ error: json.message || 'error' }); + }) + .catch(() => { + this.setState({ error: 'error' }); + }); + } + } + + componentDidMount() { + const session = LoginStore.snapshot(); + const myPub = session.publicKey; + /* + ProfileLoader.Cache.hook(h, myPub); + SocialNetwork.getProfile( + myPub, + (profile) => { + const irisToActive = + profile && profile.nip05 && profile.nip05valid && profile.nip05.endsWith('@iris.to'); + this.setState({ profile, irisToActive }); + if (profile && !irisToActive) { + this.checkExistingAccount(myPub); + } + }, + true, + ); + + */ + this.checkExistingAccount(myPub); + } + + async checkExistingAccount(pub: any) { + const res = await fetch(`https://api.iris.to/user/find?public_key=${pub}`); + if (res.status === 200) { + const json = await res.json(); + this.setState({ existing: json }); + } + } +} diff --git a/packages/app/src/Element/IrisAccount/ReservedAccount.tsx b/packages/app/src/Element/IrisAccount/ReservedAccount.tsx new file mode 100644 index 00000000..68f23bd0 --- /dev/null +++ b/packages/app/src/Element/IrisAccount/ReservedAccount.tsx @@ -0,0 +1,22 @@ +import AccountName from './AccountName'; + +export default function ReservedAccount({ name = '', enableReserved = () => {}, declineReserved = () => {} }) { + return ( +
+

+ Username iris.to/{name} is reserved for you! +

+ +

+ +

+

+ +

+
+ ); +} diff --git a/packages/app/src/Pages/FreeNostrAddressPage.tsx b/packages/app/src/Pages/FreeNostrAddressPage.tsx new file mode 100644 index 00000000..a3d5ca24 --- /dev/null +++ b/packages/app/src/Pages/FreeNostrAddressPage.tsx @@ -0,0 +1,38 @@ +import FormattedMessage from "@snort/app/src/Element/FormattedMessage"; + +/* +import { IrisHost } from "Const"; +import Nip5Service from "Element/Nip5Service"; + */ + +import messages from "./messages"; +import IrisAccount from "../Element/IrisAccount/IrisAccount"; + +export default function FreeNostrAddressPage() { + return ( +
+

+ +

+

+ +

+

+ +

+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+ + +
+ ); +} diff --git a/packages/app/src/Pages/settings/Profile.tsx b/packages/app/src/Pages/settings/Profile.tsx index 8a8a5a70..15f66c90 100644 --- a/packages/app/src/Pages/settings/Profile.tsx +++ b/packages/app/src/Pages/settings/Profile.tsx @@ -162,7 +162,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) { - diff --git a/packages/app/src/index.tsx b/packages/app/src/index.tsx index 316cf278..35b5d009 100644 --- a/packages/app/src/index.tsx +++ b/packages/app/src/index.tsx @@ -45,6 +45,7 @@ import { db } from "Db"; import { preload, RelayMetrics, UserCache, UserRelays } from "Cache"; import { LoginStore } from "Login"; import { SnortDeckLayout } from "Pages/DeckLayout"; +import FreeNostrAddressPage from "./Pages/FreeNostrAddressPage"; const WasmQueryOptimizer = { expandFilter: (f: ReqFilter) => { @@ -163,6 +164,10 @@ export const router = createBrowserRouter([ element: , children: SettingsRoutes, }, + { + path: "/free-nostr-address", + element: , + }, { path: "/nostr-address", element: , From f33961232bd7d5a1fd572849db28672a0f7cf54b Mon Sep 17 00:00:00 2001 From: Martti Malmi Date: Sat, 30 Sep 2023 13:21:47 +0300 Subject: [PATCH 2/7] add challenges.cloudflare.com to content security policy --- packages/app/_headers | 2 +- .../src/Element/IrisAccount/AccountName.tsx | 11 +- .../src/Element/IrisAccount/ActiveAccount.tsx | 18 +-- .../src/Element/IrisAccount/IrisAccount.tsx | 121 ++++++++---------- .../Element/IrisAccount/ReservedAccount.tsx | 4 +- packages/app/src/lang.json | 3 + packages/app/src/translations/en.json | 1 + 7 files changed, 75 insertions(+), 85 deletions(-) diff --git a/packages/app/_headers b/packages/app/_headers index 5921f994..46544d63 100644 --- a/packages/app/_headers +++ b/packages/app/_headers @@ -1,2 +1,2 @@ /* - Content-Security-Policy: default-src 'self'; manifest-src *; child-src 'none'; worker-src 'self'; frame-src youtube.com www.youtube.com https://platform.twitter.com https://embed.tidal.com https://w.soundcloud.com https://www.mixcloud.com https://open.spotify.com https://player.twitch.tv https://embed.music.apple.com https://nostrnests.com https://embed.wavlake.com; style-src 'self' 'unsafe-inline'; connect-src *; img-src * data: blob:; font-src 'self'; media-src * blob:; script-src 'self' 'wasm-unsafe-eval' https://analytics.v0l.io https://platform.twitter.com https://embed.tidal.com; \ No newline at end of file + Content-Security-Policy: default-src 'self'; manifest-src *; child-src 'none'; worker-src 'self'; frame-src youtube.com www.youtube.com https://platform.twitter.com https://embed.tidal.com https://w.soundcloud.com https://www.mixcloud.com https://open.spotify.com https://player.twitch.tv https://embed.music.apple.com https://nostrnests.com https://embed.wavlake.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; connect-src *; img-src * data: blob:; font-src 'self'; media-src * blob:; script-src 'self' 'wasm-unsafe-eval' https://analytics.v0l.io https://platform.twitter.com https://embed.tidal.com https://challenges.cloudflare.com; \ No newline at end of file diff --git a/packages/app/src/Element/IrisAccount/AccountName.tsx b/packages/app/src/Element/IrisAccount/AccountName.tsx index 77d494ac..82c98f25 100644 --- a/packages/app/src/Element/IrisAccount/AccountName.tsx +++ b/packages/app/src/Element/IrisAccount/AccountName.tsx @@ -1,6 +1,6 @@ -import {useNavigate} from "react-router-dom"; +import { useNavigate } from "react-router-dom"; -export default function AccountName({ name = '', link = true }) { +export default function AccountName({ name = "", link = true }) { const navigate = useNavigate(); return ( <> @@ -8,15 +8,14 @@ export default function AccountName({ name = '', link = true }) { Username: {name}
- Short link:{' '} + Short link:{" "} {link ? ( { + onClick={e => { e.preventDefault(); navigate(`/${name}`); - }} - > + }}> iris.to/{name} ) : ( diff --git a/packages/app/src/Element/IrisAccount/ActiveAccount.tsx b/packages/app/src/Element/IrisAccount/ActiveAccount.tsx index f237f86b..6bdaea72 100644 --- a/packages/app/src/Element/IrisAccount/ActiveAccount.tsx +++ b/packages/app/src/Element/IrisAccount/ActiveAccount.tsx @@ -1,12 +1,12 @@ -import AccountName from './AccountName'; +import AccountName from "./AccountName"; import useLogin from "../../Hooks/useLogin"; -import {useUserProfile} from "@snort/system-react"; -import {System} from "../../index"; -import {UserCache} from "../../Cache"; +import { useUserProfile } from "@snort/system-react"; +import { System } from "../../index"; +import { UserCache } from "../../Cache"; import useEventPublisher from "../../Hooks/useEventPublisher"; -import {mapEventToProfile} from "@snort/system"; +import { mapEventToProfile } from "@snort/system"; -export default function ActiveAccount({ name = '', setAsPrimary = () => {} }) { +export default function ActiveAccount({ name = "", setAsPrimary = () => {} }) { const { publicKey, readonly } = useLogin(s => ({ publicKey: s.publicKey, readonly: s.readonly, @@ -20,7 +20,7 @@ export default function ActiveAccount({ name = '', setAsPrimary = () => {} }) { } // copy user object and delete internal fields const userCopy = { - ...profile, + ...(profile || {}), nip05, } as Record; delete userCopy["loaded"]; @@ -44,7 +44,7 @@ export default function ActiveAccount({ name = '', setAsPrimary = () => {} }) { } const onClick = () => { - const newNip = name + '@iris.to'; + const newNip = name + "@iris.to"; const timeout = setTimeout(() => { saveProfile(newNip); }, 2000); @@ -64,7 +64,7 @@ export default function ActiveAccount({ name = '', setAsPrimary = () => {} }) {

-

diff --git a/packages/app/src/Element/IrisAccount/IrisAccount.tsx b/packages/app/src/Element/IrisAccount/IrisAccount.tsx index 1ea18a2f..1e658b86 100644 --- a/packages/app/src/Element/IrisAccount/IrisAccount.tsx +++ b/packages/app/src/Element/IrisAccount/IrisAccount.tsx @@ -1,10 +1,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Component, FormEvent} from 'react'; +import { Component, FormEvent } from "react"; import { LoginStore } from "Login"; -import AccountName from './AccountName'; -import ActiveAccount from './ActiveAccount'; -import ReservedAccount from './ReservedAccount'; +import AccountName from "./AccountName"; +import ActiveAccount from "./ActiveAccount"; +import ReservedAccount from "./ReservedAccount"; +import { ProfileLoader } from "../../index"; //import {ProfileLoader} from "../../index"; declare global { @@ -19,7 +20,7 @@ export default class IrisAccount extends Component { irisToActive: false, existing: null as any, profile: null as any, - newUserName: '', + newUserName: "", newUserNameValid: false, error: null as any, showChallenge: false, @@ -30,14 +31,11 @@ export default class IrisAccount extends Component { let view: any; if (this.state.irisToActive) { - const username = this.state.profile.nip05.split('@')[0]; - view = ; + const username = this.state.profile?.nip05.split("@")[0]; + view = ; } else if (this.state.existing && this.state.existing.confirmed) { view = ( - this.setState({irisToActive: true})} - /> + this.setState({ irisToActive: true })} /> ); } else if (this.state.existing) { view = ( @@ -56,36 +54,33 @@ export default class IrisAccount extends Component {
+ data-callback="cf_turnstile_callback"> ); } else { view = (

Register an Iris username (iris.to/username)

-
this.showChallenge(e)}> + this.showChallenge(e)}>
this.onNewUserNameChange(e)} + onInput={e => this.onNewUserNameChange(e)} /> - +
{this.state.newUserNameValid ? ( <> Username is available - + ) : ( {this.state.invalidUsernameMessage} @@ -113,7 +108,7 @@ export default class IrisAccount extends Component { this.setState({ newUserName, newUserNameValid: false, - invalidUsernameMessage: '', + invalidUsernameMessage: "", }); return; } @@ -121,7 +116,7 @@ export default class IrisAccount extends Component { this.setState({ newUserName, newUserNameValid: false, - invalidUsernameMessage: 'Username must be between 8 and 15 characters', + invalidUsernameMessage: "Username must be between 8 and 15 characters", }); return; } @@ -129,13 +124,13 @@ export default class IrisAccount extends Component { this.setState({ newUserName, newUserNameValid: false, - invalidUsernameMessage: 'Username must only contain lowercase letters and numbers', + invalidUsernameMessage: "Username must only contain lowercase letters and numbers", }); return; } this.setState({ newUserName, - invalidUsernameMessage: '', + invalidUsernameMessage: "", }); this.checkAvailabilityFromAPI(newUserName); } @@ -148,7 +143,7 @@ export default class IrisAccount extends Component { if (res.status < 500) { const json = await res.json(); if (json.available) { - this.setState({newUserNameValid: true}); + this.setState({ newUserNameValid: true }); } else { this.setState({ newUserNameValid: false, @@ -158,10 +153,10 @@ export default class IrisAccount extends Component { } else { this.setState({ newUserNameValid: false, - invalidUsernameMessage: 'Error checking username availability', + invalidUsernameMessage: "Error checking username availability", }); } - } + }; showChallenge(e: FormEvent) { e.preventDefault(); @@ -181,15 +176,15 @@ export default class IrisAccount extends Component { } async register(cfToken: any) { - console.log('register', cfToken); + console.log("register", cfToken); const login = LoginStore.snapshot(); - const publisher = LoginStore.getPublisher(login.id) + const publisher = LoginStore.getPublisher(login.id); const event = await publisher?.note(`iris.to/${this.state.newUserName}`); // post signed event as request body to https://api.iris.to/user/confirm_user - const res = await fetch('https://api.iris.to/user/signup', { - method: 'POST', + const res = await fetch("https://api.iris.to/user/signup", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ event, cfToken }), }); @@ -205,24 +200,24 @@ export default class IrisAccount extends Component { } else { res .json() - .then((json) => { - this.setState({ error: json.message || 'error' }); + .then(json => { + this.setState({ error: json.message || "error" }); }) .catch(() => { - this.setState({ error: 'error' }); + this.setState({ error: "error" }); }); } } async enableReserved() { const login = LoginStore.snapshot(); - const publisher = LoginStore.getPublisher(login.id) + const publisher = LoginStore.getPublisher(login.id); const event = await publisher?.note(`iris.to/${this.state.newUserName}`); // post signed event as request body to https://api.iris.to/user/confirm_user - const res = await fetch('https://api.iris.to/user/confirm_user', { - method: 'POST', + const res = await fetch("https://api.iris.to/user/confirm_user", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify(event), }); @@ -234,11 +229,11 @@ export default class IrisAccount extends Component { } else { res .json() - .then((json) => { - this.setState({ error: json.message || 'error' }); + .then(json => { + this.setState({ error: json.message || "error" }); }) .catch(() => { - this.setState({ error: 'error' }); + this.setState({ error: "error" }); }); } } @@ -248,13 +243,13 @@ export default class IrisAccount extends Component { return; } const login = LoginStore.snapshot(); - const publisher = LoginStore.getPublisher(login.id) + const publisher = LoginStore.getPublisher(login.id); const event = await publisher?.note(`decline iris.to/${this.state.newUserName}`); // post signed event as request body to https://api.iris.to/user/confirm_user - const res = await fetch('https://api.iris.to/user/decline_user', { - method: 'POST', + const res = await fetch("https://api.iris.to/user/decline_user", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify(event), }); @@ -263,11 +258,11 @@ export default class IrisAccount extends Component { } else { res .json() - .then((json) => { - this.setState({ error: json.message || 'error' }); + .then(json => { + this.setState({ error: json.message || "error" }); }) .catch(() => { - this.setState({ error: 'error' }); + this.setState({ error: "error" }); }); } } @@ -275,22 +270,14 @@ export default class IrisAccount extends Component { componentDidMount() { const session = LoginStore.snapshot(); const myPub = session.publicKey; - /* - ProfileLoader.Cache.hook(h, myPub); - SocialNetwork.getProfile( - myPub, - (profile) => { - const irisToActive = - profile && profile.nip05 && profile.nip05valid && profile.nip05.endsWith('@iris.to'); - this.setState({ profile, irisToActive }); - if (profile && !irisToActive) { - this.checkExistingAccount(myPub); - } - }, - true, - ); - - */ + ProfileLoader.Cache.hook(() => { + const profile = ProfileLoader.Cache.getFromCache(myPub); + const irisToActive = profile && profile.nip05 && profile.nip05.endsWith("@iris.to"); + this.setState({ profile, irisToActive }); + if (profile && !irisToActive) { + this.checkExistingAccount(myPub); + } + }, myPub); this.checkExistingAccount(myPub); } diff --git a/packages/app/src/Element/IrisAccount/ReservedAccount.tsx b/packages/app/src/Element/IrisAccount/ReservedAccount.tsx index 68f23bd0..25b459cf 100644 --- a/packages/app/src/Element/IrisAccount/ReservedAccount.tsx +++ b/packages/app/src/Element/IrisAccount/ReservedAccount.tsx @@ -1,6 +1,6 @@ -import AccountName from './AccountName'; +import AccountName from "./AccountName"; -export default function ReservedAccount({ name = '', enableReserved = () => {}, declineReserved = () => {} }) { +export default function ReservedAccount({ name = "", enableReserved = () => {}, declineReserved = () => {} }) { return (

diff --git a/packages/app/src/lang.json b/packages/app/src/lang.json index 7b59ae7b..690be4c2 100644 --- a/packages/app/src/lang.json +++ b/packages/app/src/lang.json @@ -662,6 +662,9 @@ "OLEm6z": { "defaultMessage": "Unknown login error" }, + "OQSOJF": { + "defaultMessage": "Get a free nostr address" + }, "OQXnew": { "defaultMessage": "You subscription is still active, you can't renew yet" }, diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index 6659d9e0..bf0658dd 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -217,6 +217,7 @@ "OEW7yJ": "Zaps", "OKhRC6": "Share", "OLEm6z": "Unknown login error", + "OQSOJF": "Get a free nostr address", "OQXnew": "You subscription is still active, you can't renew yet", "ORGv1Q": "Created", "P61BTu": "Copy Event JSON", From 317b58b2983f53b355b34f1ee477bfff0d0b7ea1 Mon Sep 17 00:00:00 2001 From: Martti Malmi Date: Sat, 30 Sep 2023 18:02:49 +0300 Subject: [PATCH 3/7] rm commented-out line --- packages/app/src/Element/IrisAccount/IrisAccount.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app/src/Element/IrisAccount/IrisAccount.tsx b/packages/app/src/Element/IrisAccount/IrisAccount.tsx index 1e658b86..a00ede90 100644 --- a/packages/app/src/Element/IrisAccount/IrisAccount.tsx +++ b/packages/app/src/Element/IrisAccount/IrisAccount.tsx @@ -6,7 +6,6 @@ import AccountName from "./AccountName"; import ActiveAccount from "./ActiveAccount"; import ReservedAccount from "./ReservedAccount"; import { ProfileLoader } from "../../index"; -//import {ProfileLoader} from "../../index"; declare global { interface Window { From e191528c4cc5eefafb47bb141dbd3e939abfafa9 Mon Sep 17 00:00:00 2001 From: Martti Malmi Date: Sat, 30 Sep 2023 19:03:11 +0300 Subject: [PATCH 4/7] catch idb write error in set and bulkSet --- packages/shared/src/feed-cache.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/shared/src/feed-cache.ts b/packages/shared/src/feed-cache.ts index 2ba22c1b..022bd4a0 100644 --- a/packages/shared/src/feed-cache.ts +++ b/packages/shared/src/feed-cache.ts @@ -12,7 +12,7 @@ export interface KeyedHookFilter { /** * Dexie backed generic hookable store */ -export abstract class FeedCache { +export abstract class FeedCache { #name: string; #hooks: Array = []; #snapshot: Array = []; @@ -109,16 +109,24 @@ export abstract class FeedCache { const k = this.key(obj); this.cache.set(k, obj); if (this.table) { - await this.table.put(obj); - this.onTable.add(k); + try { + await this.table.put(obj); + this.onTable.add(k); + } catch (e) { + console.error(e); + } } this.notifyChange([k]); } async bulkSet(obj: Array | Readonly>) { if (this.table) { - await this.table.bulkPut(obj); - obj.forEach(a => this.onTable.add(this.key(a))); + try { + await this.table.bulkPut(obj); + obj.forEach(a => this.onTable.add(this.key(a))); + } catch (e) { + console.error(e); + } } obj.forEach(v => this.cache.set(this.key(v), v)); this.notifyChange(obj.map(a => this.key(a))); From 2efef99c95056cf615c8ed0769f57b5c53f7cfbe Mon Sep 17 00:00:00 2001 From: Martti Malmi Date: Sun, 1 Oct 2023 15:38:31 +0300 Subject: [PATCH 5/7] use FormattedMessage for Iris account strings --- .../src/Element/IrisAccount/AccountName.tsx | 7 ++-- .../src/Element/IrisAccount/ActiveAccount.tsx | 5 ++- .../src/Element/IrisAccount/IrisAccount.tsx | 33 ++++++++++++---- .../Element/IrisAccount/ReservedAccount.tsx | 10 +++-- packages/app/src/Element/messages.ts | 2 + packages/app/src/lang.json | 39 +++++++++++++++++++ packages/app/src/translations/en.json | 13 +++++++ packages/shared/src/feed-cache.ts | 2 +- 8 files changed, 94 insertions(+), 17 deletions(-) diff --git a/packages/app/src/Element/IrisAccount/AccountName.tsx b/packages/app/src/Element/IrisAccount/AccountName.tsx index 82c98f25..573e766d 100644 --- a/packages/app/src/Element/IrisAccount/AccountName.tsx +++ b/packages/app/src/Element/IrisAccount/AccountName.tsx @@ -1,14 +1,15 @@ import { useNavigate } from "react-router-dom"; +import FormattedMessage from "Element/FormattedMessage"; export default function AccountName({ name = "", link = true }) { const navigate = useNavigate(); return ( <>

- Username: {name} + : {name}
- Short link:{" "} + :{" "} {link ? (
- Nostr address (nip05): {name}@iris.to + : {name}@iris.to
); diff --git a/packages/app/src/Element/IrisAccount/ActiveAccount.tsx b/packages/app/src/Element/IrisAccount/ActiveAccount.tsx index 6bdaea72..fb77be99 100644 --- a/packages/app/src/Element/IrisAccount/ActiveAccount.tsx +++ b/packages/app/src/Element/IrisAccount/ActiveAccount.tsx @@ -5,6 +5,7 @@ import { System } from "../../index"; import { UserCache } from "../../Cache"; import useEventPublisher from "../../Hooks/useEventPublisher"; import { mapEventToProfile } from "@snort/system"; +import FormattedMessage from "Element/FormattedMessage"; export default function ActiveAccount({ name = "", setAsPrimary = () => {} }) { const { publicKey, readonly } = useLogin(s => ({ @@ -60,12 +61,12 @@ export default function ActiveAccount({ name = "", setAsPrimary = () => {} }) { return (
- You have an active iris.to account: + :

diff --git a/packages/app/src/Element/IrisAccount/IrisAccount.tsx b/packages/app/src/Element/IrisAccount/IrisAccount.tsx index a00ede90..03d79ac5 100644 --- a/packages/app/src/Element/IrisAccount/IrisAccount.tsx +++ b/packages/app/src/Element/IrisAccount/IrisAccount.tsx @@ -6,6 +6,9 @@ import AccountName from "./AccountName"; import ActiveAccount from "./ActiveAccount"; import ReservedAccount from "./ReservedAccount"; import { ProfileLoader } from "../../index"; +import FormattedMessage from "Element/FormattedMessage"; +import { injectIntl } from "react-intl"; +import messages from "Element/messages"; declare global { interface Window { @@ -13,8 +16,12 @@ declare global { } } +type Props = { + intl: any; +}; + // TODO split into smaller components -export default class IrisAccount extends Component { +class IrisAccount extends Component { state = { irisToActive: false, existing: null as any, @@ -63,7 +70,9 @@ export default class IrisAccount extends Component { } else { view = (
-

Register an Iris username (iris.to/username)

+

+ (iris.to/username) +

this.showChallenge(e)}>
this.onNewUserNameChange(e)} /> - +
{this.state.newUserNameValid ? ( <> - Username is available + + + ) : ( @@ -92,7 +105,9 @@ export default class IrisAccount extends Component { return ( <> -

Iris.to account

+

+ +

{view}

FAQ @@ -111,11 +126,12 @@ export default class IrisAccount extends Component { }); return; } + if (newUserName.length < 8 || newUserName.length > 15) { this.setState({ newUserName, newUserNameValid: false, - invalidUsernameMessage: "Username must be between 8 and 15 characters", + invalidUsernameMessage: this.props.intl.formatMessage(messages.IrisUserNameLengthError), }); return; } @@ -123,7 +139,7 @@ export default class IrisAccount extends Component { this.setState({ newUserName, newUserNameValid: false, - invalidUsernameMessage: "Username must only contain lowercase letters and numbers", + invalidUsernameMessage: this.props.intl.formatMessage(messages.IrisUserNameFormatError), }); return; } @@ -244,7 +260,6 @@ export default class IrisAccount extends Component { const login = LoginStore.snapshot(); const publisher = LoginStore.getPublisher(login.id); const event = await publisher?.note(`decline iris.to/${this.state.newUserName}`); - // post signed event as request body to https://api.iris.to/user/confirm_user const res = await fetch("https://api.iris.to/user/decline_user", { method: "POST", headers: { @@ -288,3 +303,5 @@ export default class IrisAccount extends Component { } } } + +export default injectIntl(IrisAccount); diff --git a/packages/app/src/Element/IrisAccount/ReservedAccount.tsx b/packages/app/src/Element/IrisAccount/ReservedAccount.tsx index 25b459cf..e9377438 100644 --- a/packages/app/src/Element/IrisAccount/ReservedAccount.tsx +++ b/packages/app/src/Element/IrisAccount/ReservedAccount.tsx @@ -1,20 +1,24 @@ import AccountName from "./AccountName"; +import FormattedMessage from "Element/FormattedMessage"; export default function ReservedAccount({ name = "", enableReserved = () => {}, declineReserved = () => {} }) { return (

- Username iris.to/{name} is reserved for you! + {s} }} + />

diff --git a/packages/app/src/Element/messages.ts b/packages/app/src/Element/messages.ts index 904ac049..9d2f31f4 100644 --- a/packages/app/src/Element/messages.ts +++ b/packages/app/src/Element/messages.ts @@ -98,4 +98,6 @@ export default defineMessages({ ConfirmUnpin: { defaultMessage: "Are you sure you want to unpin this note?" }, ReactionsLink: { defaultMessage: "{n} Reactions" }, ReBroadcast: { defaultMessage: "Broadcast Again" }, + IrisUserNameLengthError: { defaultMessage: "Name must be between 1 and 32 characters" }, + IrisUserNameFormatError: { defaultMessage: "Username must only contain lowercase letters and numbers" }, }); diff --git a/packages/app/src/lang.json b/packages/app/src/lang.json index 690be4c2..8859ed06 100644 --- a/packages/app/src/lang.json +++ b/packages/app/src/lang.json @@ -157,6 +157,9 @@ "4L2vUY": { "defaultMessage": "Your new NIP-05 handle is:" }, + "4MBtMa": { + "defaultMessage": "Name must be between 1 and 32 characters" + }, "4OB335": { "defaultMessage": "Dislike" }, @@ -329,6 +332,9 @@ "BcGMo+": { "defaultMessage": "Notes hold text content, the most popular usage of these notes is to store \"tweet like\" messages." }, + "BjNwZW": { + "defaultMessage": "Nostr address (nip05)" + }, "C1LjMx": { "defaultMessage": "Lightning Donation" }, @@ -401,6 +407,9 @@ "EcZF24": { "defaultMessage": "Custom Relays" }, + "EcfIwB": { + "defaultMessage": "Username is available" + }, "EcglP9": { "defaultMessage": "Key" }, @@ -617,6 +626,9 @@ "MWTx65": { "defaultMessage": "Default Page" }, + "MiMipu": { + "defaultMessage": "Set as primary Nostr address (nip05)" + }, "Mrpkot": { "defaultMessage": "Pay for subscription" }, @@ -626,6 +638,9 @@ "MzRYWH": { "defaultMessage": "Buying {item}" }, + "Mzizei": { + "defaultMessage": "Iris.to account" + }, "N2IrpM": { "defaultMessage": "Confirm" }, @@ -720,6 +735,9 @@ "RDZVQL": { "defaultMessage": "Check" }, + "RSr2uB": { + "defaultMessage": "Username must only contain lowercase letters and numbers" + }, "RahCRH": { "defaultMessage": "Expired" }, @@ -792,6 +810,9 @@ "Up5U7K": { "defaultMessage": "Block" }, + "UrKTqQ": { + "defaultMessage": "You have an active iris.to account" + }, "VBadwB": { "defaultMessage": "Hmm, can't find a key manager extension.. try reloading the page." }, @@ -804,6 +825,9 @@ "VR5eHw": { "defaultMessage": "Public key (npub/nprofile)" }, + "VcwrfF": { + "defaultMessage": "Yes please" + }, "VlJkSk": { "defaultMessage": "{n} muted" }, @@ -880,6 +904,9 @@ "ZS+jRE": { "defaultMessage": "Send zap splits to" }, + "Zff6lu": { + "defaultMessage": "Username iris.to/{name} is reserved for you!" + }, "Zr5TMx": { "defaultMessage": "Setup profile" }, @@ -916,6 +943,9 @@ "bxv59V": { "defaultMessage": "Just now" }, + "c+JYNI": { + "defaultMessage": "No thanks" + }, "c+oiJe": { "defaultMessage": "Install Extension", "description": "Heading for install key manager extension" @@ -964,6 +994,9 @@ "dOQCL8": { "defaultMessage": "Display name" }, + "deEeEI": { + "defaultMessage": "Register" + }, "e61Jf3": { "defaultMessage": "Coming soon" }, @@ -1112,6 +1145,9 @@ "k7sKNy": { "defaultMessage": "Our very own NIP-05 verification service, help support the development of this site and get a shiny special badge on our site!" }, + "kEZUR8": { + "defaultMessage": "Register an Iris username" + }, "kJYo0u": { "defaultMessage": "{n,plural,=0{{name} reposted} other{{name} & {n} others reposted}}" }, @@ -1295,6 +1331,9 @@ "rudscU": { "defaultMessage": "Failed to load follows, please try again later" }, + "rx1i0i": { + "defaultMessage": "Short link" + }, "sKDn4e": { "defaultMessage": "Show Badges" }, diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index bf0658dd..9d77d3fd 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -51,6 +51,7 @@ "47FYwb": "Cancel", "4IPzdn": "Primary Developers", "4L2vUY": "Your new NIP-05 handle is:", + "4MBtMa": "Name must be between 1 and 32 characters", "4OB335": "Dislike", "4Vmpt4": "Nostr Plebs is one of the first NIP-05 providers in the space and offers a good collection of domains at reasonable prices", "4Z3t5i": "Use imgproxy to compress images", @@ -107,6 +108,7 @@ "BOr9z/": "Snort is an open source project built by passionate people in their free time", "BWpuKl": "Update", "BcGMo+": "Notes hold text content, the most popular usage of these notes is to store \"tweet like\" messages.", + "BjNwZW": "Nostr address (nip05)", "C1LjMx": "Lightning Donation", "C5xzTC": "Premium", "C81/uG": "Logout", @@ -131,6 +133,7 @@ "EWyQH5": "Global", "Ebl/B2": "Translate to {lang}", "EcZF24": "Custom Relays", + "EcfIwB": "Username is available", "EcglP9": "Key", "EjFyoR": "On-chain Donation Address", "EnCOBJ": "Buy", @@ -202,9 +205,11 @@ "MP54GY": "Wallet password", "MRp6Ly": "Twitter username", "MWTx65": "Default Page", + "MiMipu": "Set as primary Nostr address (nip05)", "Mrpkot": "Pay for subscription", "MuVeKe": "Buy nostr address", "MzRYWH": "Buying {item}", + "Mzizei": "Iris.to account", "N2IrpM": "Confirm", "NAidKb": "Notifications", "NAuFNH": "You already have a subscription of this type, please renew or pay", @@ -236,6 +241,7 @@ "R/6nsx": "Subscription", "R81upa": "People you follow", "RDZVQL": "Check", + "RSr2uB": "Username must only contain lowercase letters and numbers", "RahCRH": "Expired", "RfhLwC": "By: {author}", "RhDAoS": "Are you sure you want to delete {id}", @@ -259,10 +265,12 @@ "UT7Nkj": "New Chat", "UUPFlt": "Users must accept the content warning to show the content of your note.", "Up5U7K": "Block", + "UrKTqQ": "You have an active iris.to account", "VBadwB": "Hmm, can't find a key manager extension.. try reloading the page.", "VN0+Fz": "Balance: {amount} sats", "VOjC1i": "Pick which upload service you want to upload attachments to", "VR5eHw": "Public key (npub/nprofile)", + "VcwrfF": "Yes please", "VlJkSk": "{n} muted", "VnXp8Z": "Avatar", "VvaJst": "View Wallets", @@ -288,6 +296,7 @@ "ZKORll": "Activate Now", "ZLmyG9": "Contributors", "ZS+jRE": "Send zap splits to", + "Zff6lu": "Username iris.to/{name} is reserved for you!", "Zr5TMx": "Setup profile", "a5UPxh": "Fund developers and platforms providing NIP-05 verification services", "a7TDNm": "Notes will stream in real time into global and notes tab", @@ -300,6 +309,7 @@ "bfvyfs": "Anon", "brAXSu": "Pick a username", "bxv59V": "Just now", + "c+JYNI": "No thanks", "c+oiJe": "Install Extension", "c2DTVd": "Enter a pin to encrypt your private key, you must enter this pin every time you open Snort.", "c35bj2": "If you have an enquiry about your NIP-05 order please DM {link}", @@ -315,6 +325,7 @@ "d6CyG5": "History", "d7d0/x": "LN Address", "dOQCL8": "Display name", + "deEeEI": "Register", "e61Jf3": "Coming soon", "e7VmYP": "Enter pin to unlock your private key", "e7qqly": "Mark All Read", @@ -364,6 +375,7 @@ "jzgQ2z": "{n} Reactions", "k2veDA": "Write", "k7sKNy": "Our very own NIP-05 verification service, help support the development of this site and get a shiny special badge on our site!", + "kEZUR8": "Register an Iris username", "kJYo0u": "{n,plural,=0{{name} reposted} other{{name} & {n} others reposted}}", "kaaf1E": "now", "kuPHYE": "{n,plural,=0{{name} liked} other{{name} & {n} others liked}}", @@ -424,6 +436,7 @@ "rmdsT4": "{n} days", "rrfdTe": "This is the same technology which is used by Bitcoin and has been proven to be extremely secure.", "rudscU": "Failed to load follows, please try again later", + "rx1i0i": "Short link", "sKDn4e": "Show Badges", "sUNhQE": "user", "sWnYKw": "Snort is designed to have a similar experience to Twitter.", diff --git a/packages/shared/src/feed-cache.ts b/packages/shared/src/feed-cache.ts index 022bd4a0..ec32b4d8 100644 --- a/packages/shared/src/feed-cache.ts +++ b/packages/shared/src/feed-cache.ts @@ -12,7 +12,7 @@ export interface KeyedHookFilter { /** * Dexie backed generic hookable store */ -export abstract class FeedCache { +export abstract class FeedCache { #name: string; #hooks: Array = []; #snapshot: Array = []; From 9d19d02636c0ebd2a49205481429e40037d1fd55 Mon Sep 17 00:00:00 2001 From: Martti Malmi Date: Wed, 4 Oct 2023 13:37:38 +0300 Subject: [PATCH 6/7] error and success classNames --- packages/app/src/Element/IrisAccount/IrisAccount.tsx | 6 +++--- packages/app/src/Element/IrisAccount/ReservedAccount.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/app/src/Element/IrisAccount/IrisAccount.tsx b/packages/app/src/Element/IrisAccount/IrisAccount.tsx index 03d79ac5..776e8cc3 100644 --- a/packages/app/src/Element/IrisAccount/IrisAccount.tsx +++ b/packages/app/src/Element/IrisAccount/IrisAccount.tsx @@ -52,7 +52,7 @@ class IrisAccount extends Component { /> ); } else if (this.state.error) { - view =
Error: {this.state.error}
; + view =
Error: {this.state.error}
; } else if (this.state.showChallenge) { window.cf_turnstile_callback = (token: any) => this.register(token); view = ( @@ -89,13 +89,13 @@ class IrisAccount extends Component {
{this.state.newUserNameValid ? ( <> - + ) : ( - {this.state.invalidUsernameMessage} + {this.state.invalidUsernameMessage} )}
diff --git a/packages/app/src/Element/IrisAccount/ReservedAccount.tsx b/packages/app/src/Element/IrisAccount/ReservedAccount.tsx index e9377438..3c4818ac 100644 --- a/packages/app/src/Element/IrisAccount/ReservedAccount.tsx +++ b/packages/app/src/Element/IrisAccount/ReservedAccount.tsx @@ -4,7 +4,7 @@ import FormattedMessage from "Element/FormattedMessage"; export default function ReservedAccount({ name = "", enableReserved = () => {}, declineReserved = () => {} }) { return (
-

+

{s} }} From 1eb2fa4b9012e3094befc9b062b9fadd372f529d Mon Sep 17 00:00:00 2001 From: Martti Malmi Date: Wed, 4 Oct 2023 13:39:53 +0300 Subject: [PATCH 7/7] gap-2 -> g8 --- packages/app/src/Element/IrisAccount/IrisAccount.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/Element/IrisAccount/IrisAccount.tsx b/packages/app/src/Element/IrisAccount/IrisAccount.tsx index 776e8cc3..9923d630 100644 --- a/packages/app/src/Element/IrisAccount/IrisAccount.tsx +++ b/packages/app/src/Element/IrisAccount/IrisAccount.tsx @@ -74,7 +74,7 @@ class IrisAccount extends Component { (iris.to/username)

this.showChallenge(e)}> -
+