-
Notifications
You must be signed in to change notification settings - Fork 245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: streak warning prompt for timezone #4071
Changes from 7 commits
461168e
bc97f48
2effc23
80e94c0
f2f94c3
d9fca98
0305674
2a6fead
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,6 +34,7 @@ export function PromptElement(props: Partial<ModalProps>): ReactElement { | |
cancelButton = {}, | ||
okButton = {}, | ||
className = {}, | ||
shouldCloseOnOverlayClick, | ||
}, | ||
} = prompt; | ||
return ( | ||
|
@@ -46,6 +47,7 @@ export function PromptElement(props: Partial<ModalProps>): ReactElement { | |
overlayClassName="!z-max" | ||
isDrawerOnMobile | ||
drawerProps={{ displayCloseButton: false, appendOnRoot: true }} | ||
shouldCloseOnOverlayClick={shouldCloseOnOverlayClick} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. adding support to disable outside click on prompt |
||
{...props} | ||
> | ||
<Modal.Body> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,7 @@ import classnames from 'classnames'; | |
import { ReadingStreakPopup } from './popup'; | ||
import type { ButtonIconPosition } from '../buttons/Button'; | ||
import { Button, ButtonSize, ButtonVariant } from '../buttons/Button'; | ||
import { ReadingStreakIcon } from '../icons'; | ||
import { ReadingStreakIcon, WarningIcon } from '../icons'; | ||
import { SimpleTooltip } from '../tooltips'; | ||
import type { UserStreak } from '../../graphql/users'; | ||
import { useViewSize, ViewSize } from '../../hooks'; | ||
|
@@ -17,6 +17,8 @@ import ConditionalWrapper from '../ConditionalWrapper'; | |
import type { TooltipPosition } from '../tooltips/BaseTooltipContainer'; | ||
import { useAuthContext } from '../../contexts/AuthContext'; | ||
import { isSameDayInTimezone } from '../../lib/timezones'; | ||
import { IconWrapper } from '../Icon'; | ||
import { useStreakTimezoneOk } from '../../hooks/streaks/useStreakTimezoneOk'; | ||
|
||
interface ReadingStreakButtonProps { | ||
streak: UserStreak; | ||
|
@@ -77,6 +79,7 @@ export function ReadingStreakButton({ | |
const hasReadToday = | ||
streak?.lastViewAt && | ||
isSameDayInTimezone(new Date(streak.lastViewAt), new Date(), user.timezone); | ||
const isTimezoneOk = useStreakTimezoneOk(); | ||
|
||
const handleToggle = useCallback(() => { | ||
setShouldShowStreaks((state) => !state); | ||
|
@@ -118,7 +121,14 @@ export function ReadingStreakButton({ | |
id="reading-streak-header-button" | ||
type="button" | ||
iconPosition={iconPosition} | ||
icon={<ReadingStreakIcon secondary={hasReadToday} />} | ||
icon={ | ||
<IconWrapper wrapperClassName="relative flex items-center gap-2"> | ||
<ReadingStreakIcon secondary={hasReadToday} /> | ||
{!isTimezoneOk && ( | ||
<WarningIcon className="text-raw-cheese-40" secondary /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we want this specific color |
||
)} | ||
</IconWrapper> | ||
} | ||
variant={ | ||
isLaptop || isMobile ? ButtonVariant.Tertiary : ButtonVariant.Float | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ import { addDays, subDays } from 'date-fns'; | |
import { useQuery } from '@tanstack/react-query'; | ||
import classNames from 'classnames'; | ||
import Link from 'next/link'; | ||
import { useRouter } from 'next/router'; | ||
import { StreakSection } from './StreakSection'; | ||
import { DayStreak, Streak } from './DayStreak'; | ||
import { generateQueryKey, RequestKey, StaleTime } from '../../../lib/query'; | ||
|
@@ -13,15 +14,31 @@ import { useAuthContext } from '../../../contexts/AuthContext'; | |
import { useActions, useViewSize, ViewSize } from '../../../hooks'; | ||
import { ActionType } from '../../../graphql/actions'; | ||
import { Button, ButtonVariant } from '../../buttons/Button'; | ||
import { SettingsIcon } from '../../icons'; | ||
import { SettingsIcon, WarningIcon } from '../../icons'; | ||
import StreakReminderSwitch from '../StreakReminderSwitch'; | ||
import ReadingStreakSwitch from '../ReadingStreakSwitch'; | ||
import { useToggle } from '../../../hooks/useToggle'; | ||
import { ToggleWeekStart } from '../../widgets/ToggleWeekStart'; | ||
import { isWeekend, DayOfWeek } from '../../../lib/date'; | ||
import { DEFAULT_TIMEZONE, isSameDayInTimezone } from '../../../lib/timezones'; | ||
import { | ||
DEFAULT_TIMEZONE, | ||
getTimezoneOffsetLabel, | ||
isSameDayInTimezone, | ||
} from '../../../lib/timezones'; | ||
import { SimpleTooltip } from '../../tooltips'; | ||
import { isTesting } from '../../../lib/constants'; | ||
import { | ||
timezoneMismatchIgnoreKey, | ||
useStreakTimezoneOk, | ||
} from '../../../hooks/streaks/useStreakTimezoneOk'; | ||
import { usePrompt } from '../../../hooks/usePrompt'; | ||
import usePersistentContext from '../../../hooks/usePersistentContext'; | ||
import { useLogContext } from '../../../contexts/LogContext'; | ||
import { | ||
LogEvent, | ||
StreakTimezonePromptAction, | ||
TargetId, | ||
} from '../../../lib/log'; | ||
|
||
const getStreak = ({ | ||
value, | ||
|
@@ -82,10 +99,13 @@ interface ReadingStreakPopupProps { | |
fullWidth?: boolean; | ||
} | ||
|
||
const timezoneSettingsHref = '/account/notifications?s=timezone'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just in case we want to do something special when user comes from timezone prompt in the future |
||
|
||
export function ReadingStreakPopup({ | ||
streak, | ||
fullWidth, | ||
}: ReadingStreakPopupProps): ReactElement { | ||
const router = useRouter(); | ||
const isMobile = useViewSize(ViewSize.MobileL); | ||
const { user } = useAuthContext(); | ||
const { completeAction } = useActions(); | ||
|
@@ -95,6 +115,11 @@ export function ReadingStreakPopup({ | |
staleTime: StaleTime.Default, | ||
}); | ||
const [showStreakConfig, toggleShowStreakConfig] = useToggle(false); | ||
const isTimezoneOk = useStreakTimezoneOk(); | ||
const { showPrompt } = usePrompt(); | ||
const [timezoneMismatchIgnore, setTimezoneMismatchIgnore] = | ||
usePersistentContext(timezoneMismatchIgnoreKey, ''); | ||
const { logEvent } = useLogContext(); | ||
|
||
const streaks = useMemo(() => { | ||
const today = new Date(); | ||
|
@@ -159,16 +184,89 @@ export function ReadingStreakPopup({ | |
forceLoad={!isTesting} | ||
content={ | ||
<div className="flex text-center"> | ||
We are showing your reading streak in your selected timezone. | ||
<br /> | ||
Click to adjust your timezone if needed or traveling. | ||
{isTimezoneOk ? ( | ||
<> | ||
We are showing your reading streak in your selected | ||
timezone. | ||
<br /> | ||
Click to adjust your timezone if needed or traveling. | ||
</> | ||
) : ( | ||
<>Click for more info</> | ||
)} | ||
</div> | ||
} | ||
> | ||
<div className="m-auto flex justify-center font-normal !text-text-quaternary underline decoration-raw-pepper-10 tablet:m-0 tablet:justify-start"> | ||
<Link href="/account/notifications?s=timezone"> | ||
{user.timezone || DEFAULT_TIMEZONE} | ||
</Link> | ||
<div className="m-auto flex items-center tablet:m-0"> | ||
{!isTimezoneOk && ( | ||
<WarningIcon className="text-raw-cheese-40" secondary /> | ||
)} | ||
<div className="flex justify-center font-normal !text-text-quaternary underline decoration-raw-pepper-10 tablet:m-0 tablet:justify-start"> | ||
<Link | ||
onClick={async (event) => { | ||
const deviceTimezone = | ||
Intl.DateTimeFormat().resolvedOptions().timeZone; | ||
const eventExtra = { | ||
device_timezone: deviceTimezone, | ||
user_timezone: user.timezone, | ||
timezone_ok: isTimezoneOk, | ||
timezone_ignore: timezoneMismatchIgnore, | ||
}; | ||
|
||
logEvent({ | ||
event_name: LogEvent.Click, | ||
target_type: TargetId.StreakTimezoneLabel, | ||
extra: JSON.stringify(eventExtra), | ||
}); | ||
|
||
if (isTimezoneOk) { | ||
return; | ||
} | ||
|
||
event.preventDefault(); | ||
|
||
const promptResult = await showPrompt({ | ||
title: 'Streak timezone mismatch', | ||
description: `We detected your current timezone setting ${getTimezoneOffsetLabel( | ||
user?.timezone, | ||
)} does not match your current device timezone ${getTimezoneOffsetLabel( | ||
deviceTimezone, | ||
)}. You can update your timezone in settings.`, | ||
okButton: { | ||
title: 'Go to settings', | ||
}, | ||
cancelButton: { | ||
title: 'Ignore', | ||
}, | ||
shouldCloseOnOverlayClick: false, | ||
}); | ||
|
||
logEvent({ | ||
event_name: LogEvent.Click, | ||
target_type: TargetId.StreakTimezoneMismatchPrompt, | ||
extra: JSON.stringify({ | ||
...eventExtra, | ||
action: promptResult | ||
? StreakTimezonePromptAction.Settings | ||
: StreakTimezonePromptAction.Ignore, | ||
}), | ||
}); | ||
|
||
if (!promptResult) { | ||
setTimezoneMismatchIgnore(deviceTimezone); | ||
|
||
return; | ||
} | ||
|
||
router.push(timezoneSettingsHref); | ||
}} | ||
href={timezoneSettingsHref} | ||
> | ||
{isTimezoneOk | ||
? user.timezone || DEFAULT_TIMEZONE | ||
: 'Timezone mismatch'} | ||
</Link> | ||
</div> | ||
</div> | ||
</SimpleTooltip> | ||
</div> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { getTimezoneOffset } from 'date-fns-tz'; | ||
import { useEffect, useMemo } from 'react'; | ||
import { useAuthContext } from '../../contexts/AuthContext'; | ||
import { DEFAULT_TIMEZONE } from '../../lib/timezones'; | ||
import usePersistentContext from '../usePersistentContext'; | ||
import { useActions } from '../useActions'; | ||
import { ActionType } from '../../graphql/actions'; | ||
import { useLogContext } from '../../contexts/LogContext'; | ||
import { LogEvent } from '../../lib/log'; | ||
|
||
export const timezoneMismatchIgnoreKey = 'timezoneMismatchIgnore'; | ||
|
||
export const useStreakTimezoneOk = (): boolean => { | ||
const { user, isLoggedIn } = useAuthContext(); | ||
const { checkHasCompleted, isActionsFetched, completeAction } = useActions(); | ||
const { logEvent } = useLogContext(); | ||
|
||
const [ignoredTimezone] = usePersistentContext(timezoneMismatchIgnoreKey, ''); | ||
const deviceTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; | ||
|
||
const isTimezoneOk = useMemo(() => { | ||
if (ignoredTimezone === deviceTimezone) { | ||
return true; | ||
} | ||
|
||
if (!isLoggedIn) { | ||
return true; | ||
} | ||
|
||
return ( | ||
getTimezoneOffset(user?.timezone || DEFAULT_TIMEZONE) === | ||
getTimezoneOffset(Intl.DateTimeFormat().resolvedOptions().timeZone) | ||
); | ||
}, [deviceTimezone, ignoredTimezone, isLoggedIn, user?.timezone]); | ||
|
||
// once off check to see how many users with timezone mismatches we have in the wild | ||
useEffect(() => { | ||
Comment on lines
+36
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can also log this every time but for now it felt it was too much because for users that had the wrong timezone it would log every time they open the app |
||
if (isTimezoneOk) { | ||
return; | ||
} | ||
|
||
if (!isActionsFetched) { | ||
return; | ||
} | ||
|
||
if (checkHasCompleted(ActionType.StreakTimezoneMismatch)) { | ||
return; | ||
} | ||
|
||
logEvent({ | ||
event_name: LogEvent.StreakTimezoneMismatch, | ||
extra: JSON.stringify({ | ||
device_timezone: deviceTimezone, | ||
user_timezone: user?.timezone, | ||
timezone_ok: isTimezoneOk, | ||
timezone_ignore: ignoredTimezone, | ||
}), | ||
}); | ||
|
||
completeAction(ActionType.StreakTimezoneMismatch); | ||
}, [ | ||
isTimezoneOk, | ||
ignoredTimezone, | ||
isActionsFetched, | ||
checkHasCompleted, | ||
completeAction, | ||
logEvent, | ||
deviceTimezone, | ||
user?.timezone, | ||
]); | ||
|
||
return isTimezoneOk; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some reason the old icon was broken, we did not use it anywhere else in the project