Wizard: add a first-run wizard to help create new accounts.

master
Tom Hacohen 4 years ago
parent 2a43a9e94e
commit 7ba75ae3c1

@ -94,6 +94,8 @@ export const routeResolver = new RouteResolver({
}, },
signup: { signup: {
}, },
wizard: {
},
settings: { settings: {
}, },
debug: { debug: {

@ -23,12 +23,6 @@ import { Redirect, useLocation } from "react-router";
import { routeResolver } from "./App"; import { routeResolver } from "./App";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
interface LocationState {
from: {
pathname: string;
};
}
export default function LoginPage() { export default function LoginPage() {
const credentials = useCredentials(); const credentials = useCredentials();
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -36,10 +30,9 @@ export default function LoginPage() {
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const [fetchError, setFetchError] = React.useState<Error>(); const [fetchError, setFetchError] = React.useState<Error>();
const { from } = location.state as LocationState || { from: { pathname: routeResolver.getRoute("home") } };
if (credentials) { if (credentials) {
return ( return (
<Redirect to={from.pathname} /> <Redirect to={{ pathname: routeResolver.getRoute("wizard"), state: location.state }} />
); );
} }

@ -9,6 +9,7 @@ import SyncGate from "./SyncGate";
import { routeResolver } from "./App"; import { routeResolver } from "./App";
import SignupPage from "./SignupPage"; import SignupPage from "./SignupPage";
import LoginPage from "./LoginPage"; import LoginPage from "./LoginPage";
import WizardPage from "./WizardPage";
export default function MainRouter() { export default function MainRouter() {
@ -26,6 +27,12 @@ export default function MainRouter() {
> >
<LoginPage /> <LoginPage />
</Route> </Route>
<PrivateRoute
path={routeResolver.getRoute("wizard")}
exact
>
<WizardPage />
</PrivateRoute>
<PrivateRoute <PrivateRoute
path="*" path="*"
> >

@ -0,0 +1,162 @@
// SPDX-FileCopyrightText: © 2017 EteSync Authors
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from "react";
import * as Etebase from "etebase";
import { Redirect, useLocation } from "react-router";
import Button from "@material-ui/core/Button";
import Alert from "@material-ui/lab/Alert";
import { routeResolver } from "./App";
import Container from "./widgets/Container";
import LoadingIndicator from "./widgets/LoadingIndicator";
import Wizard, { WizardNavigationBar, PagePropsType } from "./widgets/Wizard";
import { SyncManager } from "./sync/SyncManager";
import { store } from "./store";
import { useCredentials } from "./credentials";
import wizardWelcome from "./images/wizard-welcome.svg";
import wizardCreate from "./images/wizard-create.svg";
interface LocationState {
from: {
pathname: string;
};
}
const wizardPages = [
(props: PagePropsType) => (
<>
<div style={{ display: "flex", flexDirection: "column", flex: 1, justifyContent: "center", alignItems: "center" }}>
<h2 style={{ textAlign: "center" }}>Welcome to EteSync!</h2>
<p style={{ textAlign: "center" }}>
Please follow these few quick steps to help you get started.
</p>
<img src={wizardWelcome} style={{ maxWidth: "30em", marginTop: "2em" }} />
</div>
<WizardNavigationBar {...props} />
</>
),
(props: PagePropsType) => (
<SetupCollectionsPage {...props} />
),
];
function SetupCollectionsPage(props: PagePropsType) {
const etebase = useCredentials()!;
const [error, setError] = React.useState<Error>();
const [loading, setLoading] = React.useState(false);
async function onNext() {
setLoading(true);
try {
const colMgr = etebase.getCollectionManager();
const types = [
["etebase.vcard", "My Contacts"],
["etebase.vevent", "My Calendar"],
["etebase.vtodo", "My Tasks"],
];
for (const [type, name] of types) {
const meta: Etebase.CollectionMetadata = {
type,
name,
};
const collection = await colMgr.create(meta, "");
await colMgr.upload(collection);
}
props.next?.();
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
}
const next = (loading) ? undefined : onNext;
return (
<>
<div style={{ display: "flex", flexDirection: "column", flex: 1, justifyContent: "center", alignItems: "center" }}>
<h2 style={{ textAlign: "center" }}>Setup Collections</h2>
<p style={{ textAlign: "center", maxWidth: "50em" }}>
In order to start using EteSync you need to create collections to store your data. Clicking <i>Finish</i> below will create a default calendar, address book and a task list for you.
</p>
{(loading) ? (
<LoadingIndicator style={{ display: "block", margin: "40px auto" }} />
) : (error) ? (
<Alert severity="error">{error.message}</Alert>
) : (
<img src={wizardCreate} style={{ maxWidth: "30em", marginTop: "2em" }} />
)}
</div>
<WizardNavigationBar {...props} next={next} />
</>
);
}
export default function WizardPage() {
const [tryCount, setTryCount] = React.useState(0);
const [ranWizard, setRanWizard] = React.useState(false);
const [syncError, setSyncError] = React.useState<Error>();
const etebase = useCredentials();
const location = useLocation();
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
setLoading(true);
setSyncError(undefined);
(async () => {
const syncManager = SyncManager.getManager(etebase!);
const sync = syncManager.sync(true);
try {
await sync;
const cachedCollection = store.getState().cache.collections;
// XXX new account - though should change test to see if there are any PIM types
if (cachedCollection.size > 0) {
setRanWizard(true);
}
} catch (e) {
setSyncError(e);
}
setLoading(false);
})();
}, [tryCount]);
if (syncError) {
return (
<Container>
<div style={{ display: "flex", flexDirection: "column", flex: 1, justifyContent: "center", alignItems: "center" }}>
<h2 style={{ textAlign: "center" }}>Error!</h2>
<p style={{ textAlign: "center" }}>
{syncError?.message}
</p>
<Button
variant="contained"
color="primary"
onClick={() => setTryCount(tryCount + 1)}
>
Retry
</Button>
</div>
</Container>
);
}
if (loading) {
return (<LoadingIndicator style={{ display: "block", margin: "40px auto" }} />);
}
if (!ranWizard) {
return (
<Wizard pages={wizardPages} onFinish={() => setRanWizard(true)} style={{ display: "flex", flexDirection: "column", flex: 1 }} />
);
}
const { from } = location.state as LocationState || { from: { pathname: routeResolver.getRoute("home") } };
return (
<Redirect to={from.pathname} />
);
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 49 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 40 KiB

@ -87,7 +87,7 @@ export class SyncManager {
return true; return true;
} }
public async sync() { public async sync(alwaysThrowErrors = false) {
if (this.isSyncing) { if (this.isSyncing) {
return false; return false;
} }
@ -97,6 +97,10 @@ export class SyncManager {
const stoken = await this.fetchAllCollections(); const stoken = await this.fetchAllCollections();
return stoken; return stoken;
} catch (e) { } catch (e) {
if (alwaysThrowErrors) {
throw e;
}
if (e instanceof Etebase.NetworkError || e instanceof Etebase.TemporaryServerError) { if (e instanceof Etebase.NetworkError || e instanceof Etebase.TemporaryServerError) {
// Ignore network errors // Ignore network errors
return null; return null;

@ -0,0 +1,63 @@
// SPDX-FileCopyrightText: © 2019 EteSync Authors
// SPDX-License-Identifier: GPL-3.0-only
import * as React from "react";
import Button from "@material-ui/core/Button";
import Container from "./Container";
export interface PagePropsType {
prev?: () => void;
next?: () => void;
currentPage: number;
totalPages: number;
}
export function WizardNavigationBar(props: PagePropsType) {
const first = props.currentPage === 0;
const last = props.currentPage === props.totalPages - 1;
return (
<div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", marginTop: "auto" }}>
<Button
variant="contained"
disabled={first}
onClick={props.prev}
>
Prev
</Button>
<Button
variant="contained"
color="primary"
disabled={!props.next}
onClick={props.next}
>
{(last) ? "Finish" : "Next"}
</Button>
</div>
);
}
interface PropsType extends React.HTMLProps<HTMLDivElement> {
pages: ((props: PagePropsType) => React.ReactNode)[];
onFinish: () => void;
}
export default function Wizard(inProps: PropsType) {
const [currentPage, setCurrentPage] = React.useState(0);
const { pages, onFinish, ...props } = inProps;
const Content = pages[currentPage];
const first = currentPage === 0;
const last = currentPage === pages.length - 1;
const prev = !first ? () => setCurrentPage(currentPage - 1) : undefined;
const next = !last ? () => setCurrentPage(currentPage + 1) : onFinish;
return (
<Container {...props}>
{Content({ prev, next, currentPage, totalPages: pages.length })}
</Container>
);
}
Loading…
Cancel
Save