Files
snort/packages/app/src/Element/IrisAccount/IrisAccount.tsx
2023-10-04 13:22:55 +03:00

308 lines
9.0 KiB
TypeScript

/* 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";
import FormattedMessage from "Element/FormattedMessage";
import { injectIntl } from "react-intl";
import messages from "Element/messages";
declare global {
interface Window {
cf_turnstile_callback: any;
}
}
type Props = {
intl: any;
};
// TODO split into smaller components
class IrisAccount extends Component<Props> {
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>
<FormattedMessage defaultMessage="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 type="submit">
<FormattedMessage defaultMessage="Register" />
</button>
</div>
<div>
{this.state.newUserNameValid ? (
<>
<span className="text-iris-green">
<FormattedMessage defaultMessage="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>
<FormattedMessage defaultMessage="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: this.props.intl.formatMessage(messages.IrisUserNameLengthError),
});
return;
}
if (!newUserName.match(/^[a-z0-9_.]+$/)) {
this.setState({
newUserName,
newUserNameValid: false,
invalidUsernameMessage: this.props.intl.formatMessage(messages.IrisUserNameFormatError),
});
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}`);
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(() => {
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);
}
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 });
}
}
}
export default injectIntl(IrisAccount);