Check for expires compelted tutorial cookie.

This commit is contained in:
Lucid Kobold
2022-06-23 17:20:05 -05:00
parent 124ea164f5
commit 9c18730126
4 changed files with 130 additions and 22 deletions

View File

@@ -0,0 +1,22 @@
/**
* Function to convert the version string to a number tha represents the most recent major release.
* @param {string }version The version string.
* @returns {number} a number that represents the most recent major release.
*/
const versionStringToNumber = (version: string): number => {
const versionStrArr: string[] = version.split(".");
const versionArr: number[] = versionStrArr.map((str) => parseInt(str));
if (versionArr[0] === 0 && versionArr[1] === 0 && versionArr[2] > 1) {
versionArr[1] = 1;
}
const versionStr = `${versionArr[0]}` + "." + `${versionArr[1]}`;
const versionNum: number = parseFloat(versionStr);
return versionNum;
};
export default versionStringToNumber;

View File

@@ -12,10 +12,6 @@ interface TutorialProps {
} }
const Tutorial = ({ isLoading }: TutorialProps): JSX.Element => { const Tutorial = ({ isLoading }: TutorialProps): JSX.Element => {
// TODO: Add an expiration validator.
// TODO: Add a version validator that removed the completed tutorial storages when there were major changes to the tutorial.
// * The changes are tracked via env variables. The last version that user saw the tutorial is saved in storage.
return ( return (
<VStack <VStack
h="auto" h="auto"
@@ -27,6 +23,7 @@ const Tutorial = ({ isLoading }: TutorialProps): JSX.Element => {
py={4} py={4}
px={{ base: 0, sm: 2, md: 4 }} px={{ base: 0, sm: 2, md: 4 }}
bg="gray.700" bg="gray.700"
borderRadius={{ base: "", sm: "2xl" }}
> >
<TutorialHeading /> <TutorialHeading />
<TutorialAboutApp /> <TutorialAboutApp />

View File

@@ -1,12 +1,25 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { addMonths } from "date-fns"; import { addMonths, endOfDay } from "date-fns";
import versionStringToNumber from "../../../lib/versionStringToNumber";
interface StorageState { export interface StorageState {
exp: string; exp: string;
version: string; version: number;
completed: boolean; completed: boolean;
} }
const endOfToday: Date = endOfDay(new Date());
const generateExpDate = (): string => {
return endOfDay(addMonths(endOfToday, 1)).toJSON();
};
const generateVersion = (): number => {
const versionStr: string = process.env.NEXT_PUBLIC_APP_VERSION;
return versionStringToNumber(versionStr);
};
// * Storage Helpers * // // * Storage Helpers * //
const setTempStorage = (storageState: StorageState): void => { const setTempStorage = (storageState: StorageState): void => {
@@ -53,8 +66,8 @@ const tutorialSlice = createSlice({
reducers: { reducers: {
// Set temp complete // Set temp complete
setTempTutorialComplete(state: TutorialSlice) { setTempTutorialComplete(state: TutorialSlice) {
const exp: string = addMonths(new Date(), 1).toJSON(); const exp: string = generateExpDate();
const version: string = process.env.NEXT_PUBLIC_APP_VERSION.split("-")[0]; const version: number = generateVersion();
const storageState: StorageState = { const storageState: StorageState = {
exp, exp,
version, version,
@@ -67,8 +80,8 @@ const tutorialSlice = createSlice({
}, },
// Set completed (remember) // Set completed (remember)
setTutorialCompleted(state: TutorialSlice) { setTutorialCompleted(state: TutorialSlice) {
const exp: string = addMonths(new Date(), 1).toJSON(); const exp: string = generateExpDate();
const version: string = process.env.NEXT_PUBLIC_APP_VERSION.split("-")[0]; const version: number = generateVersion();
const storageState: StorageState = { const storageState: StorageState = {
exp, exp,
version, version,

View File

@@ -3,29 +3,38 @@ import { Provider } from "react-redux";
import { store } from "../app/store"; import { store } from "../app/store";
import { useAppDispatch, useAppSelector } from "../app/hooks"; import { useAppDispatch, useAppSelector } from "../app/hooks";
import { updateLoading } from "../features/calender"; import { updateLoading } from "../features/calender";
import { getAndSetTutorial } from "../features/tutorial"; import {
clearTutorialCompleted,
getAndSetTutorial,
StorageState
} from "../features/tutorial";
import { Box } from "@chakra-ui/react"; import { Box } from "@chakra-ui/react";
import { format } from "date-fns"; import { format, isAfter, isBefore, startOfDay } from "date-fns";
import Calender from "../components/calender"; import Calender from "../components/calender";
import Tutorial from "../components/tutorial"; import Tutorial from "../components/tutorial";
import LoadingOverlay from "../components/loading/LoadingOverlay"; import LoadingOverlay from "../components/loading/LoadingOverlay";
import versionStringToNumber from "../../lib/versionStringToNumber";
const IndexPage = (): JSX.Element => { const IndexPage = (): JSX.Element => {
const isLoading = useAppSelector((state) => state.calender.isLoading); const currDateStr: string = useAppSelector(
const completedTutorial = useAppSelector( (state) => state.calender.currDate
);
const isLoading: boolean = useAppSelector(
(state) => state.calender.isLoading
);
const currDateObj: Date = new Date(currDateStr);
// * Tutorial * //
const completedTutorial: boolean = useAppSelector(
(state) => state.tutorial.completedTutorial (state) => state.tutorial.completedTutorial
); );
const tutorialCompletionInfo = useAppSelector( const tutorialCompletionInfo: StorageState = useAppSelector(
(state) => state.tutorial.storageState (state) => state.tutorial.storageState
); );
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const currDate = useRef<UpdateCalenderPropsDateLayout>({ // Get the completed tutorial cookie or have it set to false.
year: parseInt(format(new Date(), "y")),
month: parseInt(format(new Date(), "M")),
day: parseInt(format(new Date(), "d"))
});
useEffect(() => { useEffect(() => {
if (completedTutorial === null && tutorialCompletionInfo === null) { if (completedTutorial === null && tutorialCompletionInfo === null) {
dispatch(getAndSetTutorial()); dispatch(getAndSetTutorial());
@@ -36,6 +45,73 @@ const IndexPage = (): JSX.Element => {
} }
}, [completedTutorial, dispatch, tutorialCompletionInfo]); }, [completedTutorial, dispatch, tutorialCompletionInfo]);
// Checking the exp date of completed tutorial cookie and if the version completed is out of date.
useEffect(() => {
if (tutorialCompletionInfo !== null) {
const { exp, version } = tutorialCompletionInfo;
const currDateObj: Date = new Date(currDateStr);
/**
* Checks if the completed tutorial cookie is expired.
* @param {Date} expDate the date when the completed tutorital cookie expires.
* @returns {boolean} true if the cookie is expired, false is otherwise.
*/
const expDateValidator = (expDate: Date): boolean => {
let flag = false;
const startOfToday = startOfDay(currDateObj);
if (isAfter(startOfToday, expDate)) {
flag = true;
}
return flag;
};
/**
* Checks if the last time the completed tutorial is before an update to the tutorial.
* @param {number} lastVersionCompleted the version number the tutorial was last completed.
* @returns {boolean} true if the version given is before the changes to the tutorial, false otherwise.
*/
const versionValidator = (lastVersionCompleted: number): boolean => {
const lastVersionWithChangeStr: string =
process.env.NEXT_PUBLIC_NEW_TUTORIAL_VERSION;
const lastVersionWithChange: number = versionStringToNumber(
lastVersionWithChangeStr
);
const lastUpdatedDateStr: string =
process.env.NEXT_PUBLIC_LAST_UPDATE_DATE;
const lastUpdatedDate: Date = new Date(lastUpdatedDateStr);
let flag = false;
if (
lastVersionCompleted < lastVersionWithChange ||
(lastVersionCompleted === lastVersionWithChange &&
isBefore(currDateObj, lastUpdatedDate))
) {
flag = true;
console.error("Completed cookie version is out of date.");
}
return flag;
};
if (expDateValidator(new Date(exp)) || versionValidator(version)) {
console.warn("Version outdated or cookie expired.");
dispatch(clearTutorialCompleted());
}
}
}, [currDateStr, dispatch, tutorialCompletionInfo]);
// Current date
const currDate = useRef<UpdateCalenderPropsDateLayout>({
year: parseInt(format(currDateObj, "y")),
month: parseInt(format(currDateObj, "M")),
day: parseInt(format(currDateObj, "d"))
});
return ( return (
<Box textAlign="center" w="100%" h="auto" pt="50px" minWidth="min-content"> <Box textAlign="center" w="100%" h="auto" pt="50px" minWidth="min-content">
<Provider store={store}> <Provider store={store}>