From b26426e5cd58dd32bf4f20eb9a7a4ed4feed8b81 Mon Sep 17 00:00:00 2001
From: Mostafa Elgaafary <3999755+melgaafary@users.noreply.github.com>
Date: Fri, 28 Feb 2020 17:21:19 +0100
Subject: [PATCH 1/5] [ui] refactor: create a shared onboarding components for
userflow and admin
---
.../components/onboarding/dashboard-steps.js | 73 +++++++++++
packages/ui/components/onboarding/index.js | 40 +++++++
.../ui/components/onboarding/last-step-btn.js | 27 +++++
.../components/onboarding/no-account-steps.js | 113 ++++++++++++++++++
.../components/onboarding/tooltip-content.js | 16 +++
.../ui/components/onboarding/tour-ended.js | 0
6 files changed, 269 insertions(+)
create mode 100644 packages/ui/components/onboarding/dashboard-steps.js
create mode 100644 packages/ui/components/onboarding/index.js
create mode 100644 packages/ui/components/onboarding/last-step-btn.js
create mode 100644 packages/ui/components/onboarding/no-account-steps.js
create mode 100644 packages/ui/components/onboarding/tooltip-content.js
create mode 100644 packages/ui/components/onboarding/tour-ended.js
diff --git a/packages/ui/components/onboarding/dashboard-steps.js b/packages/ui/components/onboarding/dashboard-steps.js
new file mode 100644
index 000000000..9bba43ac0
--- /dev/null
+++ b/packages/ui/components/onboarding/dashboard-steps.js
@@ -0,0 +1,73 @@
+import React from 'react';
+import ContentBox from './tooltip-content';
+
+export const ONBOARDING_SETTINGS_STEP = {
+ selector: '.settings-menu',
+ content: ,
+ className: 'settings-menu',
+};
+export const ONBOARDING_SETTINGS_FORM_STEP = {
+ selector: '.settings-form',
+ content: (
+
+ ),
+ className: 'settings-form',
+ redirect: '/settings/company',
+ action: () => {}, // for some reason this is needed for inputs to work
+};
+export const ONBOARDING_SETTINGS_SAVE_STEP = {
+ selector: '.settings-save-btn',
+ content: (
+
+ ),
+ className: 'settings-save-btn',
+ redirect: '/settings/company',
+};
+export const ONBOARDING_INTEGRATIONS_STEP = {
+ selector: '.integrations',
+ content: (
+
+ ),
+ className: 'integrations',
+};
+export const ONBOARDING_DATA_REQUESTS_STEP = {
+ selector: '.data-requests',
+ content: (
+
+ ),
+ className: 'data-requests',
+ redirect: '/integrations',
+};
+export const ONBOARDING_CONNECT_TRELLO_STEP = {
+ selector: '.trello-connect',
+ content: (
+
+ ),
+ className: 'trello-connect',
+ redirect: '/integrations/requests/trello/connect',
+};
+export const ONBOARDING_TRELLO_CREATE_BOARDS_STEP = {
+ selector: '.trello-create-boards',
+ content: (
+
+ ),
+ className: 'trello-create-boards',
+ redirect: '/integrations/requests/trello',
+};
+export const ONBOARDING_VIEW_DEPLOY = {
+ selector: '.deploy-view',
+ content: (
+
+ ),
+ className: 'deploy-view',
+ redirect: '/deploy/new',
+};
+
+export const ONBOARDING_DEPLOY = {
+ selector: '.deploy',
+ content: (
+
+ ),
+ className: 'deploy',
+ redirect: '/deploy/new',
+};
diff --git a/packages/ui/components/onboarding/index.js b/packages/ui/components/onboarding/index.js
new file mode 100644
index 000000000..907b34c17
--- /dev/null
+++ b/packages/ui/components/onboarding/index.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {Box} from 'grommet';
+import Tour from 'reactour';
+import styled from 'styled-components';
+import ChevronRight from '../../svg/chevron-right.svg';
+import ChevronLeft from '../../svg/chevron-left.svg';
+import LastStep from './last-step-btn';
+
+const StyledTour = styled(Tour)`
+ nav {
+ flex-wrap: nowrap;
+ }
+`;
+
+export default function Onboarding({endTour, ...props}) {
+ return (
+ }
+ nextButton={
+
+
+
+ }
+ prevButton={
+
+
+
+ }
+ {...props}
+ />
+ );
+}
+
+Onboarding.propTypes = {
+ endTour: PropTypes.func.isRequired,
+};
diff --git a/packages/ui/components/onboarding/last-step-btn.js b/packages/ui/components/onboarding/last-step-btn.js
new file mode 100644
index 000000000..5618cdfe8
--- /dev/null
+++ b/packages/ui/components/onboarding/last-step-btn.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {Box} from 'grommet';
+import Button from '../button';
+import FormattedText from '../formatted-text';
+
+const LastStep = ({onClick}) => (
+
+
+ }
+ />
+
+);
+
+LastStep.propTypes = {
+ onClick: PropTypes.func.isRequired,
+};
+
+export default LastStep;
diff --git a/packages/ui/components/onboarding/no-account-steps.js b/packages/ui/components/onboarding/no-account-steps.js
new file mode 100644
index 000000000..4914f2d58
--- /dev/null
+++ b/packages/ui/components/onboarding/no-account-steps.js
@@ -0,0 +1,113 @@
+import React from 'react';
+import ContentBox from './tooltip-content';
+
+export const ONBOARDING_LANDING = {
+ selector: '.landing',
+ content: (
+
+ ),
+ className: 'landing',
+ redirect: '/',
+};
+
+export const ONBOARDING_DATA_REQUEST = {
+ selector: '.data-request',
+ content: ,
+ className: 'data-request',
+ redirect: '/',
+};
+
+export const ONBOARDING_CONTACT_SECTIION = {
+ selector: '.contact',
+ content: (
+
+ ),
+ className: 'contact',
+ redirect: '/',
+};
+
+export const ONBOARDING_REQUEST_TYPE = {
+ selector: '.request-type',
+ content: (
+
+ ),
+ className: 'request-type',
+ redirect: '/request',
+};
+
+export const ONBOARDING_EMAIL = {
+ selector: '.email',
+ content: (
+
+ ),
+ className: 'email',
+ redirect: '/request/delete',
+};
+
+export const ONBOARDING_PHONE = {
+ selector: '.phone',
+ content: (
+
+ ),
+ className: 'phone',
+ redirect: '/request/delete',
+};
+export const ONBOARDING_IDENTITY = {
+ selector: '.address',
+ content: (
+
+ ),
+ className: 'address',
+ redirect: '/request/delete',
+};
+export const ONBOARDING_CONFIRMATION = {
+ selector: '.confirmation',
+ content: (
+
+ ),
+ className: 'confirmation',
+ redirect: '/confirm-identity/unconfirmed',
+};
+export const ONBOARDING_CONFIRMED_EMAIL = {
+ selector: '.confirmed',
+ content: ,
+ className: 'confirmed',
+ redirect: '/confirm-identity/unconfirmed',
+};
+export const ONBOARDING_PENDING_EMAIL = {
+ selector: '.pending',
+ content: ,
+ className: 'pending',
+ redirect: '/confirm-identity/unconfirmed',
+};
+export const ONBOARDING_CONFIRM_EMAIL = {
+ selector: '.confirm-email',
+ content: (
+
+ ),
+ className: 'confirm-email',
+ redirect: '/confirm-identity/unconfirmed',
+};
+export const ONBOARDING_DELETE_EMAIL = {
+ selector: '.delete-email',
+ content: ,
+ className: 'delete-email',
+ redirect: '/confirm-identity/unconfirmed',
+};
+export const ONBOARDING_ADD_MORE = {
+ selector: '.more',
+ content: ,
+ className: 'more',
+ redirect: '/confirm-identity/unconfirmed',
+};
+export const ONBOARDING_CONFIRMED = {
+ selector: '.wait',
+ content: (
+
+ ),
+ className: 'wait',
+ redirect: '/wait/delete',
+};
diff --git a/packages/ui/components/onboarding/tooltip-content.js b/packages/ui/components/onboarding/tooltip-content.js
new file mode 100644
index 000000000..e872053f9
--- /dev/null
+++ b/packages/ui/components/onboarding/tooltip-content.js
@@ -0,0 +1,16 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {Box} from 'grommet';
+import FormattedText from '../formatted-text';
+
+const ContentBox = ({text}) => (
+
+
+
+);
+
+ContentBox.propTypes = {
+ text: PropTypes.string.isRequired,
+};
+
+export default ContentBox;
diff --git a/packages/ui/components/onboarding/tour-ended.js b/packages/ui/components/onboarding/tour-ended.js
new file mode 100644
index 000000000..e69de29bb
From 0e81fdc0c24b9da9811dede735286e09faf4d5cc Mon Sep 17 00:00:00 2001
From: Mostafa Elgaafary <3999755+melgaafary@users.noreply.github.com>
Date: Fri, 28 Feb 2020 17:24:36 +0100
Subject: [PATCH 2/5] [no-account-app] feat: onboarding state for userflow
---
packages/no-account-app/src/app.js | 46 ++++-
packages/no-account-app/src/index.js | 13 +-
.../src/screens/confirm-identity.js | 57 +++++-
.../src/screens/request-form.js | 3 +
.../no-account-app/src/state/onboarding.js | 176 ++++++++++++++++++
5 files changed, 287 insertions(+), 8 deletions(-)
create mode 100644 packages/no-account-app/src/state/onboarding.js
diff --git a/packages/no-account-app/src/app.js b/packages/no-account-app/src/app.js
index d98ef9aae..a14985945 100644
--- a/packages/no-account-app/src/app.js
+++ b/packages/no-account-app/src/app.js
@@ -5,6 +5,10 @@ import {injectIntl} from 'react-intl';
import Container from '@datawallet/ui/components/data-lookup/container';
+import Onboarding from '@datawallet/ui/components/onboarding/index';
+import TourEnded from '@datawallet/ui/modals/dashboard/tour-ended';
+
+import OnboardingState from './state/onboarding';
import Theme from './state/theme';
import Company from './state/company';
@@ -23,8 +27,20 @@ const App = ({intl}) => {
const {publicData} = Company.useContainer();
const {theme} = Theme.useContainer();
const {brandSettings} = theme.global;
-
const companyName = publicData && publicData.name;
+ const {
+ onBoardingStep,
+ tourOpen,
+ nextStep,
+ prevStep,
+ pauseTour,
+ endTour,
+ tourEnded,
+ startTour,
+ openTourEndedModal,
+ closeTourEndedModal,
+ ONBOARDING_STEPS,
+ } = OnboardingState.useContainer();
return (
@@ -37,6 +53,10 @@ const App = ({intl}) => {
}}
minBoxHeight="100vh"
helpUrl={publicData && publicData.helpUrl}
+ showTour={!tourEnded && !openTourEndedModal}
+ tourEnded={tourEnded}
+ tourOpen={tourOpen}
+ startTour={() => startTour({delay: 1000})}
>
{
{companyName},
)}
/>
-
@@ -62,6 +81,29 @@ const App = ({intl}) => {
+ {openTourEndedModal && (
+ <>
+
+ >
+ )}
+ {tourOpen && (
+ nextStep({delay: 200})}
+ prevStep={() => prevStep({delay: 100})}
+ startAt={onBoardingStep}
+ goToStep={onBoardingStep}
+ endTour={endTour}
+ rounded={18}
+ scrollOffset={10}
+ />
+ )}
);
diff --git a/packages/no-account-app/src/index.js b/packages/no-account-app/src/index.js
index 21210e876..f1fdba3cf 100644
--- a/packages/no-account-app/src/index.js
+++ b/packages/no-account-app/src/index.js
@@ -9,6 +9,7 @@ import '@datawallet/ui/css/base.css';
import App from './app';
import Theme from './state/theme';
import Company from './state/company';
+import Onboarding from './state/onboarding';
function IntlApp({defaultMessages}) {
const {messages} = Intl.useContainer();
@@ -31,11 +32,13 @@ export default function Root({messages}) {
return (
-
-
-
-
-
+
+
+
+
+
+
+
);
diff --git a/packages/no-account-app/src/screens/confirm-identity.js b/packages/no-account-app/src/screens/confirm-identity.js
index 24cce41b8..7c9083189 100644
--- a/packages/no-account-app/src/screens/confirm-identity.js
+++ b/packages/no-account-app/src/screens/confirm-identity.js
@@ -18,6 +18,7 @@ import {
updateUserIdentity,
startLookupProcess,
} from '../services/data-lookup';
+import OnboardingState from '../state/onboarding';
const ConfirmIdentityContainer = ({history, match}) => {
const {id} = match.params;
@@ -47,11 +48,65 @@ const ConfirmIdentityContainer = ({history, match}) => {
status,
requestStatus,
} = request;
+ const {dummyUser} = OnboardingState.useContainer();
const fetchUser = useCallback(async () => {
+ if (dummyUser && id === 'unconfirmed') {
+ setRequest({
+ status: 'pending',
+ requestType: 'delete',
+ requestStatus: 'pending',
+ emails: [
+ {id: 'dummy@datawallet.com', status: 'confirmed'},
+ {id: 'dummy@datawallet.com', status: 'pending'},
+ ],
+ phones: [{id: '+13224324234', status: 'confirmed'}],
+ addresses: [
+ {
+ line1: 'Dummy address1',
+ line2: 'Dummy address2',
+ zip: '12345',
+ state: 'DU',
+ status: 'confirmed',
+ },
+ ],
+ identity: {
+ firstName: 'dummy first name',
+ lastName: 'dummy last name',
+ middleNames: 'dummy middle name',
+ },
+ });
+
+ return;
+ }
+ if (dummyUser && id === 'confirmed') {
+ setRequest({
+ status: 'confirmed',
+ requestType: 'delete',
+ requestStatus: 'confirmed',
+ emails: [{id: 'dummy@datawallet.com', status: 'confirmed'}],
+ phones: [{id: '+13224324234', status: 'confirmed'}],
+ addresses: [
+ {
+ line1: 'Dummy address1',
+ line2: 'Dummy address2',
+ zip: '12345',
+ state: 'DU',
+ status: 'confirmed',
+ },
+ ],
+ identity: {
+ firstName: 'dummy first name',
+ lastName: 'dummy last name',
+ middleNames: 'dummy middle name',
+ },
+ });
+
+ return;
+ }
const user = await retrieveUserData(id);
setRequest(user);
- }, [id]);
+ }, [id, dummyUser]);
useEffect(() => {
if (id) {
diff --git a/packages/no-account-app/src/screens/request-form.js b/packages/no-account-app/src/screens/request-form.js
index e396c177c..53265c7af 100644
--- a/packages/no-account-app/src/screens/request-form.js
+++ b/packages/no-account-app/src/screens/request-form.js
@@ -8,6 +8,7 @@ import RequestForm from '@datawallet/ui/screens/data-lookup/request-form';
import Back from '@datawallet/ui/components/back';
import Company from '../state/company';
+import OnboardingState from '../state/onboarding';
import {sendRequestForm} from '../services/data-lookup';
const recaptchaSiteKey = process.env.RECAPTCHA_SITE_KEY;
@@ -18,6 +19,7 @@ const RequestFormContainer = ({
match,
}) => {
const {publicData} = Company.useContainer();
+ const {dummyUser} = OnboardingState.useContainer();
const {privacyUrl} = publicData;
const {requestType} = match.params;
const [submitting, setSubmitting] = useState(false);
@@ -54,6 +56,7 @@ const RequestFormContainer = ({
modules={getAvailableModules(requestType)}
requiredIdentityPieces={minRequirements(requestType)}
recaptchaSiteKey={recaptchaSiteKey}
+ dummyUser={dummyUser}
/>
>
);
diff --git a/packages/no-account-app/src/state/onboarding.js b/packages/no-account-app/src/state/onboarding.js
new file mode 100644
index 000000000..9f9ac6972
--- /dev/null
+++ b/packages/no-account-app/src/state/onboarding.js
@@ -0,0 +1,176 @@
+import {useState, useEffect} from 'react';
+import {useHistory, useLocation} from 'react-router';
+import {createContainer} from 'unstated-next';
+import {
+ ONBOARDING_LANDING,
+ ONBOARDING_REQUEST_TYPE,
+ ONBOARDING_CONTACT_SECTIION,
+ ONBOARDING_EMAIL,
+ ONBOARDING_IDENTITY,
+ ONBOARDING_PHONE,
+ ONBOARDING_CONFIRMATION,
+ ONBOARDING_CONFIRMED_EMAIL,
+ ONBOARDING_PENDING_EMAIL,
+ ONBOARDING_ADD_MORE,
+ ONBOARDING_CONFIRM_EMAIL,
+ ONBOARDING_DELETE_EMAIL,
+ ONBOARDING_CONFIRMED,
+} from '@datawallet/ui/components/onboarding/no-account-steps';
+import {
+ ONBOARDING_STEP,
+ ONBOARDING_ENDED,
+ ONBOARDING_ENDED_MODAL,
+ ONBOARDING_TOUR_OPEN,
+} from '@datawallet/constants/onboarding';
+
+const ONBOARDING_DELAY = 1000;
+export const ONBOARDING_NEXT = 'NEXT';
+export const ONBOARDING_PREV = 'PREV';
+
+export function useOnboarding() {
+ const {push: redirect} = useHistory();
+ const {pathname} = useLocation();
+ const [tourOpen, setTourOpen] = useState(
+ localStorage.getItem(ONBOARDING_TOUR_OPEN) === 'true',
+ );
+ const localStorageStep = localStorage.getItem(ONBOARDING_STEP);
+ const [onBoardingStep, setOnBoardingStep] = useState(
+ localStorageStep ? Number(localStorageStep) : 0,
+ );
+ const [action, setAction] = useState(null);
+ const [dummyUser, setDummyUser] = useState(true);
+ const [tourEnded, setTourEnded] = useState(
+ localStorage.getItem(ONBOARDING_ENDED) === 'true',
+ );
+
+ const ONBOARDING_STEPS = [
+ ONBOARDING_LANDING,
+ ONBOARDING_CONTACT_SECTIION,
+ ONBOARDING_REQUEST_TYPE,
+ ONBOARDING_EMAIL,
+ ONBOARDING_PHONE,
+ ONBOARDING_IDENTITY,
+ ONBOARDING_CONFIRMATION,
+ ONBOARDING_CONFIRMED_EMAIL,
+ ONBOARDING_PENDING_EMAIL,
+ ONBOARDING_CONFIRM_EMAIL,
+ ONBOARDING_DELETE_EMAIL,
+ ONBOARDING_ADD_MORE,
+ ONBOARDING_CONFIRMED,
+ ];
+ const skip = () => {
+ setDummyUser(true);
+ redirect('/confirm-identity/dummy');
+ // setOnBoardingStep()
+ };
+
+ const [openTourEndedModal, setTourEndedModal] = useState(
+ localStorage.getItem(ONBOARDING_ENDED_MODAL) === 'true',
+ );
+
+ useEffect(() => {
+ localStorage.setItem(ONBOARDING_STEP, onBoardingStep);
+ }, [onBoardingStep]);
+ useEffect(() => {
+ localStorage.setItem(ONBOARDING_TOUR_OPEN, tourOpen);
+ }, [tourOpen]);
+ useEffect(() => {
+ localStorage.setItem(ONBOARDING_ENDED, tourEnded);
+ }, [tourEnded]);
+ useEffect(() => {
+ localStorage.setItem(ONBOARDING_ENDED_MODAL, openTourEndedModal);
+ }, [openTourEndedModal]);
+
+ const prevStep = ({delay}) => {
+ const prev = onBoardingStep - 1;
+ setOnBoardingStep(prev);
+ if (ONBOARDING_STEPS[prev].redirect) {
+ setTourOpen(false);
+ redirect(ONBOARDING_STEPS[prev].redirect);
+ if (delay) {
+ setTimeout(() => setTourOpen(true), delay);
+ } else {
+ setTourOpen(true);
+ }
+ }
+ setAction(ONBOARDING_PREV);
+ };
+
+ const pauseTour = () => {
+ setTourOpen(false);
+ setDummyUser(false);
+ };
+ const startTour = ({delay}) => {
+ if (
+ ONBOARDING_STEPS[onBoardingStep] &&
+ pathname !== ONBOARDING_STEPS[onBoardingStep].redirect
+ ) {
+ redirect(ONBOARDING_STEPS[onBoardingStep].redirect);
+ }
+ if (!onBoardingStep) {
+ setOnBoardingStep(0);
+ setTourEnded(false);
+ }
+
+ setDummyUser(true);
+ if (delay) {
+ setTimeout(() => setTourOpen(true), ONBOARDING_DELAY);
+ } else {
+ setTourOpen(true);
+ }
+ };
+
+ const endTour = () => {
+ setTourEnded(true);
+ setTourEndedModal(true);
+ setTourOpen(false);
+ setOnBoardingStep(null);
+ setDummyUser(false);
+ redirect('/');
+ };
+
+ const nextStep = ({delay}) => {
+ const next = onBoardingStep + 1;
+ if (next >= ONBOARDING_STEPS.length) {
+ return endTour();
+ }
+ setOnBoardingStep(next);
+ if (ONBOARDING_STEPS[next].redirect) {
+ setTourOpen(false);
+ redirect(ONBOARDING_STEPS[next].redirect);
+ if (delay) {
+ setTimeout(() => setTourOpen(true), delay);
+ } else {
+ setTourOpen(true);
+ }
+ }
+ return setAction(ONBOARDING_NEXT);
+ };
+
+ const closeTourEndedModal = () => {
+ setTourEndedModal(false);
+ };
+ return {
+ tourEnded,
+ setTourEnded,
+ onBoardingStep,
+ setOnBoardingStep,
+ tourOpen,
+ setTourOpen,
+ openTourEndedModal,
+ setTourEndedModal,
+ nextStep,
+ prevStep,
+ pauseTour,
+ startTour,
+ endTour,
+ action,
+ closeTourEndedModal,
+ skip,
+ dummyUser,
+ ONBOARDING_STEPS,
+ };
+}
+
+const Onboarding = createContainer(useOnboarding);
+export default Onboarding;
From 9680034168c8637fff284beb2142d36672ea140e Mon Sep 17 00:00:00 2001
From: Mostafa Elgaafary <3999755+melgaafary@users.noreply.github.com>
Date: Fri, 28 Feb 2020 17:30:42 +0100
Subject: [PATCH 3/5] [ui] feat: add user flow class names, adjust tour ended
modal to work for both admin and userflow
---
.../__snapshots__/storyshots.test.js.snap | 118 +++++++++---------
.../ui/components/data-lookup/confirmation.js | 13 ++
.../ui/components/data-lookup/container.js | 75 +++++++++--
.../ui/components/data-lookup/email-form.js | 2 +
.../components/data-lookup/identity-form.js | 2 +
.../ui/components/data-lookup/phone-form.js | 2 +
packages/ui/modals/dashboard/tour-ended.js | 18 +--
.../screens/data-lookup/confirm-identity.js | 12 +-
packages/ui/screens/data-lookup/home.js | 13 +-
.../data-lookup/select-request-type.js | 2 +
packages/ui/screens/data-lookup/wait.js | 3 +-
11 files changed, 182 insertions(+), 78 deletions(-)
diff --git a/packages/storybook/__snapshots__/storyshots.test.js.snap b/packages/storybook/__snapshots__/storyshots.test.js.snap
index 8e57362c5..207f48647 100644
--- a/packages/storybook/__snapshots__/storyshots.test.js.snap
+++ b/packages/storybook/__snapshots__/storyshots.test.js.snap
@@ -20150,7 +20150,7 @@ exports[`Storyshots Screens/Data Lookup Confirm Identity - Already submitted + f
}
>
(
(
background={confirmed ? '#e5b3401a' : 'transparent'}
pad={{vertical: 'medium', horizontal: 'large'}}
border={{side: 'top', color: 'dark-4'}}
+ className={
+ !confirmed
+ ? ONBOARDING_PENDING_EMAIL.className
+ : ONBOARDING_CONFIRMED_EMAIL.className
+ }
{...props}
/>
);
@@ -43,6 +54,7 @@ Row.propTypes = {
const Remove = props => (
(
const ConfirmButton = ({isSent, ...props}) => (