From 7ba75ae3c19f37428c50ad202f8c02fb7a41f986 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 7 Sep 2020 17:33:04 +0300 Subject: [PATCH] Wizard: add a first-run wizard to help create new accounts. --- src/App.tsx | 2 + src/LoginPage.tsx | 9 +- src/MainRouter.tsx | 7 ++ src/WizardPage.tsx | 162 ++++++++++++++++++++++++++++++++++ src/images/wizard-create.svg | 1 + src/images/wizard-welcome.svg | 1 + src/sync/SyncManager.ts | 6 +- src/widgets/Wizard.tsx | 63 +++++++++++++ 8 files changed, 242 insertions(+), 9 deletions(-) create mode 100644 src/WizardPage.tsx create mode 100644 src/images/wizard-create.svg create mode 100644 src/images/wizard-welcome.svg create mode 100644 src/widgets/Wizard.tsx diff --git a/src/App.tsx b/src/App.tsx index 2f664ac..bc69bdb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -94,6 +94,8 @@ export const routeResolver = new RouteResolver({ }, signup: { }, + wizard: { + }, settings: { }, debug: { diff --git a/src/LoginPage.tsx b/src/LoginPage.tsx index 9ad0e81..4c3d5d9 100644 --- a/src/LoginPage.tsx +++ b/src/LoginPage.tsx @@ -23,12 +23,6 @@ import { Redirect, useLocation } from "react-router"; import { routeResolver } from "./App"; import { Link } from "react-router-dom"; -interface LocationState { - from: { - pathname: string; - }; -} - export default function LoginPage() { const credentials = useCredentials(); const dispatch = useDispatch(); @@ -36,10 +30,9 @@ export default function LoginPage() { const [loading, setLoading] = React.useState(false); const [fetchError, setFetchError] = React.useState(); - const { from } = location.state as LocationState || { from: { pathname: routeResolver.getRoute("home") } }; if (credentials) { return ( - + ); } diff --git a/src/MainRouter.tsx b/src/MainRouter.tsx index 585815a..b57be8a 100644 --- a/src/MainRouter.tsx +++ b/src/MainRouter.tsx @@ -9,6 +9,7 @@ import SyncGate from "./SyncGate"; import { routeResolver } from "./App"; import SignupPage from "./SignupPage"; import LoginPage from "./LoginPage"; +import WizardPage from "./WizardPage"; export default function MainRouter() { @@ -26,6 +27,12 @@ export default function MainRouter() { > + + + diff --git a/src/WizardPage.tsx b/src/WizardPage.tsx new file mode 100644 index 0000000..3cf72f7 --- /dev/null +++ b/src/WizardPage.tsx @@ -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) => ( + <> +
+

Welcome to EteSync!

+

+ Please follow these few quick steps to help you get started. +

+ +
+ + + ), + (props: PagePropsType) => ( + + ), +]; + +function SetupCollectionsPage(props: PagePropsType) { + const etebase = useCredentials()!; + const [error, setError] = React.useState(); + 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 ( + <> +
+

Setup Collections

+

+ In order to start using EteSync you need to create collections to store your data. Clicking Finish below will create a default calendar, address book and a task list for you. +

+ {(loading) ? ( + + ) : (error) ? ( + {error.message} + ) : ( + + )} +
+ + + ); +} + +export default function WizardPage() { + const [tryCount, setTryCount] = React.useState(0); + const [ranWizard, setRanWizard] = React.useState(false); + const [syncError, setSyncError] = React.useState(); + 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 ( + +
+

Error!

+

+ {syncError?.message} +

+ +
+
+ ); + } + + if (loading) { + return (); + } + + if (!ranWizard) { + return ( + setRanWizard(true)} style={{ display: "flex", flexDirection: "column", flex: 1 }} /> + ); + } + + const { from } = location.state as LocationState || { from: { pathname: routeResolver.getRoute("home") } }; + return ( + + ); +} diff --git a/src/images/wizard-create.svg b/src/images/wizard-create.svg new file mode 100644 index 0000000..4cb3c9d --- /dev/null +++ b/src/images/wizard-create.svg @@ -0,0 +1 @@ +upload \ No newline at end of file diff --git a/src/images/wizard-welcome.svg b/src/images/wizard-welcome.svg new file mode 100644 index 0000000..8555c21 --- /dev/null +++ b/src/images/wizard-welcome.svg @@ -0,0 +1 @@ +product tour \ No newline at end of file diff --git a/src/sync/SyncManager.ts b/src/sync/SyncManager.ts index c5d1711..09c094a 100644 --- a/src/sync/SyncManager.ts +++ b/src/sync/SyncManager.ts @@ -87,7 +87,7 @@ export class SyncManager { return true; } - public async sync() { + public async sync(alwaysThrowErrors = false) { if (this.isSyncing) { return false; } @@ -97,6 +97,10 @@ export class SyncManager { const stoken = await this.fetchAllCollections(); return stoken; } catch (e) { + if (alwaysThrowErrors) { + throw e; + } + if (e instanceof Etebase.NetworkError || e instanceof Etebase.TemporaryServerError) { // Ignore network errors return null; diff --git a/src/widgets/Wizard.tsx b/src/widgets/Wizard.tsx new file mode 100644 index 0000000..39af413 --- /dev/null +++ b/src/widgets/Wizard.tsx @@ -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 ( +
+ + +
+ ); +} + +interface PropsType extends React.HTMLProps { + 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 ( + + {Content({ prev, next, currentPage, totalPages: pages.length })} + + ); +} +