Skip to content

Commit

Permalink
Merge pull request #686 from near/fix/notifications-infinite-nan-loop
Browse files Browse the repository at this point in the history
Fix Notifications Infinite "NaN" Loop
  • Loading branch information
calebjacob authored Oct 10, 2023
2 parents 542526f + 3c3503f commit c328760
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 181 deletions.
121 changes: 121 additions & 0 deletions src/components/NotificationsAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { useCallback, useEffect, useState } from 'react';

import { VmComponent } from '@/components/vm/VmComponent';
import { useBosComponents } from '@/hooks/useBosComponents';
import { useIosDevice } from '@/hooks/useIosDevice';
import { useAuthStore } from '@/stores/auth';
import {
handleOnCancel,
handleTurnOn,
recommendedIosVersionForNotifications,
showNotificationModal,
} from '@/utils/notifications';
import { isNotificationSupported, isPermisionGranted, isPushManagerSupported } from '@/utils/notificationsHelpers';
import { getNotificationLocalStorage, setNotificationsSessionStorage } from '@/utils/notificationsLocalStorage';
import type { TosData } from '@/utils/types';

type Props = {
tosData: TosData | null;
};

export const NotificationsAlert = ({ tosData }: Props) => {
const signedIn = useAuthStore((store) => store.signedIn);
const components = useBosComponents();
const [showNotificationModalState, setShowNotificationModalState] = useState(false);
const accountId = useAuthStore((store) => store.accountId);
const [isHomeScreenApp, setHomeScreenApp] = useState(false);
const [iosHomeScreenPrompt, setIosHomeScreenPrompt] = useState(false);
const { isIosDevice, versionOfIos } = useIosDevice();

const handleModalCloseOnEsc = useCallback(() => {
setShowNotificationModalState(false);
}, []);

const handleHomeScreenClose = useCallback(() => {
setIosHomeScreenPrompt(false);
}, []);

const turnNotificationsOn = useCallback(() => {
// for iOS devices, show a different modal asking the user to add the app to their home screen
// if the user has already added the app to their home screen, show the regular notification modal
if (isIosDevice && !isHomeScreenApp) {
setIosHomeScreenPrompt(true);
setShowNotificationModalState(false);
return;
}
return handleTurnOn(accountId, () => {
setShowNotificationModalState(false);
});
}, [accountId, isIosDevice, isHomeScreenApp]);

const pauseNotifications = useCallback(() => {
handleOnCancel();
setShowNotificationModalState(false);
}, []);

const checkNotificationModal = useCallback(() => {
if (tosData && tosData.agreementsForUser.length > 0) {
// show notification modal for new users
const tosAccepted =
tosData.agreementsForUser[tosData.agreementsForUser.length - 1].value === tosData.latestTosVersion;
// check if user has already turned on notifications
const { showOnTS } = getNotificationLocalStorage() || {};

if (!iosHomeScreenPrompt && ((tosAccepted && !showOnTS) || (tosAccepted && showOnTS < Date.now()))) {
setTimeout(() => {
setShowNotificationModalState(showNotificationModal());
}, 3000);
}
}
}, [tosData, iosHomeScreenPrompt]);

useEffect(() => {
if (!signedIn) {
return;
}
checkNotificationModal();
}, [signedIn, checkNotificationModal]);

useEffect(() => {
if (isIosDevice) {
setHomeScreenApp(window.matchMedia('(display-mode: standalone)').matches);
}
}, [isIosDevice]);

useEffect(() => {
if (isIosDevice) {
window.matchMedia('(display-mode: standalone)').addEventListener('change', (e) => setHomeScreenApp(e.matches));
// TODO: Remove event listener on cleanup
}
}, [isIosDevice]);

if (!signedIn) return null;

return (
<>
<VmComponent
src={components.nearOrg.notifications.alert}
props={{
open: showNotificationModalState,
handleTurnOn: turnNotificationsOn,
handleOnCancel: pauseNotifications,
isNotificationSupported,
isPermisionGranted,
isPushManagerSupported,
setNotificationsSessionStorage,
onOpenChange: handleModalCloseOnEsc,
iOSDevice: isIosDevice,
iOSVersion: versionOfIos,
recomendedIOSVersion: recommendedIosVersionForNotifications,
}}
/>
<VmComponent
src={components.nearOrg.notifications.iosHomeScreenAlert}
props={{
open: iosHomeScreenPrompt,
onOpenChange: handleHomeScreenClose,
}}
/>
</>
);
};
5 changes: 3 additions & 2 deletions src/hooks/useBosLoaderInitializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ export function useBosLoaderInitializer() {
useEffect(() => {
if (loaderUrl) {
fetchRedirectMap(loaderUrl);
} else {
} else if (flags) {
// We need to check if "flags" has fully resolved to avoid prematurely setting "hasResolved: true"
setStore({ hasResolved: true });
}
}, [fetchRedirectMap, loaderUrl, setStore]);
}, [flags, fetchRedirectMap, loaderUrl, setStore]);
}
34 changes: 34 additions & 0 deletions src/hooks/useIosDevice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
function detectIos() {
if (typeof window === 'undefined') return;

const isIosDevice = /iP(hone|od|ad)/.test(navigator.userAgent);
let versionOfIos = 0;

const versionMatch = navigator.userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/);

if (versionMatch) {
versionOfIos = Number(`${versionMatch[1]}.${versionMatch[2]}`);

if (isNaN(versionOfIos)) {
/*
We need to ensure that we never return "NaN". It isn't referentially stable and
causes infinite loops inside logic like useEffect().
*/
versionOfIos = 0;
}
}

return {
isIosDevice,
versionOfIos,
};
}

export function useIosDevice() {
const result = detectIos();

return {
isIosDevice: result?.isIosDevice,
versionOfIos: result?.versionOfIos,
};
}
121 changes: 6 additions & 115 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
import { isPassKeyAvailable } from '@near-js/biometric-ed25519';
import { useRouter } from 'next/router';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';

import { openToast } from '@/components/lib/Toast';
import { MetaTags } from '@/components/MetaTags';
import { ComponentWrapperPage } from '@/components/near-org/ComponentWrapperPage';
import { NearOrgHomePage } from '@/components/near-org/NearOrg.HomePage';
import { VmComponent } from '@/components/vm/VmComponent';
import { NotificationsAlert } from '@/components/NotificationsAlert';
import { useBosComponents } from '@/hooks/useBosComponents';
import { useDefaultLayout } from '@/hooks/useLayout';
import { useAuthStore } from '@/stores/auth';
import { useCurrentComponentStore } from '@/stores/current-component';
import {
detectIOSVersion,
handleOnCancel,
handleTurnOn,
isIOS,
recomendedIOSVersion,
showNotificationModal,
} from '@/utils/notifications';
import { isNotificationSupported, isPermisionGranted, isPushManagerSupported } from '@/utils/notificationsHelpers';
import { getNotificationLocalStorage, setNotificationsSessionStorage } from '@/utils/notificationsLocalStorage';
import type { NextPageWithLayout, TosData } from '@/utils/types';

const LS_ACCOUNT_ID = 'near-social-vm:v01::accountId:';
Expand All @@ -33,75 +23,8 @@ const HomePage: NextPageWithLayout = () => {
const setComponentSrc = useCurrentComponentStore((store) => store.setSrc);
const authStore = useAuthStore();
const [componentProps, setComponentProps] = useState<Record<string, unknown>>({});
const [showNotificationModalState, setShowNotificationModalState] = useState(false);
const accountId = useAuthStore((store) => store.accountId);
const [tosData, setTosData] = useState<TosData | null>(null);
const cacheTosData = useMemo(() => tosData, [tosData?.latestTosVersion]);
const [isHomeScreenApp, setHomeScreenApp] = useState(false);
const [iosHomeScreenPrompt, setIosHomeScreenPrompt] = useState(false);
const iOSDevice = useMemo(() => {
if (typeof window !== 'undefined') {
return isIOS();
}
return false;
}, []);

const iOSVersion = useMemo(() => {
if (typeof window !== 'undefined' && iOSDevice) {
return detectIOSVersion();
}
return;
}, [iOSDevice]);

const handleModalCloseOnEsc = useCallback(() => {
setShowNotificationModalState(false);
}, []);

const handleHomeScreenClose = useCallback(() => {
setIosHomeScreenPrompt(false);
}, []);

const turnNotificationsOn = useCallback(() => {
// for iOS devices, show a different modal asking the user to add the app to their home screen
// if the user has already added the app to their home screen, show the regular notification modal
if (iOSDevice && !isHomeScreenApp) {
setIosHomeScreenPrompt(true);
setShowNotificationModalState(false);
return;
}
return handleTurnOn(accountId, () => {
setShowNotificationModalState(false);
});
}, [accountId, iOSDevice, isHomeScreenApp]);

const pauseNotifications = useCallback(() => {
handleOnCancel();
setShowNotificationModalState(false);
}, []);

const checkNotificationModal = useCallback(() => {
if (cacheTosData && cacheTosData.agreementsForUser.length > 0) {
// show notification modal for new users
const tosAccepted =
cacheTosData.agreementsForUser[cacheTosData.agreementsForUser.length - 1].value ===
cacheTosData.latestTosVersion;
// check if user has already turned on notifications
const { showOnTS } = getNotificationLocalStorage() || {};

if (!iosHomeScreenPrompt && ((tosAccepted && !showOnTS) || (tosAccepted && showOnTS < Date.now()))) {
setTimeout(() => {
setShowNotificationModalState(showNotificationModal());
}, 3000);
}
}
}, [cacheTosData, iosHomeScreenPrompt]);

useEffect(() => {
if (!signedIn) {
return;
}
checkNotificationModal();
}, [signedIn, checkNotificationModal]);
const cachedTosData = useMemo(() => tosData, [tosData?.latestTosVersion]);

useEffect(() => {
const optimisticAccountId = window.localStorage.getItem(LS_ACCOUNT_ID);
Expand All @@ -114,8 +37,8 @@ const HomePage: NextPageWithLayout = () => {
}
}, [signedIn, setComponentSrc]);

// if we are loading the ActivityPage, process the query params into componentProps
useEffect(() => {
// If we are loading the ActivityPage, process the query params into componentProps
if (signedIn || signedInOptimistic) {
setComponentProps(router.query);
}
Expand All @@ -135,44 +58,12 @@ const HomePage: NextPageWithLayout = () => {
});
}
}, [signedIn]);
useEffect(() => {
if (iOSDevice) {
setHomeScreenApp(window.matchMedia('(display-mode: standalone)').matches);
}
}, [iOSDevice]);

useEffect(() => {
if (iOSDevice) {
window.matchMedia('(display-mode: standalone)').addEventListener('change', (e) => setHomeScreenApp(e.matches));
}
}, [iOSDevice]);

if (signedIn || signedInOptimistic) {
return (
<>
<VmComponent
src={components.nearOrg.notifications.alert}
props={{
open: showNotificationModalState,
handleTurnOn: turnNotificationsOn,
handleOnCancel: pauseNotifications,
isNotificationSupported,
isPermisionGranted,
isPushManagerSupported,
setNotificationsSessionStorage,
onOpenChange: handleModalCloseOnEsc,
iOSDevice,
iOSVersion,
recomendedIOSVersion,
}}
/>
<VmComponent
src={components.nearOrg.notifications.iosHomeScreenAlert}
props={{
open: iosHomeScreenPrompt,
onOpenChange: handleHomeScreenClose,
}}
/>
<NotificationsAlert tosData={cachedTosData} />

<ComponentWrapperPage
src={components.tosCheck}
componentProps={{
Expand Down
16 changes: 4 additions & 12 deletions src/pages/notifications-settings.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { useMemo } from 'react';

import { ComponentWrapperPage } from '@/components/near-org/ComponentWrapperPage';
import { useBosComponents } from '@/hooks/useBosComponents';
import { useIosDevice } from '@/hooks/useIosDevice';
import { useDefaultLayout } from '@/hooks/useLayout';
import { useAuthStore } from '@/stores/auth';
import {
handleOnCancel,
handleOnCancelBanner,
handlePushManagerUnsubscribe,
handleTurnOn,
isIOS,
} from '@/utils/notifications';
import {
isLocalStorageSupported,
Expand All @@ -23,18 +21,12 @@ import type { NextPageWithLayout } from '@/utils/types';
const NotificationsSettingsPage: NextPageWithLayout = () => {
const components = useBosComponents();
const accountId = useAuthStore((store) => store.accountId);
const iOSDevice = useMemo(() => {
if (typeof window !== 'undefined') {
return isIOS();
}
return false;
}, []);
const { isIosDevice } = useIosDevice();

return (
<ComponentWrapperPage
src={components.nearOrg.notifications.settings}
// TODO: fill
meta={{ title: '', description: '' }}
meta={{ title: 'NEAR | Notification Settings', description: '' }}
componentProps={{
isLocalStorageSupported,
isNotificationSupported,
Expand All @@ -46,7 +38,7 @@ const NotificationsSettingsPage: NextPageWithLayout = () => {
accountId,
handleTurnOn,
handlePushManagerUnsubscribe,
iOSDevice,
iOSDevice: isIosDevice,
}}
/>
);
Expand Down
Loading

0 comments on commit c328760

Please sign in to comment.