Refactor based on code review. Improved typing of useCookie hook. Added duration/delay to control length of toast.

This commit is contained in:
Dan Ditomaso
2025-01-14 21:51:06 -05:00
parent 07d4204e86
commit 7884991ac6
6 changed files with 221 additions and 143 deletions
+36 -19
View File
@@ -1,35 +1,52 @@
import React from "react";
import Cookies, { type CookieAttributes } from "js-cookie";
import { useCallback, useState } from "react";
type Cookie<T> = [
T | undefined,
(value: T, options?: CookieAttributes) => void,
() => void,
];
interface CookieHookResult<T> {
value: T | undefined;
setCookie: (value: T, options?: CookieAttributes) => void;
removeCookie: () => void;
}
const useCookie = <T>(
function useCookie<T extends object>(
cookieName: string,
initialValue?: T,
): Cookie<T> => {
const [cookieValue, setCookieValue] = React.useState<T | undefined>(() => {
const cookie = Cookies.get(cookieName);
return cookie ? (JSON.parse(cookie) as T) : initialValue;
): CookieHookResult<T> {
const [cookieValue, setCookieValue] = useState<T | undefined>(() => {
try {
const cookie = Cookies.get(cookieName);
return cookie ? (JSON.parse(cookie) as T) : initialValue;
} catch (error) {
console.error(`Error parsing cookie ${cookieName}:`, error);
return initialValue;
}
});
const setCookie = React.useCallback(
const setCookie = useCallback(
(value: T, options?: CookieAttributes) => {
Cookies.set(cookieName, JSON.stringify(value), options);
setCookieValue(value);
try {
Cookies.set(cookieName, JSON.stringify(value), options);
setCookieValue(value);
} catch (error) {
console.error(`Error setting cookie ${cookieName}:`, error);
}
},
[cookieName],
);
const removeCookie = React.useCallback(() => {
Cookies.remove(cookieName);
setCookieValue(undefined);
const removeCookie = useCallback(() => {
try {
Cookies.remove(cookieName);
setCookieValue(undefined);
} catch (error) {
console.error(`Error removing cookie ${cookieName}:`, error);
}
}, [cookieName]);
return [cookieValue, setCookie, removeCookie];
};
return {
value: cookieValue,
setCookie,
removeCookie,
};
}
export default useCookie;
+100 -69
View File
@@ -1,14 +1,14 @@
import { useEffect, useCallback } from 'react';
import { useToast } from './useToast';
import useCookie from './useCookie';
import type { CookieAttributes } from 'js-cookie';
import { Button } from '@app/components/UI/Button';
import { Button } from "@app/components/UI/Button";
import type { CookieAttributes } from "js-cookie";
import { useCallback, useEffect, useRef } from "react";
import useCookie from "./useCookie";
import { useToast } from "./useToast";
interface UseBackupReminderOptions {
suppressDays?: number;
message?: string;
reminderInDays?: number;
message: string;
onAccept?: () => void | Promise<void>;
cookieName?: string;
enabled: boolean;
cookieOptions?: CookieAttributes;
}
@@ -17,73 +17,104 @@ interface ReminderState {
lastShown: string;
}
const TOAST_DELAY = 10000;
const TOAST_APPEAR_DELAY = 10_000 // 10 seconds;
const TOAST_DURATION = 30_000 // 30 seconds;:
// remind user in 1 year to backup keys again, if they accept the reminder;
const ON_ACCEPT_REMINDER_DAYS = 365
function isReminderExpired(lastShown: string): boolean {
const lastShownDate = new Date(lastShown);
const now = new Date();
const daysSinceLastShown =
(now.getTime() - lastShownDate.getTime()) / (1000 * 60 * 60 * 24);
return daysSinceLastShown >= 7;
}
export function useBackupReminder({
suppressDays = 365,
message = "It's time to back up your key data. Would you like to do this now?",
reminderInDays = 7,
enabled,
message,
onAccept = () => { },
cookieName = "backup_reminder_state",
cookieOptions = {},
}: UseBackupReminderOptions = {}) {
cookieOptions,
}: UseBackupReminderOptions) {
const { toast } = useToast();
const toastShownRef = useRef(false);
const { value: reminderCookie, setCookie } =
useCookie<ReminderState>("key_backup_reminder");
const [reminderState, setReminderState, resetReminderState] = useCookie<ReminderState>(cookieName);
const suppressReminder = useCallback(
(days: number) => {
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + days);
const suppressReminder = useCallback(() => {
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + suppressDays);
setReminderState(
{
suppressed: true,
lastShown: new Date().toISOString(),
},
{
...cookieOptions,
expires: expiryDate,
}
);
}, [setReminderState, suppressDays, cookieOptions]);
setCookie(
{
suppressed: true,
lastShown: new Date().toISOString(),
},
{ ...cookieOptions, expires: expiryDate },
);
},
[setCookie, cookieOptions],
);
useEffect(() => {
if (!reminderState) {
setTimeout(() => {
const { dismiss: dimissToast } = toast({
title: "Backup Reminder",
description: message,
action: (
<div className="flex gap-2">
<Button
type="button"
variant={"default"}
onClick={async () => {
await onAccept();
dimissToast()
suppressReminder();
}}
>
Back up now
</Button>
<Button
type="button"
variant={"outline"}
onClick={() => {
dimissToast();
suppressReminder();
}}
>
Remind me later
</Button>
</div>
),
});
}, TOAST_DELAY);
}
}, [reminderState]);
if (!enabled || toastShownRef.current) return;
return {
resetReminder: resetReminderState
};
}
const shouldShowReminder =
!reminderCookie?.suppressed ||
isReminderExpired(reminderCookie.lastShown);
if (!shouldShowReminder) return;
toastShownRef.current = true;
const { dismiss } = toast(
{
title: "Backup Reminder",
duration: TOAST_DURATION,
delay: TOAST_APPEAR_DELAY,
description: message,
action: (
<div className="flex gap-2">
<Button
type="button"
variant="default"
onClick={() => {
onAccept();
dismiss();
suppressReminder(ON_ACCEPT_REMINDER_DAYS);
}}
>
Back up now
</Button>
<Button
type="button"
variant="outline"
onClick={() => {
dismiss();
suppressReminder(reminderInDays);
}}
>
Remind me in {reminderInDays} days
</Button>
</div>
),
},
);
return () => {
if (!toastShownRef.current) {
dismiss();
}
};
}, [
enabled,
message,
onAccept,
reminderInDays,
suppressReminder,
toast,
reminderCookie,
]);
}
+15 -12
View File
@@ -10,6 +10,7 @@ type ToasterToast = ToastProps & {
title?: ReactNode;
description?: ReactNode;
action?: ToastActionElement;
delay?: number;
};
const actionTypes = {
@@ -137,7 +138,7 @@ function dispatch(action: Action) {
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
function toast({ delay = 0, ...props }: Toast) {
const id = genId();
const update = (props: ToasterToast) =>
@@ -147,17 +148,19 @@ function toast({ ...props }: Toast) {
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
setTimeout(() => {
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
},
},
},
});
});
}, delay);
return {
id: id,
@@ -190,4 +193,4 @@ function useToast() {
};
}
export { toast, useToast };
export { toast, useToast };