diff --git a/package.json b/package.json
index f55a056..3e0d90d 100644
--- a/package.json
+++ b/package.json
@@ -29,8 +29,7 @@
"react-error-boundary": "^3.0.2",
"react-hook-form": "^6.10.1",
"react-icons": "^4.1.0",
- "react-query": "^2.26.2",
- "react-query-devtools": "^2.6.3",
+ "react-query": "^3.2.0",
"react-router": "^6.0.0-beta.0",
"react-router-dom": "^6.0.0-beta.0",
"react-scripts": "4.0.0",
diff --git a/src/api/checkmarks.js b/src/api/checkmarks.js
index 5753d6d..59011d0 100644
--- a/src/api/checkmarks.js
+++ b/src/api/checkmarks.js
@@ -1,4 +1,4 @@
-import { useMutation, useQueryCache, useQuery } from 'react-query';
+import { useMutation, useQueryClient, useQuery } from 'react-query';
import { useFirebase } from 'context/firebase-context';
import { useAuth } from 'context/auth-context';
import { EMPTY } from 'data/constants';
@@ -6,12 +6,12 @@ import { EMPTY } from 'data/constants';
/**
* Returns a function that adds a checkmark
*/
-export function useAddCheckmark() {
+export function useAddCheckmarkMutation() {
const { db } = useFirebase();
const { user } = useAuth();
- const cache = useQueryCache();
+ const queryClient = useQueryClient();
- return useMutation(
+ const addCheckmarkMutation = useMutation(
(checkmark) => {
// Get checkmark ref in the database
const newCheckmarkRef = db.ref(`checkmarks/${user.uid}`).push();
@@ -27,25 +27,27 @@ export function useAddCheckmark() {
// When mutate is called:
onMutate: (checkmark) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
- cache.cancelQueries('checkmarks');
+ queryClient.cancelQueries('checkmarks');
// Snapshot previous values
- const previousCheckmarks = cache.getQueryData('checkmarks');
+ const previousCheckmarks = queryClient.getQueryData('checkmarks');
// Optimistically add new checkmark
- cache.setQueryData('checkmarks', (old) => [...old, checkmark]);
+ queryClient.setQueryData('checkmarks', (old) => [...old, checkmark]);
// Return a context object with the snapshotted value
return { previousCheckmarks };
},
// If the mutation fails, use the context returned from onMutate to roll back
onError: (error, newCheckmark, context) => {
- cache.setQueryData('checkmarks', context.previousCheckmarks);
+ queryClient.setQueryData('checkmarks', context.previousCheckmarks);
},
// Always refetch after error or success:
- onSettled: () => cache.invalidateQueries('checkmarks'),
+ onSettled: () => queryClient.invalidateQueries('checkmarks'),
}
);
+
+ return addCheckmarkMutation;
}
/**
@@ -55,7 +57,10 @@ export function useFetchCheckmarks() {
const { db } = useFirebase();
const { user } = useAuth();
- return () => {
+ /**
+ * Fetches checkmarks all the user's checkmarks from the database.
+ */
+ const fetchCheckmarks = () => {
// Get all the user's checkmarks from the database
return db
.ref(`checkmarks/${user.uid}`)
@@ -76,6 +81,8 @@ export function useFetchCheckmarks() {
return checkmarks;
});
};
+
+ return fetchCheckmarks;
}
/**
@@ -83,13 +90,15 @@ export function useFetchCheckmarks() {
*
* Returns a react-query query, that fetches the checkmarks using `fetchCheckmark`
*/
-export function useCheckmarks() {
+export function useCheckmarksQuery() {
const fetchCheckmarks = useFetchCheckmarks();
- return useQuery('checkmarks', fetchCheckmarks, {
+ const checkmarksQuery = useQuery('checkmarks', fetchCheckmarks, {
initialData: [],
initialStale: true,
});
+
+ return checkmarksQuery;
}
/**
@@ -99,12 +108,12 @@ export function useCheckmarks() {
* The mutation has to be called with checkmark id.
*
*/
-export function useDeleteCheckmark() {
+export function useDeleteCheckmarkMutation() {
const { db } = useFirebase();
const { user } = useAuth();
- const cache = useQueryCache();
+ const queryClient = useQueryClient();
- return useMutation(
+ const deleteCheckmarkMutation = useMutation(
(checkmarkId) => {
const checkmarkRef = db.ref(`checkmarks/${user.uid}/${checkmarkId}`);
// Remove the checkmark in the database
@@ -114,13 +123,13 @@ export function useDeleteCheckmark() {
// When mutate is called:
onMutate: (checkmarkId) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
- cache.cancelQueries('checkmarks');
+ queryClient.cancelQueries('checkmarks');
// Snapshot previous values
- const previousCheckmarks = cache.getQueryData('checkmarks');
+ const previousCheckmarks = queryClient.getQueryData('checkmarks');
- // Optimistically remove the checkmark from cache
- cache.setQueryData('checkmarks', (old) =>
+ // Optimistically remove the checkmark from queryClient
+ queryClient.setQueryData('checkmarks', (old) =>
old.filter((checkmark) => checkmark.id !== checkmarkId)
);
@@ -129,12 +138,14 @@ export function useDeleteCheckmark() {
},
// If the mutation fails, use the context returned from onMutate to roll back
onError: (error, checkmarkId, context) => {
- cache.setQueryData('checkmarks', context.previousCheckmarks);
+ queryClient.setQueryData('checkmarks', context.previousCheckmarks);
},
// Always refetch after error or success:
- onSettled: () => cache.invalidateQueries('checkmarks'),
+ onSettled: () => queryClient.invalidateQueries('checkmarks'),
}
);
+
+ return deleteCheckmarkMutation;
}
/**
@@ -143,12 +154,12 @@ export function useDeleteCheckmark() {
* Returns a react-query mutation, that updates the checkmark value.
* The mutation has to be called with checkmark object.
*/
-export function useUpdateCheckmarkValue() {
+export function useUpdateCheckmarkMutation() {
const { db } = useFirebase();
const { user } = useAuth();
- const cache = useQueryCache();
+ const queryClient = useQueryClient();
- return useMutation(
+ const updateCheckmarkMutation = useMutation(
(checkmark) => {
// Get checkmark database ref
const checkmarkRef = db.ref(`checkmarks/${user.uid}/${checkmark.id}`);
@@ -164,13 +175,13 @@ export function useUpdateCheckmarkValue() {
// When mutate is called:
onMutate: (newCheckmark) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
- cache.cancelQueries('checkmarks');
+ queryClient.cancelQueries('checkmarks');
// Snapshot previous values
- const previousCheckmarks = cache.getQueryData('checkmarks');
+ const previousCheckmarks = queryClient.getQueryData('checkmarks');
// Optimistically update to the new checkmark value
- cache.setQueryData('checkmarks', (old) =>
+ queryClient.setQueryData('checkmarks', (old) =>
old.map((checkmark) => {
if (checkmark.id === newCheckmark.id) {
return {
@@ -188,20 +199,27 @@ export function useUpdateCheckmarkValue() {
},
// If the mutation fails, use the context returned from onMutate to roll back
onError: (error, newCheckmark, context) => {
- cache.setQueryData('checkmarks', context.previousCheckmarks);
+ queryClient.setQueryData('checkmarks', context.previousCheckmarks);
},
// Always refetch after error or success:
- onSettled: () => cache.invalidateQueries('checkmarks'),
+ onSettled: () => queryClient.invalidateQueries('checkmarks'),
}
);
-}
-export function useUpdateCheckmarkInDb() {
- const [addCheckmark] = useAddCheckmark();
- const [updateCheckmarkValue] = useUpdateCheckmarkValue();
- const [deleteCheckmarkById] = useDeleteCheckmark();
+ return updateCheckmarkMutation;
+}
- return ({ checkmarkId, habitId, value, date }) => {
+export function useUpdateCheckmarkInDbMutate() {
+ const addCheckmark = useAddCheckmarkMutation();
+ const updateCheckmark = useUpdateCheckmarkMutation();
+ const deleteCheckmark = useDeleteCheckmarkMutation();
+
+ const updateCheckmarkInDbMutation = ({
+ checkmarkId,
+ habitId,
+ value,
+ date,
+ }) => {
// Checkmark id is falsy so the checkmark doesn't exists.
// A new checkmark should be added.
const shouldAdd = !checkmarkId;
@@ -216,15 +234,21 @@ export function useUpdateCheckmarkInDb() {
// Update
if (shouldUpdate) {
- return updateCheckmarkValue({ id: checkmarkId, value });
+ return updateCheckmark.mutate({ id: checkmarkId, value });
// Delete
} else if (shouldDelete) {
- return deleteCheckmarkById(checkmarkId);
+ return deleteCheckmark.mutate(checkmarkId);
// Add
} else if (shouldAdd) {
- return addCheckmark({ habitId, date, value });
+ return addCheckmark.mutate({ habitId, date, value });
+ } else {
+ throw new Error(
+ 'Unhandled case when updating the checkmark in the database.'
+ );
}
};
+
+ return updateCheckmarkInDbMutation;
}
diff --git a/src/api/habits.js b/src/api/habits.js
index 9804191..496b31d 100644
--- a/src/api/habits.js
+++ b/src/api/habits.js
@@ -1,19 +1,19 @@
import { useAuth } from 'context/auth-context';
import { useFirebase } from 'context/firebase-context';
-import { useQueryCache, useQuery, useMutation } from 'react-query';
+import { useQueryClient, useQuery, useMutation } from 'react-query';
/**
- * Use add habit hook
- *
+ * Use add habit mutation
+ *
* @returns a react-query mutation that adds a new habit to the database.
* The mutation takes as an argument a new habit object.
*/
-export function useAddHabit() {
+export function useAddHabitMutation() {
const { db } = useFirebase();
const { user } = useAuth();
- const cache = useQueryCache();
+ const queryClient = useQueryClient();
- return useMutation(
+ const addHabitMutation = useMutation(
(habit) => {
const { name, description, frequency, position } = habit;
@@ -30,25 +30,27 @@ export function useAddHabit() {
});
},
{
- onSuccess: () => cache.invalidateQueries('habits'),
+ onSuccess: () => queryClient.invalidateQueries('habits'),
}
);
+
+ return addHabitMutation;
}
/**
* Use delete habit hook
- *
+ *
* @returns a react-query mutation that deletes the habit and all the checkmarks
* associated with it from the database.
- *
+ *
* The mutation takes as an argument habit id.
*/
-export function useDeleteHabit() {
+export function useDeleteHabitMutationMutation() {
const { db } = useFirebase();
const { user } = useAuth();
- const cache = useQueryCache();
+ const queryClient = useQueryClient();
- return useMutation(
+ const deleteHabitMutation = useMutation(
async (habitId) => {
// When deleting the habit we have to delete both the habit
// and all the habit's checkmarks
@@ -82,20 +84,20 @@ export function useDeleteHabit() {
// When mutate is called:
onMutate: async (habitId) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
- await cache.cancelQueries('habits');
- await cache.cancelQueries('checkmarks');
+ await queryClient.cancelQueries('habits');
+ await queryClient.cancelQueries('checkmarks');
// Snapshot previous values
- const previousHabits = cache.getQueryData('habits');
- const previousCheckmarks = cache.getQueryData('checkmarks');
+ const previousHabits = queryClient.getQueryData('habits');
+ const previousCheckmarks = queryClient.getQueryData('checkmarks');
- // Optimistically remove the habit from cache
- cache.setQueryData('habits', (old) =>
+ // Optimistically remove the habit from queryClient
+ queryClient.setQueryData('habits', (old) =>
old.filter((habit) => habit.id !== habitId)
);
- // Optimistically remove the habit's checkmarks from cache
- cache.setQueryData('checkmarks', (old) =>
+ // Optimistically remove the habit's checkmarks from queryClient
+ queryClient.setQueryData('checkmarks', (old) =>
old.filter((checkmark) => checkmark.habitId !== habitId)
);
@@ -104,28 +106,35 @@ export function useDeleteHabit() {
},
// If the mutation fails, use the context returned from onMutate to roll back
onError: (error, habitId, context) => {
- cache.setQueryData('habits', context.previousHabits);
- cache.setQueryData('checkmarks', context.previousCheckmarks);
+ queryClient.setQueryData('habits', context.previousHabits);
+ queryClient.setQueryData('checkmarks', context.previousCheckmarks);
},
// Always refetch after error or success:
- onSettled: () => {
- cache.invalidateQueries('habits');
- cache.invalidateQueries('checkmarks');
- }
+ onSettled: () => {
+ queryClient.invalidateQueries('habits');
+ queryClient.invalidateQueries('checkmarks');
+ },
}
);
+
+ return deleteHabitMutation;
}
/**
* Use fetch habit by id hook
- *
+ *
* @returns a function that fetches a habit by id from the database.
*/
-export function useFetchHabitById() {
+export function useFetchHabit() {
const { db } = useFirebase();
const { user } = useAuth();
- return (key, { id }) => {
+ /**
+ * Fetch habit
+ *
+ * @param {string} id - ID of the habit to fetch
+ */
+ const fetchHabit = (id) => {
// Get habit database ref
const habitRef = db.ref(`habits/${user.uid}/${id}`);
@@ -144,34 +153,38 @@ export function useFetchHabitById() {
}
});
};
+
+ return fetchHabit;
}
/**
* Use habit by id hook
- *
+ *
* @param {string} habitId
- *
- * @returns a react-query query that fetches the habit by id, using `fetchHabitById`
+ *
+ * @returns a react-query query that fetches the habit by id, using `fetchHabit`
*/
-export function useHabitById(habitId) {
- const fetchHabitById = useFetchHabitById();
+export function useHabitQuery(id) {
+ const fetchHabit = useFetchHabit();
- return useQuery(habitId && ['habit', { id: habitId }], fetchHabitById, {
- enabled: habitId !== null,
+ const habitQuery = useQuery(id && ['habit', id], () => fetchHabit(id), {
+ enabled: id !== null,
});
+
+ return habitQuery;
}
/**
* Use habits hook
- *
+ *
* @returns react-query query for that fetches all the user's habits from
* the database.
*/
-export function useHabits() {
+export function useHabitsQuery() {
const { db } = useFirebase();
const { user } = useAuth();
- return useQuery('habits', () => {
+ const habitsQuery = useQuery('habits', () => {
// Get all the user's habits from the database
return db
.ref(`habits/${user.uid}`)
@@ -192,20 +205,22 @@ export function useHabits() {
return fetchedHabits;
});
});
+
+ return habitsQuery;
}
/**
* Use update habit hook
- *
+ *
* @returns a react-query mutation that updates the habit in the database.
* The mutation takes as an argument habit object.
*/
-export function useUpdateHabit() {
+export function useUpdateHabitMutation() {
const { db } = useFirebase();
const { user } = useAuth();
- const cache = useQueryCache();
+ const queryClient = useQueryClient();
- return useMutation(
+ const updateHabitMutation = useMutation(
(habit) => {
const { id, name, description, frequency } = habit;
@@ -227,10 +242,10 @@ export function useUpdateHabit() {
{
// When mutate is called:
onMutate: (habit) => {
- const previousHabit = cache.getQueryData(['habit', { id: habit.id }]);
+ const previousHabit = queryClient.getQueryData(['habit', habit.id]);
// Snapshot previous values
- cache.setQueryData(['habit', { id: habit.id }], (old) => ({
+ queryClient.setQueryData(['habit', habit.id], (old) => ({
...old,
...habit,
}));
@@ -240,13 +255,15 @@ export function useUpdateHabit() {
},
// If the mutation fails, use the context returned from onMutate to roll back
onError: (error, newCheckmark, context) => {
- cache.setQueryData('habits', context.previousCheckmarks);
+ queryClient.setQueryData('habits', context.previousCheckmarks);
},
// Always refetch after error or success:
onSuccess: async (habit) => {
- cache.refetchQueries('habits');
- await cache.refetchQueries(['habit', { id: habit.id }]);
+ queryClient.refetchQueries('habits');
+ await queryClient.refetchQueries(['habit', habit.id]);
},
}
);
+
+ return updateHabitMutation;
}
diff --git a/src/api/user-data.js b/src/api/user-data.js
index 51db0dc..4d8e32b 100644
--- a/src/api/user-data.js
+++ b/src/api/user-data.js
@@ -1,5 +1,6 @@
import { useFirebase } from 'context/firebase-context';
import { useAuth } from 'context/auth-context';
+import { useQueryClient } from 'react-query';
/**
* Use update performance goal hook
@@ -40,6 +41,7 @@ export function useUpdateLocaleCode() {
export function useDeleteUserData() {
const { user } = useAuth();
const { db } = useFirebase();
+ const queryClient = useQueryClient();
return () => {
const updates = {};
@@ -48,6 +50,33 @@ export function useDeleteUserData() {
updates[`checkmarks/${user.uid}`] = null;
updates[`users/${user.uid}`] = null;
+ return db.ref().update(updates).then(() => {
+ queryClient.invalidateQueries();
+ });
+ };
+}
+
+/**
+ * Returns a function that updates the user data in the database.
+ */
+export function useUpdateUserData() {
+ const { user } = useAuth();
+ const { db } = useFirebase();
+
+ /**
+ * Updates the user data in the database.
+ *
+ * @param { {checkmarks: string[]} }
+ */
+ const updateUserData = ({ checkmarks, habits, settings }) => {
+ const updates = {};
+
+ if (checkmarks) updates[`checkmarks/${user.uid}`] = checkmarks;
+ if (habits) updates[`habits/${user.uid}`] = habits;
+ if (settings) updates[`users/${user.uid}`] = settings;
+
return db.ref().update(updates);
};
+
+ return updateUserData;
}
diff --git a/src/app/authenticated-app.js b/src/app/authenticated-app.js
index 50b3861..66badfa 100644
--- a/src/app/authenticated-app.js
+++ b/src/app/authenticated-app.js
@@ -8,11 +8,12 @@ import { EditHabitScreen } from 'screens/edit-habit';
import { ManageHabitsScreen } from 'screens/manage-habits';
import { NotFoundScreen } from 'screens/not-found';
import { UserSettingsScreen } from 'screens/user-settings';
-import { useUpdateLocaleCode } from 'api/user-data';
+import { useUpdateLocaleCode, useUpdateUserData } from 'api/user-data';
import { FullPageErrorFallback, ErrorFallback } from 'components/lib';
import { LocaleSelect } from 'components/locale-select';
import { useAuth } from 'context/auth-context';
import { useDialog } from 'context/dialog-context';
+import { useQueryClient } from 'react-query';
import { Divider, List, Toolbar, Typography } from '@material-ui/core';
import {
Add as AddIcon,
@@ -30,21 +31,72 @@ import {
SidebarLink,
} from 'layout/authenticated-layout';
import { GithubRepoLink } from 'components/github-repo-link';
+import { useDeleteUserData } from 'api/user-data';
+import * as guestData from 'data/guest';
/**
* Authenticated App
*/
function AuthenticatedApp() {
- const { signOut } = useAuth();
+ const queryClient = useQueryClient();
+ const { user, signOut } = useAuth();
const { openDialog } = useDialog();
const t = useTranslation();
+ const deleteUserData = useDeleteUserData();
+ const updateUserData = useUpdateUserData();
+
+ /**
+ * Initializing the data when user is Anonymous
+ *
+ * When user logs in as guest we provide him with some dummy data. This data
+ * is immediately stored in the cache and updated in user's data point in the database.
+ *
+ * This will be handy when creating an account by linking this anonymous account.
+ */
+ React.useEffect(() => {
+ /**
+ * Check if user is anonymous and if they have habits and checkmarks in the cache.
+ * If they've already have the data in cache it means that it has already been initialized
+ * and there is no need to do it again.
+ */
+
+ if (user.isAnonymous) {
+ const { habits, dbHabits, checkmarks, dbCheckmarks } = guestData;
+
+ // Set data in the cache
+ queryClient.setQueryData('habits', habits);
+ queryClient.setQueryData('checkmarks', checkmarks);
+
+ // Set data in the database
+ updateUserData({
+ habits: dbHabits,
+ checkmarks: dbCheckmarks,
+ });
+ }
+ // Run ONLY on initial mount.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ // Logout click handler
const handleLogoutClick = () => {
openDialog({
title: t('signOutQuestion'),
description: t('signOutDescription'),
confirmText: t('signOutConfirm'),
- onConfirm: signOut,
+ onConfirm: async () => {
+ try {
+ // When signing out and user is anonymous, delete their data
+ if (user.isAnonymous) {
+ await deleteUserData();
+ await signOut();
+ } else {
+ await signOut();
+ }
+ } catch (error) {
+ console.log(error, error.message);
+ }
+ },
color: 'secondary',
});
};
diff --git a/src/app/unauthenticated-app.js b/src/app/unauthenticated-app.js
index e0d1c1e..b6d6ca7 100644
--- a/src/app/unauthenticated-app.js
+++ b/src/app/unauthenticated-app.js
@@ -16,6 +16,7 @@ import {
makeStyles,
Toolbar,
Typography,
+ Box,
} from '@material-ui/core';
import { FullPageImageBackground, LandingScreen } from 'screens/landing';
import { ResetPasswordScreen } from 'screens/reset-password';
@@ -25,6 +26,7 @@ import { LocaleSelect } from 'components/locale-select';
import { useTranslation } from 'translations';
import { GithubRepoLink } from 'components/github-repo-link';
import { DarkModeSwitch } from 'components/dark-mode-switch';
+import { Copyright } from 'components/copyright';
const useStyles = makeStyles((theme) => ({
// App
@@ -68,6 +70,7 @@ function UnathenticatedApp() {