diff --git a/src/account/accountProfileUtils.js b/src/account/accountProfileUtils.js new file mode 100644 index 000000000..3b72d6230 --- /dev/null +++ b/src/account/accountProfileUtils.js @@ -0,0 +1,75 @@ +/* + * Copyright © 2024 Technology Matters + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { useEffect, useState } from 'react'; +import { jwtDecode } from 'jwt-decode'; +import { useNavigate } from 'react-router-dom'; +import { getToken } from 'terraso-client-shared/account/auth'; +import { useSelector } from 'terrasoApi/store'; + +const getCreatedWithService = async () => { + const token = await getToken(); + return token === undefined ? undefined : jwtDecode(token).createdWithService; +}; + +const getStoredCompletedProfile = email => { + const storedCompletedProfile = localStorage.getItem( + 'completedProfileDisplayed' + ); + try { + const parsed = JSON.parse(storedCompletedProfile); + return parsed[email]; + } catch (error) { + return false; + } +}; + +export const profileCompleted = email => { + if (!email) { + return; + } + localStorage.setItem( + 'completedProfileDisplayed', + JSON.stringify({ + [email]: true, + }) + ); +}; + +export const useCompleteProfile = () => { + const navigate = useNavigate(); + const { data: user } = useSelector(state => state.account.currentUser); + const [createdWithService, setCreatedWithService] = useState(); + + useEffect(() => { + getCreatedWithService().then(createdWithService => { + setCreatedWithService(createdWithService); + }); + }, []); + + useEffect(() => { + if (!createdWithService || !user?.email) { + return; + } + + const completedProfile = getStoredCompletedProfile(user?.email); + if (completedProfile) { + return; + } + + navigate('/account/profile/completeProfile'); + }, [createdWithService, user?.email, navigate]); +}; diff --git a/src/account/components/AccountProfile.js b/src/account/components/AccountProfile.js index cfdd805b2..3029a1b59 100644 --- a/src/account/components/AccountProfile.js +++ b/src/account/components/AccountProfile.js @@ -14,10 +14,11 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import _ from 'lodash/fp'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; +import { useParams } from 'react-router-dom'; import { fetchProfile, savePreference, @@ -25,7 +26,13 @@ import { } from 'terraso-client-shared/account/accountSlice'; import { useFetchData } from 'terraso-client-shared/store/utils'; import * as yup from 'yup'; -import { Checkbox, FormControlLabel, Grid, Typography } from '@mui/material'; +import { + Alert, + Checkbox, + FormControlLabel, + Grid, + Typography, +} from '@mui/material'; import { withProps } from 'react-hoc'; @@ -36,6 +43,7 @@ import PageHeader from 'layout/PageHeader'; import PageLoader from 'layout/PageLoader'; import LocalePickerSelect from 'localization/components/LocalePickerSelect'; import { useAnalytics } from 'monitoring/analytics'; +import { profileCompleted } from 'account/accountProfileUtils'; import AccountAvatar from './AccountAvatar'; @@ -170,6 +178,7 @@ const AccountProfile = () => { const dispatch = useDispatch(); const { trackEvent } = useAnalytics(); const { t } = useTranslation(); + const { completeProfile } = useParams(); const { data: user, fetching } = useSelector(_.get('account.profile')); useFetchData(fetchProfile); @@ -177,6 +186,13 @@ const AccountProfile = () => { useDocumentTitle(t('account.profile_document_title')); useDocumentDescription(t('account.profile_document_description')); + useEffect( + () => () => { + profileCompleted(user?.email); + }, + [user?.email] + ); + const onSave = updatedProfile => { // Save user data dispatch( @@ -223,6 +239,9 @@ const AccountProfile = () => { return ( + {completeProfile && ( + {t('account.profile_complete_message')} + )}
{ const location = useLocation(); @@ -31,6 +32,8 @@ const RequireAuth = ({ children }) => { ); const hasToken = useSelector(state => state.account.hasToken); + useCompleteProfile(); + useFetchData( useCallback( () => (hasToken && !user ? fetchUser() : null), diff --git a/src/account/components/RequireAuth.test.js b/src/account/components/RequireAuth.test.js index 0feae1e54..3cf8a0a33 100644 --- a/src/account/components/RequireAuth.test.js +++ b/src/account/components/RequireAuth.test.js @@ -17,8 +17,8 @@ import { render, screen } from 'tests/utils'; import React from 'react'; import _ from 'lodash/fp'; -import { useLocation, useParams } from 'react-router-dom'; -import { getUserEmail } from 'terraso-client-shared/account/auth'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; +import { getToken, getUserEmail } from 'terraso-client-shared/account/auth'; import * as terrasoApi from 'terraso-client-shared/terrasoApi/api'; import RequireAuth from 'account/components/RequireAuth'; @@ -29,17 +29,24 @@ jest.mock('terraso-client-shared/terrasoApi/api'); jest.mock('terraso-client-shared/account/auth', () => ({ ...jest.requireActual('terraso-client-shared/account/auth'), getUserEmail: jest.fn(), + getToken: jest.fn(), })); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useParams: jest.fn(), useLocation: jest.fn(), + useNavigate: jest.fn(), Navigate: props =>
To: {props.to}
, })); +// Payload: { "createdWithService": "google" } +const CREATED_WITH_SERVICE_TOKEN = + 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkV2l0aFNlcnZpY2UiOiJnb29nbGUifQ.aznynzRP1qRh-GVKPM_Xhi7ZhG7XuM7R6SIXNd7rfCo2bgnXJen3btm4VnpcVDalnCQPpp8e-1f7t8qlTLZu0Q'; + beforeEach(() => { global.fetch = jest.fn(); + useNavigate.mockReturnValue(jest.fn()); }); test('Auth: test redirect', async () => { @@ -186,3 +193,55 @@ test('Auth: test fetch user', async () => { expect(terrasoApi.requestGraphQL).toHaveBeenCalledTimes(2); }); + +test('Auth: Test redirect complete profile', async () => { + const navigate = jest.fn(); + useNavigate.mockReturnValue(navigate); + getToken.mockResolvedValue(CREATED_WITH_SERVICE_TOKEN); + await render( + +
+ , + { + account: { + currentUser: { + data: { + email: 'test@test.com', + }, + }, + }, + } + ); + + expect(navigate).toHaveBeenCalledWith('/account/profile/completeProfile'); +}); + +test('Auth: Avoid redirect if profile complete already displayed for user', async () => { + const navigate = jest.fn(); + useNavigate.mockReturnValue(navigate); + getToken.mockResolvedValue(CREATED_WITH_SERVICE_TOKEN); + + localStorage.setItem( + 'completedProfileDisplayed', + JSON.stringify({ + 'test@test.com': true, + }) + ); + + await render( + +
+ , + { + account: { + currentUser: { + data: { + email: 'test@test.com', + }, + }, + }, + } + ); + + expect(navigate).not.toHaveBeenCalled(); +}); diff --git a/src/localization/locales/en-US.json b/src/localization/locales/en-US.json index 579dfb8e5..313415132 100644 --- a/src/localization/locales/en-US.json +++ b/src/localization/locales/en-US.json @@ -219,6 +219,7 @@ "profile_document_title": "Account", "profile_document_description": "View and edit your Terraso profile and preferences", "profile_form_label": "Manage profile", + "profile_complete_message": "We just need a few details about you before you can get started.", "form_language_label": "Language", "form_notifications_section_label": "Email notifications", "form_notifications_section_when_label": "Notify me when:", diff --git a/src/navigation/components/Routes.js b/src/navigation/components/Routes.js index 780af7a09..d91916154 100644 --- a/src/navigation/components/Routes.js +++ b/src/navigation/components/Routes.js @@ -185,6 +185,7 @@ const paths = [ breadcrumbsLabel: 'tools.breadcrumbs_list', }), path('/account', AccountLogin, { auth: false }), + path('/account/profile/:completeProfile', AccountProfile), path('/account/profile', AccountProfile), path('/contact', ContactForm), path('/tools/story-maps', StoryMapsToolsHome, {