iris account registration
This commit is contained in:
parent
fc38049b87
commit
ecb0f0e78a
@ -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
|
||||
*/
|
||||
|
31
packages/app/src/Element/IrisAccount/AccountName.tsx
Normal file
31
packages/app/src/Element/IrisAccount/AccountName.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import {useNavigate} from "react-router-dom";
|
||||
|
||||
export default function AccountName({ name = '', link = true }) {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
Username: <b>{name}</b>
|
||||
</div>
|
||||
<div>
|
||||
Short link:{' '}
|
||||
{link ? (
|
||||
<a
|
||||
href={`https://iris.to/${name}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(`/${name}`);
|
||||
}}
|
||||
>
|
||||
iris.to/{name}
|
||||
</a>
|
||||
) : (
|
||||
<>iris.to/{name}</>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
Nostr address (nip05): <b>{name}@iris.to</b>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
73
packages/app/src/Element/IrisAccount/ActiveAccount.tsx
Normal file
73
packages/app/src/Element/IrisAccount/ActiveAccount.tsx
Normal file
@ -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<string, string | number | undefined | boolean>;
|
||||
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 (
|
||||
<div>
|
||||
<div className="negative">
|
||||
You have an active iris.to account:
|
||||
<AccountName name={name} />
|
||||
</div>
|
||||
<p>
|
||||
<button className="btn btn-sm btn-primary" onClick={onClick}>
|
||||
Set as primary Nostr address (nip05)
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
304
packages/app/src/Element/IrisAccount/IrisAccount.tsx
Normal file
304
packages/app/src/Element/IrisAccount/IrisAccount.tsx
Normal file
@ -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 = <AccountName name={username}/>;
|
||||
} else if (this.state.existing && this.state.existing.confirmed) {
|
||||
view = (
|
||||
<ActiveAccount
|
||||
name={this.state.existing.name}
|
||||
setAsPrimary={() => this.setState({irisToActive: true})}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.existing) {
|
||||
view = (
|
||||
<ReservedAccount
|
||||
name={this.state.existing.name}
|
||||
enableReserved={() => this.enableReserved()}
|
||||
declineReserved={() => this.declineReserved()}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.error) {
|
||||
view = <div className="text-iris-red">Error: {this.state.error}</div>;
|
||||
} else if (this.state.showChallenge) {
|
||||
window.cf_turnstile_callback = (token: any) => this.register(token);
|
||||
view = (
|
||||
<>
|
||||
<div
|
||||
className="cf-turnstile"
|
||||
data-sitekey={
|
||||
['iris.to', 'beta.iris.to', 'snort.social'].includes(window.location.hostname)
|
||||
? '0x4AAAAAAACsEd8XuwpPTFwz'
|
||||
: '3x00000000000000000000FF'
|
||||
}
|
||||
data-callback="cf_turnstile_callback"
|
||||
></div>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
view = (
|
||||
<div>
|
||||
<p>Register an Iris username (iris.to/username)</p>
|
||||
<form onSubmit={(e) => this.showChallenge(e)}>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={this.state.newUserName}
|
||||
onInput={(e) => this.onNewUserNameChange(e)}
|
||||
/>
|
||||
<button className="btn btn-primary" type="submit">
|
||||
Register
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
{this.state.newUserNameValid ? (
|
||||
<>
|
||||
<span className="text-iris-green">Username is available</span>
|
||||
<AccountName name={this.state.newUserName} link={false}/>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-iris-red">{this.state.invalidUsernameMessage}</span>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>Iris.to account</h3>
|
||||
{view}
|
||||
<p>
|
||||
<a href="https://github.com/irislib/faq#iris-username">FAQ</a>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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<HTMLFormElement>) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
22
packages/app/src/Element/IrisAccount/ReservedAccount.tsx
Normal file
22
packages/app/src/Element/IrisAccount/ReservedAccount.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import AccountName from './AccountName';
|
||||
|
||||
export default function ReservedAccount({ name = '', enableReserved = () => {}, declineReserved = () => {} }) {
|
||||
return (
|
||||
<div>
|
||||
<p className="text-iris-green">
|
||||
Username iris.to/<b>{name}</b> is reserved for you!
|
||||
</p>
|
||||
<AccountName name={name} link={false} />
|
||||
<p>
|
||||
<button className="btn btn-sm btn-primary" onClick={() => enableReserved()}>
|
||||
Yes please
|
||||
</button>
|
||||
</p>
|
||||
<p>
|
||||
<button className="btn btn-sm btn-neutral" onClick={() => declineReserved()}>
|
||||
No thanks
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
38
packages/app/src/Pages/FreeNostrAddressPage.tsx
Normal file
38
packages/app/src/Pages/FreeNostrAddressPage.tsx
Normal file
@ -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 (
|
||||
<div className="main-content p">
|
||||
<h2>
|
||||
<FormattedMessage defaultMessage="Get a free nostr address" />
|
||||
</h2>
|
||||
<p>
|
||||
<FormattedMessage {...messages.Nip05} />
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage {...messages.Nip05Pros} />
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<FormattedMessage {...messages.AvoidImpersonators} />
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage {...messages.EasierToFind} />
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage {...messages.Funding} />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<IrisAccount />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -162,7 +162,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
||||
<button className="flex f-center" type="button" onClick={() => navigate("/nostr-address")}>
|
||||
<FormattedMessage defaultMessage="Buy nostr address" />
|
||||
</button>
|
||||
<button className="flex f-center secondary" type="button" onClick={() => navigate("/nostr-address")}>
|
||||
<button className="flex f-center secondary" type="button" onClick={() => navigate("/free-nostr-address")}>
|
||||
<FormattedMessage defaultMessage="Get a free one" />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -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: <SettingsPage />,
|
||||
children: SettingsRoutes,
|
||||
},
|
||||
{
|
||||
path: "/free-nostr-address",
|
||||
element: <FreeNostrAddressPage />,
|
||||
},
|
||||
{
|
||||
path: "/nostr-address",
|
||||
element: <NostrAddressPage />,
|
||||
|
Loading…
x
Reference in New Issue
Block a user