import React, { Component, ChangeEvent } from "react"; import { withStyles, Paper, Typography, Button, TextField, Fade, Link, CircularProgress, Tooltip, Dialog, DialogTitle, DialogActions, DialogContent } from "@material-ui/core"; import { styles } from "./WelcomePage.styles"; import Mastodon from "megalodon"; import { SaveClientSession } from "../types/SessionData"; import { createHyperspaceApp, getRedirectAddress } from "../utilities/login"; import { parseUrl } from "query-string"; import { getConfig } from "../utilities/settings"; import { isDarwinApp } from "../utilities/desktop"; import axios from "axios"; import { withSnackbar, withSnackbarProps } from "notistack"; import { Config } from "../types/Config"; interface IWelcomeProps extends withSnackbarProps { classes: any; } interface IWelcomeState { logoUrl?: string; backgroundUrl?: string; brandName?: string; registerBase?: string; federates?: boolean; wantsToLogin: boolean; user: string; userInputError: boolean; userInputErrorMessage: string; clientId?: string; clientSecret?: string; authUrl?: string; foundSavedLogin: boolean; authority: boolean; license?: string; repo?: string; defaultRedirectAddress: string; openAuthDialog: boolean; authCode: string; emergencyMode: boolean; version: string; } class WelcomePage extends Component { client: any; constructor(props: any) { super(props); this.state = { wantsToLogin: false, user: "", userInputError: false, foundSavedLogin: false, authority: false, userInputErrorMessage: "", defaultRedirectAddress: "", openAuthDialog: false, authCode: "", emergencyMode: false, version: "" }; getConfig() .then((result: any) => { if (result !== undefined) { let config: Config = result; if (result.location === "dynamic") { console.warn( "Recirect URI is set to dynamic, which may affect how sign-in works for some users. Careful!" ); } this.setState({ logoUrl: config.branding ? result.branding.logo : "logo.png", backgroundUrl: config.branding ? result.branding.background : "background.png", brandName: config.branding ? result.branding.name : "Hyperspace", registerBase: config.registration ? result.registration.defaultInstance : "", federates: config.federation.universalLogin, license: config.license.url, repo: config.repository, defaultRedirectAddress: config.location != "dynamic" ? config.location : `https://${window.location.host}`, version: config.version }); } }) .catch(() => { console.error( "config.json is missing. If you want to customize Hyperspace, please include config.json" ); }); } componentDidMount() { if (localStorage.getItem("login")) { this.getSavedSession(); this.setState({ foundSavedLogin: true }); this.checkForToken(); } } updateUserInfo(user: string) { this.setState({ user }); } updateAuthCode(code: string) { this.setState({ authCode: code }); } toggleAuthDialog() { this.setState({ openAuthDialog: !this.state.openAuthDialog }); } readyForAuth() { if (localStorage.getItem("baseurl")) { return true; } else { return false; } } getSavedSession() { let loginData = localStorage.getItem("login"); if (loginData) { let session: SaveClientSession = JSON.parse(loginData); this.setState({ clientId: session.clientId, clientSecret: session.clientSecret, authUrl: session.authUrl, emergencyMode: session.emergency }); } } startEmergencyLogin() { if (!this.state.emergencyMode) { this.createEmergencyLogin(); } this.toggleAuthDialog(); } startRegistration() { if (this.state.registerBase) { return "https://" + this.state.registerBase + "/auth/sign_up"; } else { return "https://joinmastodon.org/#getting-started"; } } watchUsernameField(event: any) { if (event.keyCode === 13) this.startLogin(); } watchAuthField(event: any) { if (event.keyCode === 13) this.authorizeEmergencyLogin(); } getLoginUser(user: string) { if (user.includes("@")) { if (this.state.federates) { let newUser = user; this.setState({ user: newUser }); return "https://" + newUser.split("@")[1]; } else { let newUser = `${user}@${ this.state.registerBase ? this.state.registerBase : "mastodon.social" }`; this.setState({ user: newUser }); return ( "https://" + (this.state.registerBase ? this.state.registerBase : "mastodon.social") ); } } else { let newUser = `${user}@${ this.state.registerBase ? this.state.registerBase : "mastodon.social" }`; this.setState({ user: newUser }); return ( "https://" + (this.state.registerBase ? this.state.registerBase : "mastodon.social") ); } } startLogin() { let error = this.checkForErrors(); if (!error) { const scopes = "read write follow"; const baseurl = this.getLoginUser(this.state.user); localStorage.setItem("baseurl", baseurl); createHyperspaceApp( this.state.brandName ? this.state.brandName : "Hyperspace", scopes, baseurl, getRedirectAddress(this.state.defaultRedirectAddress) ).then((resp: any) => { let saveSessionForCrashing: SaveClientSession = { clientId: resp.clientId, clientSecret: resp.clientSecret, authUrl: resp.url, emergency: false }; localStorage.setItem( "login", JSON.stringify(saveSessionForCrashing) ); this.setState({ clientId: resp.clientId, clientSecret: resp.clientSecret, authUrl: resp.url, wantsToLogin: true }); }); } else { } } createEmergencyLogin() { console.log("Creating an emergency login..."); const scopes = "read write follow"; const baseurl = localStorage.getItem("baseurl") || this.getLoginUser(this.state.user); Mastodon.registerApp( this.state.brandName ? this.state.brandName : "Hyperspace", { scopes: scopes }, baseurl ).then((appData: any) => { let saveSessionForCrashing: SaveClientSession = { clientId: appData.clientId, clientSecret: appData.clientSecret, authUrl: appData.url, emergency: true }; localStorage.setItem( "login", JSON.stringify(saveSessionForCrashing) ); this.setState({ clientId: appData.clientId, clientSecret: appData.clientSecret, authUrl: appData.url }); }); } authorizeEmergencyLogin() { window.location.href = `${this.state.defaultRedirectAddress}/?code=${this.state.authCode}#/`; } resumeLogin() { let loginData = localStorage.getItem("login"); if (loginData) { let session: SaveClientSession = JSON.parse(loginData); this.setState({ clientId: session.clientId, clientSecret: session.clientSecret, authUrl: session.authUrl, emergencyMode: session.emergency, wantsToLogin: true }); } } checkForErrors(): boolean { let userInputError = false; let userInputErrorMessage = ""; if (this.state.user === "") { userInputError = true; userInputErrorMessage = "Username cannot be blank."; this.setState({ userInputError, userInputErrorMessage }); return true; } else { if (this.state.user.includes("@")) { if (this.state.federates && this.state.federates === true) { let baseUrl = this.state.user.split("@")[1]; axios .get("https://" + baseUrl + "/api/v1/timelines/public") .catch((err: Error) => { let userInputError = true; let userInputErrorMessage = "Instance name is invalid."; this.setState({ userInputError, userInputErrorMessage }); return true; }); } else if ( this.state.user.includes( this.state.registerBase ? this.state.registerBase : "mastodon.social" ) ) { this.setState({ userInputError, userInputErrorMessage }); return false; } else { userInputError = true; userInputErrorMessage = "You cannot sign in with this username."; this.setState({ userInputError, userInputErrorMessage }); return true; } } else { this.setState({ userInputError, userInputErrorMessage }); return false; } this.setState({ userInputError, userInputErrorMessage }); return false; } } checkForToken() { let location = window.location.href; if (location.includes("?code=")) { let code = parseUrl(location).query.code as string; this.setState({ authority: true }); let loginData = localStorage.getItem("login"); if (loginData) { let clientLoginSession: SaveClientSession = JSON.parse( loginData ); Mastodon.fetchAccessToken( clientLoginSession.clientId, clientLoginSession.clientSecret, code, localStorage.getItem("baseurl") as string, this.state.emergencyMode ? undefined : clientLoginSession.authUrl.includes( "urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob" ) ? undefined : window.location.protocol === "hyperspace:" ? "hyperspace://hyperspace/app/" : `https://${window.location.host}` ) .then((tokenData: any) => { localStorage.setItem( "access_token", tokenData.access_token ); window.location.href = window.location.protocol === "hyperspace:" ? "hyperspace://hyperspace/app/" : `https://${window.location.host}/#/`; }) .catch((err: Error) => { this.props.enqueueSnackbar( `Couldn't authorize ${ this.state.brandName ? this.state.brandName : "Hyperspace" }: ${err.name}`, { variant: "error" } ); console.error(err.message); }); } } } titlebar() { const { classes } = this.props; if (isDarwinApp()) { return (
{this.state.brandName ? this.state.brandName : "Hyperspace"}
); } } showLanding() { const { classes } = this.props; return (
Sign in with your Mastodon account
this.updateUserInfo(event.target.value)} onKeyDown={event => this.watchUsernameField(event)} error={this.state.userInputError} onBlur={() => this.checkForErrors()} > {this.state.userInputError ? ( {this.state.userInputErrorMessage} ) : null}
{this.state.registerBase && this.state.federates ? ( Not from{" "} {this.state.registerBase ? this.state.registerBase : "noinstance"} ? Sign in with your{" "} full username . ) : null}
{this.state.foundSavedLogin ? ( Signing in from a previous session?{" "} this.resumeLogin()} > Continue login . ) : null}
); } showLoginAuth() { const { classes } = this.props; return (
Howdy,{" "} {this.state.user ? this.state.user.split("@")[0] : "user"} To continue, finish signing in on your instance's website and authorize{" "} {this.state.brandName ? this.state.brandName : "Hyperspace"} .
Having trouble signing in?{" "} this.startEmergencyLogin()} className={classes.welcomeLink} > Sign in with a code.
); } showAuthDialog() { const { classes } = this.props; return ( Authorize with a code If you're having trouble authorizing Hyperspace, you can manually request for an authorization code. Click 'Request Code' and then paste the code in the authorization code box to continue.

this.updateAuthCode(event.target.value) } onKeyDown={event => this.watchAuthField(event)} >
); } showAuthority() { const { classes } = this.props; return (
Authorizing Please wait while Hyperspace authorizes with Mastodon. This shouldn't take long...
); } render() { const { classes } = this.props; return (
{this.titlebar()}
{
{this.state.authority ? this.showAuthority() : this.state.wantsToLogin ? this.showLoginAuth() : this.showLanding()}
© {new Date().getFullYear()}{" "} {this.state.brandName && this.state.brandName !== "Hyperspace" ? `${this.state.brandName} developers and the ` : ""}{" "} Hyperspace {" "} developers. All rights reserved. {this.state.repo ? ( Source code {" "} |{" "} ) : null} License {" "} | File an Issue {this.state.brandName ? this.state.brandName : "Hypersapce"}{" "} v.{this.state.version}{" "} {this.state.brandName && this.state.brandName !== "Hyperspace" ? "(Hyperspace-like)" : null}
{this.showAuthDialog()}
); } } export default withStyles(styles)(withSnackbar(WelcomePage));