From 53d909d35bf7dc4162b62ea55073472abee4d933 Mon Sep 17 00:00:00 2001 From: Dmitriy <34593263+shelegdmitriy@users.noreply.github.com> Date: Thu, 5 Oct 2023 16:58:12 +0300 Subject: [PATCH 01/13] Prepare functionality to show `HomeScreenAlert` for iOS devices (#652) --- src/data/bos-components.ts | 3 ++ src/pages/index.tsx | 67 +++++++++++++++++++++++++++++--------- src/utils/notifications.ts | 7 ++-- 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/src/data/bos-components.ts b/src/data/bos-components.ts index abc53612f..056399e99 100644 --- a/src/data/bos-components.ts +++ b/src/data/bos-components.ts @@ -26,6 +26,7 @@ type NetworkComponents = { alert: string; settings: string; button: string; + iosHomeScreenAlert: string; }; }; notificationButton: string; @@ -74,6 +75,7 @@ export const componentsByNetworkId: Record { const accountId = useAuthStore((store) => store.accountId); const [tosData, setTosData] = useState(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 handleModalCloseOnEsc = useCallback(() => { setShowNotificationModalState(false); }, []); - const turnNotificationsOn = useCallback( - () => - handleTurnOn(accountId, () => { - 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(); @@ -49,27 +66,28 @@ const HomePage: NextPageWithLayout = () => { }, []); const checkNotificationModal = useCallback(() => { - if (tosData && tosData.agreementsForUser.length > 0) { + if (cacheTosData && cacheTosData.agreementsForUser.length > 0) { // show notification modal for new users const tosAccepted = - tosData.agreementsForUser[tosData.agreementsForUser.length - 1].value === tosData.latestTosVersion; + cacheTosData.agreementsForUser[cacheTosData.agreementsForUser.length - 1].value === + cacheTosData.latestTosVersion; // check if user has already turned on notifications const { showOnTS } = getNotificationLocalStorage() || {}; - if ((tosAccepted && !showOnTS) || (tosAccepted && showOnTS < Date.now())) { + if (!iosHomeScreenPrompt && ((tosAccepted && !showOnTS) || (tosAccepted && showOnTS < Date.now()))) { setTimeout(() => { setShowNotificationModalState(showNotificationModal()); - }, 10000); + }, 3000); } } - }, [cacheTosData]); + }, [cacheTosData, iosHomeScreenPrompt]); useEffect(() => { if (!signedIn) { return; } checkNotificationModal(); - }, [signedIn, cacheTosData]); + }, [signedIn, checkNotificationModal]); useEffect(() => { const optimisticAccountId = window.localStorage.getItem(LS_ACCOUNT_ID); @@ -103,6 +121,17 @@ 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 ( @@ -118,6 +147,14 @@ const HomePage: NextPageWithLayout = () => { isPushManagerSupported, setNotificationsSessionStorage, onOpenChange: handleModalCloseOnEsc, + iOSDevice, + }} + /> + { +export const isIOS = () => { const browserInfo = navigator.userAgent.toLowerCase(); return ( - browserInfo.match('iphone') || - browserInfo.match('ipad') || + browserInfo.includes('iphone') || + browserInfo.includes('ipad') || ['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'].includes(navigator.platform) ); }; From 4800c7c017d1f0ee3feb5e0a16311219e53ca57a Mon Sep 17 00:00:00 2001 From: Charles Garrett Date: Fri, 6 Oct 2023 13:04:35 -0400 Subject: [PATCH 02/13] chore: route /cookies to our cookie policy --- src/data/bos-components.ts | 3 +++ src/pages/cookies/index.tsx | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/pages/cookies/index.tsx diff --git a/src/data/bos-components.ts b/src/data/bos-components.ts index 056399e99..4092a606f 100644 --- a/src/data/bos-components.ts +++ b/src/data/bos-components.ts @@ -13,6 +13,7 @@ type NetworkComponents = { }; image: string; nearOrg: { + cookiePolicy: string; ecosystemCommunityPage: string; ecosystemGetFundingPage: string; ecosystemOverviewPage: string; @@ -62,6 +63,7 @@ export const componentsByNetworkId: Record { + useClearCurrentComponent(); + const components = useBosComponents(); + + return ( + <> + + + + ); +}; + +CookiesOverviewPage.getLayout = useDefaultLayout; + +export default CookiesOverviewPage; From 431d7b416481b361d3e7a8ab012d85a84aa9cb29 Mon Sep 17 00:00:00 2001 From: Dmitriy <34593263+shelegdmitriy@users.noreply.github.com> Date: Fri, 6 Oct 2023 20:10:28 +0300 Subject: [PATCH 03/13] Add note about minimal required iOS version (#672) * Add note about minimal required iOS version * Fix typo --- src/pages/index.tsx | 18 +++++++++++++++++- src/pages/notifications-settings.tsx | 10 ++++++++++ src/pages/notifications.tsx | 27 ++++++++++++++++++++++++++- src/utils/notifications.ts | 21 +++++++++++++++++++++ 4 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 3b3b0b0f1..c9a0f8e8f 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -11,7 +11,14 @@ import { useBosComponents } from '@/hooks/useBosComponents'; import { useDefaultLayout } from '@/hooks/useLayout'; import { useAuthStore } from '@/stores/auth'; import { useCurrentComponentStore } from '@/stores/current-component'; -import { handleOnCancel, handleTurnOn, isIOS, showNotificationModal } from '@/utils/notifications'; +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'; @@ -39,6 +46,13 @@ const HomePage: NextPageWithLayout = () => { return false; }, []); + const iOSVersion = useMemo(() => { + if (typeof window !== 'undefined' && iOSDevice) { + return detectIOSVersion(); + } + return; + }, [iOSDevice]); + const handleModalCloseOnEsc = useCallback(() => { setShowNotificationModalState(false); }, []); @@ -148,6 +162,8 @@ const HomePage: NextPageWithLayout = () => { setNotificationsSessionStorage, onOpenChange: handleModalCloseOnEsc, iOSDevice, + iOSVersion, + recomendedIOSVersion, }} /> { const components = useBosComponents(); const accountId = useAuthStore((store) => store.accountId); + const iOSDevice = useMemo(() => { + if (typeof window !== 'undefined') { + return isIOS(); + } + return false; + }, []); return ( { accountId, handleTurnOn, handlePushManagerUnsubscribe, + iOSDevice, }} /> ); diff --git a/src/pages/notifications.tsx b/src/pages/notifications.tsx index d5a8b1e50..6366038da 100644 --- a/src/pages/notifications.tsx +++ b/src/pages/notifications.tsx @@ -1,8 +1,17 @@ +import { useMemo } from 'react'; + import { ComponentWrapperPage } from '@/components/near-org/ComponentWrapperPage'; import { useBosComponents } from '@/hooks/useBosComponents'; import { useDefaultLayout } from '@/hooks/useLayout'; import { useAuthStore } from '@/stores/auth'; -import { handleOnCancel, handleOnCancelBanner, handleTurnOn } from '@/utils/notifications'; +import { + detectIOSVersion, + handleOnCancel, + handleOnCancelBanner, + handleTurnOn, + isIOS, + recomendedIOSVersion, +} from '@/utils/notifications'; import { isLocalStorageSupported, isNotificationSupported, @@ -15,6 +24,19 @@ import type { NextPageWithLayout } from '@/utils/types'; const NotificationsPage: NextPageWithLayout = () => { const components = useBosComponents(); const accountId = useAuthStore((store) => store.accountId); + const iOSDevice = useMemo(() => { + if (typeof window !== 'undefined') { + return isIOS(); + } + return false; + }, []); + + const iOSVersion = useMemo(() => { + if (typeof window !== 'undefined' && iOSDevice) { + return detectIOSVersion(); + } + return; + }, [iOSDevice]); return ( { handleOnCancelBanner, accountId, handleTurnOn, + iOSDevice, + iOSVersion, + recomendedIOSVersion, }} /> ); diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts index f6df181ff..5edc17837 100644 --- a/src/utils/notifications.ts +++ b/src/utils/notifications.ts @@ -20,6 +20,9 @@ const applicationServerKey = 'BH_QFHjBU9x3VlmE9_XM4Awhm5vj2wF9WNQIz5wdlO6hc5anwE const HOST = 'https://discovery-notifications-mainnet.near.org'; const GATEWAY_URL = 'https://near.org'; +// min version for iOS to support notifications +export const recomendedIOSVersion = 16.4; + export const isIOS = () => { const browserInfo = navigator.userAgent.toLowerCase(); @@ -30,6 +33,24 @@ export const isIOS = () => { ); }; +export const detectIOSVersion = () => { + const userAgent = navigator.userAgent; + const iOSVersionMatch = userAgent.match(/iPhone|iPad|iPod/i); + let iOSVersion; + if (iOSVersionMatch) { + // Extract the iOS version from the user agent + const iOSVersionString = userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/); + if (iOSVersionString) { + const versionParts = iOSVersionString + .slice(1) + .filter((i) => i !== undefined) + .map(Number); + iOSVersion = Number(versionParts.map(Number).join('.')); + } + } + return iOSVersion; +}; + const handleRequestPermission = () => Notification.requestPermission(); const registerServiceWorker = () => navigator.serviceWorker.register('/service-worker.js'); From f0a1a9802769709700d415c0a18bcc2d01c2caac Mon Sep 17 00:00:00 2001 From: Charles Garrett Date: Fri, 6 Oct 2023 14:26:30 -0400 Subject: [PATCH 04/13] chore: remote duplicate logic --- src/utils/notificationsLocalStorage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/notificationsLocalStorage.ts b/src/utils/notificationsLocalStorage.ts index a82c4bbc6..75f43ad0b 100644 --- a/src/utils/notificationsLocalStorage.ts +++ b/src/utils/notificationsLocalStorage.ts @@ -173,6 +173,6 @@ export const getNotificationLocalStorage = () => { const accountIdLS = getLSAccountId(); - const notificationsLS = isLocalStorageSupported() && JSON.parse(localStorage.getItem(NOTIFICATIONS_STORAGE) || '{}'); + const notificationsLS = JSON.parse(localStorage.getItem(NOTIFICATIONS_STORAGE) || '{}'); return notificationsLS[accountIdLS]; }; From b574eb7e2bd845a99ee7f6b30a086ac6c4a60db3 Mon Sep 17 00:00:00 2001 From: Charles Garrett Date: Fri, 6 Oct 2023 14:27:08 -0400 Subject: [PATCH 05/13] fix: avoid state undefined expection --- src/utils/notifications.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts index f6df181ff..199787fd6 100644 --- a/src/utils/notifications.ts +++ b/src/utils/notifications.ts @@ -126,7 +126,7 @@ export const showNotificationModal = () => { return false; } - const state = getNotificationLocalStorage(); + const state = getNotificationLocalStorage() || {}; if ((isLocalStorageSupported() && !state.showOnTS) || state.showOnTS < Date.now()) { return true; From b5be25b5db7dabebb3947f47ba47da047e8979d8 Mon Sep 17 00:00:00 2001 From: Charles Garrett Date: Fri, 6 Oct 2023 14:45:16 -0400 Subject: [PATCH 06/13] chore: add iubenda cookie prompt with automatic rejection of 3rd party cookies --- src/pages/_app.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 693ea6db1..b7cd60c70 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -82,6 +82,12 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) { return ( <> + + + From ad0dadc86a130ca27a257801b0eac338c4cac925 Mon Sep 17 00:00:00 2001 From: Caleb Jacob Date: Mon, 9 Oct 2023 10:52:08 -0600 Subject: [PATCH 07/13] Implement header UX updates, bring back Current Component UI --- .../navigation/CurrentComponent.tsx | 10 +- src/components/navigation/Navigation.tsx | 4 +- .../navigation/desktop/DesktopNavigation.tsx | 2 +- .../navigation/desktop/MainNavigationMenu.tsx | 61 +++++---- .../navigation/mobile/AccordionMenu.tsx | 91 ++++++++------ .../navigation/navigation-categories.ts | 119 ++++-------------- 6 files changed, 123 insertions(+), 164 deletions(-) diff --git a/src/components/navigation/CurrentComponent.tsx b/src/components/navigation/CurrentComponent.tsx index 9410eac72..3db7acd3e 100644 --- a/src/components/navigation/CurrentComponent.tsx +++ b/src/components/navigation/CurrentComponent.tsx @@ -5,11 +5,11 @@ import { VmComponent } from '@/components/vm/VmComponent'; import { useBosComponents } from '@/hooks/useBosComponents'; import { useCurrentComponentStore } from '@/stores/current-component'; -const StyledCurrentComponent = styled.div` +const Wrapper = styled.div` border: 1px solid #eeeeec; background-color: #f9f9f8; border-radius: 4px; - min-height: 100%; + min-width: 253px; .title { color: #868682; @@ -31,7 +31,7 @@ const StyledCurrentComponent = styled.div` justify-content: center; } > div { - padding: 15px; + padding: 15px 15px 0; div:nth-child(1) { flex-direction: column; text-align: center; @@ -64,7 +64,7 @@ export const CurrentComponent = () => { if (!src) return null; return ( - +
Current Component
{ showTags: true, }} /> -
+ ); }; diff --git a/src/components/navigation/Navigation.tsx b/src/components/navigation/Navigation.tsx index c35262b6d..c52546b4e 100644 --- a/src/components/navigation/Navigation.tsx +++ b/src/components/navigation/Navigation.tsx @@ -7,11 +7,11 @@ export const Navigation = () => { const [matches, setMatches] = useState(true); useEffect(() => { - setMatches(window.matchMedia('(min-width: 1120px)').matches); + setMatches(window.matchMedia('(min-width: 1024px)').matches); }, []); useEffect(() => { - window.matchMedia('(min-width: 1120px)').addEventListener('change', (e) => setMatches(e.matches)); + window.matchMedia('(min-width: 1024px)').addEventListener('change', (e) => setMatches(e.matches)); }, []); return ( diff --git a/src/components/navigation/desktop/DesktopNavigation.tsx b/src/components/navigation/desktop/DesktopNavigation.tsx index 5a986f956..cf3f875e6 100644 --- a/src/components/navigation/desktop/DesktopNavigation.tsx +++ b/src/components/navigation/desktop/DesktopNavigation.tsx @@ -71,7 +71,7 @@ const Search = styled.div` border: 1px solid var(--sand6); background-color: white; font-size: 16px; - margin-left: 1rem; + margin-left: 2rem; width: 200px; transition: all 200ms; diff --git a/src/components/navigation/desktop/MainNavigationMenu.tsx b/src/components/navigation/desktop/MainNavigationMenu.tsx index 0cf2db7f7..4735d7fea 100644 --- a/src/components/navigation/desktop/MainNavigationMenu.tsx +++ b/src/components/navigation/desktop/MainNavigationMenu.tsx @@ -2,14 +2,16 @@ import * as NavigationMenu from '@radix-ui/react-navigation-menu'; import Link from 'next/link'; import styled from 'styled-components'; -import { Button } from '@/components/lib/Button'; +import { useCurrentComponentStore } from '@/stores/current-component'; import { recordMouseEnter } from '@/utils/analytics'; +import { CurrentComponent } from '../CurrentComponent'; import { navigationCategories } from '../navigation-categories'; const Wrapper = styled.div` position: relative; display: flex; + justify-content: center; z-index: 1; flex-grow: 1; padding: 0 1rem; @@ -92,7 +94,7 @@ const Container = styled.div` const NavLink = styled(NavigationMenu.Link)` display: inline-block; min-width: 120px; - padding: 8px 0; + padding: 7px 0; font: var(--text-s); color: var(--sand10); transition: color 200ms; @@ -109,31 +111,30 @@ const NavLink = styled(NavigationMenu.Link)` const Section = styled.div` display: flex; flex-direction: column; - padding: 24px 24px 0; - - &:nth-child(1), - &:nth-child(2) { - padding-top: 0; - } - - &:nth-child(odd) { - border-right: 1px solid var(--sand4); - } + padding: 0 24px 0; + gap: 24px; + border-right: 1px solid var(--sand4); &:first-child:last-child { border-right: none; } `; +const CurrentComponentSection = styled.div` + padding: 0 24px 0; +`; + const SectionTitle = styled.p` font: var(--text-s); color: var(--sand12); font-weight: 600; - padding: 8px 0; + padding: 7px 0; margin: 0; `; export const MainNavigationMenu = () => { + const currentComponentSrc = useCurrentComponentStore((store) => store.src); + return ( @@ -146,19 +147,27 @@ export const MainNavigationMenu = () => { - {category.sections.map((section) => ( -
- {section.title && {section.title}} - - {section.links.map((link) => ( - - - {link.title} - - - ))} -
- ))} +
+ {category.sections.map((section) => ( +
+ {section.title && {section.title}} + + {section.links.map((link) => ( + + + {link.title} + + + ))} +
+ ))} +
+ + {currentComponentSrc && category.title === 'Develop' && ( + + + + )}
diff --git a/src/components/navigation/mobile/AccordionMenu.tsx b/src/components/navigation/mobile/AccordionMenu.tsx index cf6c2187e..2fee6451a 100644 --- a/src/components/navigation/mobile/AccordionMenu.tsx +++ b/src/components/navigation/mobile/AccordionMenu.tsx @@ -2,6 +2,9 @@ import * as Accordion from '@radix-ui/react-accordion'; import Link from 'next/link'; import styled from 'styled-components'; +import { useCurrentComponentStore } from '@/stores/current-component'; + +import { CurrentComponent } from '../CurrentComponent'; import { navigationCategories } from '../navigation-categories'; type Props = { @@ -90,6 +93,10 @@ const Section = styled.div` } `; +const CurrentComponentSection = styled.div` + padding: 24px; +`; + const SectionTitle = styled.p` font: var(--text-s); color: var(--sand12); @@ -98,40 +105,50 @@ const SectionTitle = styled.p` margin: 0; `; -export const AccordionMenu = (props: Props) => ( - - - {navigationCategories - .filter((category) => category.visible === 'all' || category.visible === 'mobile') - .map((category) => ( - - - - {category.title} - - - - - - {category.sections.map((section) => ( -
- {section.title && {section.title}} - - {section.links.map((link) => ( - - {link.title} - - ))} -
- ))} -
-
- ))} -
-
-); +export const AccordionMenu = (props: Props) => { + const currentComponentSrc = useCurrentComponentStore((store) => store.src); + + return ( + + + {navigationCategories + .filter((category) => category.visible === 'all' || category.visible === 'mobile') + .map((category) => ( + + + + {category.title} + + + + + + {category.sections.map((section) => ( +
+ {section.title && {section.title}} + + {section.links.map((link) => ( + + {link.title} + + ))} +
+ ))} + + {currentComponentSrc && category.title === 'Develop' && ( + + + + )} +
+
+ ))} +
+
+ ); +}; diff --git a/src/components/navigation/navigation-categories.ts b/src/components/navigation/navigation-categories.ts index 63ba87fe0..96f2b2d06 100644 --- a/src/components/navigation/navigation-categories.ts +++ b/src/components/navigation/navigation-categories.ts @@ -1,113 +1,59 @@ export const navigationCategories = [ { - title: 'Platform', + title: 'Develop', visible: 'all', sections: [ { - title: null, + title: 'Docs', links: [ { - title: 'Decentralized Front-Ends', - url: 'https://docs.near.org/bos/components', + title: 'All', + url: 'https://docs.near.org', }, { - title: 'Decentralized Hosting', - url: 'https://docs.near.org/bos/tutorial/bos-gateway', + title: 'Overview', + url: 'https://docs.near.org/concepts/welcome', }, { - title: 'Data Platform', - url: 'https://docs.near.org/concepts/data-flow/data-storage', + title: 'Smart Contracts', + url: 'https://docs.near.org/develop/contracts/welcome', }, { - title: 'Fast Auth', - url: 'https://docs.near.org/tools/fastauth-sdk', + title: 'Applications', + url: 'https://docs.near.org/develop/integrate/welcome', }, { - title: 'Near Protocol', - url: 'https://docs.near.org/concepts/web3/near', + title: 'Data Query', + url: 'https://docs.near.org/bos/queryapi/intro', }, - ], - }, - { - title: 'Discover', - links: [ { - title: 'Applications', - url: '/applications', - }, - { - title: 'Gateways', - url: 'https://near.org/gateways', + title: 'RPC', + url: 'https://docs.near.org/api/rpc/introduction', }, ], }, - ], - }, - - { - title: 'Developers', - visible: 'all', - sections: [ { title: 'Resources', links: [ { - title: 'Documentation', - url: 'https://docs.near.org', - }, - { - title: 'Sandbox', - url: '/sandbox', + title: 'Tools', + url: 'https://docs.near.org/tools/welcome', }, { - title: 'Tutorials', + title: 'Examples & Tutorials', url: 'https://docs.near.org/bos/tutorial/quickstart', }, { title: 'GitHub', url: 'https://github.com/near/dx', }, - ], - }, - { - title: 'Discover', - links: [ - { - title: 'Components', - url: '/components', - }, { title: 'Standards & Proposals', url: 'https://github.com/near/NEPs', }, - ], - }, - { - title: 'Tools', - links: [ - { - title: 'VS Code Extension', - url: 'https://docs.near.org/bos/dev/vscode', - }, - { - title: 'BOS Loader', - url: 'https://docs.near.org/bos/dev/bos-loader', - }, - { - title: 'APIs', - url: 'https://docs.near.org/bos/api/home', - }, - { - title: 'SDKs', - url: 'https://docs.near.org/sdk/welcome', - }, { - title: 'Command Line Tools', - url: 'https://github.com/bos-cli-rs/bos-cli-rs', - }, - { - title: 'View All', - url: 'https://docs.near.org/tools/welcome', + title: 'Technical Papers', + url: 'https://docs.near.org/concepts/advanced/papers', }, ], }, @@ -133,33 +79,29 @@ export const navigationCategories = [ title: 'Gateways', url: '/gateways', }, - { - title: 'People', - url: '/people', - }, - { - title: 'Standards & Proposals', - url: 'https://github.com/near/NEPs', - }, ], }, ], }, { - title: 'Community', + title: 'Ecosystem', visible: 'all', sections: [ { - title: 'Ecosystem', + title: null, links: [ { title: 'Overview', url: '/ecosystem', }, + { + title: 'People', + url: '/people', + }, { title: 'News', - url: 'https://near.org/nearweekapp.near/widget/nearweek-news', + url: '/nearweekapp.near/widget/nearweek-news', }, { title: 'Events', @@ -167,15 +109,6 @@ export const navigationCategories = [ }, ], }, - { - title: 'Discover', - links: [ - { - title: 'People', - url: '/people', - }, - ], - }, ], }, From 174e73f66cd6f0bfd931647ff12eb9f452a91980 Mon Sep 17 00:00:00 2001 From: Charles Garrett Date: Mon, 9 Oct 2023 13:54:50 -0400 Subject: [PATCH 08/13] chore: simplify the options on the cookie consent banner --- src/pages/_app.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index b7cd60c70..4f1b0f806 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -84,7 +84,7 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) { From a4a53179e6fd3c3ca9edcb4135a1be4d35ac0ab3 Mon Sep 17 00:00:00 2001 From: Charles Garrett Date: Mon, 9 Oct 2023 16:06:17 -0400 Subject: [PATCH 09/13] remove: iubenda config in favor of importing this via google tag manager. this avoids js exceptions --- src/pages/_app.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 4f1b0f806..693ea6db1 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -82,12 +82,6 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) { return ( <> - - - From 84c874f2a5437cc7493195cee6047aa726220d1a Mon Sep 17 00:00:00 2001 From: Caleb Jacob Date: Tue, 10 Oct 2023 11:51:30 -0600 Subject: [PATCH 10/13] Fix premature "hasResolved: true" in bos init --- src/hooks/useBosLoaderInitializer.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/hooks/useBosLoaderInitializer.ts b/src/hooks/useBosLoaderInitializer.ts index 14885f275..d6d5d3698 100644 --- a/src/hooks/useBosLoaderInitializer.ts +++ b/src/hooks/useBosLoaderInitializer.ts @@ -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]); } From 3c3503fe2df22579776e912e120230cd6163b744 Mon Sep 17 00:00:00 2001 From: Caleb Jacob Date: Tue, 10 Oct 2023 11:52:14 -0600 Subject: [PATCH 11/13] Refactor notification alert and iOS detection to avoid "NaN" infinite loop and improve home page flickering --- src/components/NotificationsAlert.tsx | 121 ++++++++++++++++++++++++++ src/hooks/useIosDevice.ts | 34 ++++++++ src/pages/index.tsx | 121 ++------------------------ src/pages/notifications-settings.tsx | 16 +--- src/pages/notifications.tsx | 30 ++----- src/utils/notifications.ts | 30 +------ 6 files changed, 173 insertions(+), 179 deletions(-) create mode 100644 src/components/NotificationsAlert.tsx create mode 100644 src/hooks/useIosDevice.ts diff --git a/src/components/NotificationsAlert.tsx b/src/components/NotificationsAlert.tsx new file mode 100644 index 000000000..f6f759142 --- /dev/null +++ b/src/components/NotificationsAlert.tsx @@ -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 ( + <> + + + + ); +}; diff --git a/src/hooks/useIosDevice.ts b/src/hooks/useIosDevice.ts new file mode 100644 index 000000000..69b828a94 --- /dev/null +++ b/src/hooks/useIosDevice.ts @@ -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, + }; +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index c9a0f8e8f..ef48eb2a3 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -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:'; @@ -33,75 +23,8 @@ const HomePage: NextPageWithLayout = () => { const setComponentSrc = useCurrentComponentStore((store) => store.setSrc); const authStore = useAuthStore(); const [componentProps, setComponentProps] = useState>({}); - const [showNotificationModalState, setShowNotificationModalState] = useState(false); - const accountId = useAuthStore((store) => store.accountId); const [tosData, setTosData] = useState(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); @@ -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); } @@ -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 ( <> - - + + { const components = useBosComponents(); const accountId = useAuthStore((store) => store.accountId); - const iOSDevice = useMemo(() => { - if (typeof window !== 'undefined') { - return isIOS(); - } - return false; - }, []); + const { isIosDevice } = useIosDevice(); return ( { accountId, handleTurnOn, handlePushManagerUnsubscribe, - iOSDevice, + iOSDevice: isIosDevice, }} /> ); diff --git a/src/pages/notifications.tsx b/src/pages/notifications.tsx index 6366038da..ffdcb78f2 100644 --- a/src/pages/notifications.tsx +++ b/src/pages/notifications.tsx @@ -1,16 +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 { - detectIOSVersion, handleOnCancel, handleOnCancelBanner, handleTurnOn, - isIOS, - recomendedIOSVersion, + recommendedIosVersionForNotifications, } from '@/utils/notifications'; import { isLocalStorageSupported, @@ -24,25 +21,12 @@ import type { NextPageWithLayout } from '@/utils/types'; const NotificationsPage: NextPageWithLayout = () => { const components = useBosComponents(); const accountId = useAuthStore((store) => store.accountId); - const iOSDevice = useMemo(() => { - if (typeof window !== 'undefined') { - return isIOS(); - } - return false; - }, []); - - const iOSVersion = useMemo(() => { - if (typeof window !== 'undefined' && iOSDevice) { - return detectIOSVersion(); - } - return; - }, [iOSDevice]); + const { isIosDevice, versionOfIos } = useIosDevice(); return ( { handleOnCancelBanner, accountId, handleTurnOn, - iOSDevice, - iOSVersion, - recomendedIOSVersion, + iOSDevice: isIosDevice, + iOSVersion: versionOfIos, + recomendedIOSVersion: recommendedIosVersionForNotifications, }} /> ); diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts index 046d3f1a6..e808efe96 100644 --- a/src/utils/notifications.ts +++ b/src/utils/notifications.ts @@ -21,35 +21,7 @@ const HOST = 'https://discovery-notifications-mainnet.near.org'; const GATEWAY_URL = 'https://near.org'; // min version for iOS to support notifications -export const recomendedIOSVersion = 16.4; - -export const isIOS = () => { - const browserInfo = navigator.userAgent.toLowerCase(); - - return ( - browserInfo.includes('iphone') || - browserInfo.includes('ipad') || - ['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'].includes(navigator.platform) - ); -}; - -export const detectIOSVersion = () => { - const userAgent = navigator.userAgent; - const iOSVersionMatch = userAgent.match(/iPhone|iPad|iPod/i); - let iOSVersion; - if (iOSVersionMatch) { - // Extract the iOS version from the user agent - const iOSVersionString = userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/); - if (iOSVersionString) { - const versionParts = iOSVersionString - .slice(1) - .filter((i) => i !== undefined) - .map(Number); - iOSVersion = Number(versionParts.map(Number).join('.')); - } - } - return iOSVersion; -}; +export const recommendedIosVersionForNotifications = 16.4; const handleRequestPermission = () => Notification.requestPermission(); From 23820b99ce944a3c1058772aef1630c8d497458d Mon Sep 17 00:00:00 2001 From: Caleb Jacob Date: Tue, 10 Oct 2023 12:37:02 -0600 Subject: [PATCH 12/13] Update news link in nav --- src/components/navigation/navigation-categories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/navigation/navigation-categories.ts b/src/components/navigation/navigation-categories.ts index 96f2b2d06..32e588602 100644 --- a/src/components/navigation/navigation-categories.ts +++ b/src/components/navigation/navigation-categories.ts @@ -101,7 +101,7 @@ export const navigationCategories = [ }, { title: 'News', - url: '/nearweekapp.near/widget/nearweek-news', + url: '/nearweekapp.near/widget/NEARWEEKNews', }, { title: 'Events', From 3bd00fc156562f59a76783a37bf51fa09749c9f4 Mon Sep 17 00:00:00 2001 From: Dmitriy <34593263+shelegdmitriy@users.noreply.github.com> Date: Wed, 11 Oct 2023 17:24:57 +0300 Subject: [PATCH 13/13] Prevent NotificationAlert from appearing (#685) * Prevent NotificationAlert from appearing * Rename Toc -> Tos --- src/components/NotificationsAlert.tsx | 15 ++++++++++----- src/pages/notifications-settings.tsx | 3 +++ src/utils/notifications.ts | 20 +++++++++++++++++--- src/utils/notificationsLocalStorage.ts | 9 +++++++++ 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/components/NotificationsAlert.tsx b/src/components/NotificationsAlert.tsx index f6f759142..774fb5be2 100644 --- a/src/components/NotificationsAlert.tsx +++ b/src/components/NotificationsAlert.tsx @@ -26,6 +26,7 @@ export const NotificationsAlert = ({ tosData }: Props) => { const [isHomeScreenApp, setHomeScreenApp] = useState(false); const [iosHomeScreenPrompt, setIosHomeScreenPrompt] = useState(false); const { isIosDevice, versionOfIos } = useIosDevice(); + const { showOnTS, subscribeStarted, subscribeError } = getNotificationLocalStorage() || {}; const handleModalCloseOnEsc = useCallback(() => { setShowNotificationModalState(false); @@ -59,15 +60,15 @@ export const NotificationsAlert = ({ tosData }: Props) => { const tosAccepted = tosData.agreementsForUser[tosData.agreementsForUser.length - 1].value === tosData.latestTosVersion; // check if user has already turned on notifications - const { showOnTS } = getNotificationLocalStorage() || {}; + const showNotificationPrompt = showNotificationModal(); - if (!iosHomeScreenPrompt && ((tosAccepted && !showOnTS) || (tosAccepted && showOnTS < Date.now()))) { + if (!subscribeError && showNotificationPrompt && tosAccepted && (!showOnTS || !iosHomeScreenPrompt)) { setTimeout(() => { - setShowNotificationModalState(showNotificationModal()); + setShowNotificationModalState(showNotificationPrompt); }, 3000); } } - }, [tosData, iosHomeScreenPrompt]); + }, [tosData, subscribeError, showOnTS, iosHomeScreenPrompt]); useEffect(() => { if (!signedIn) { @@ -85,7 +86,10 @@ export const NotificationsAlert = ({ tosData }: Props) => { useEffect(() => { if (isIosDevice) { window.matchMedia('(display-mode: standalone)').addEventListener('change', (e) => setHomeScreenApp(e.matches)); - // TODO: Remove event listener on cleanup + // Remove event listener + return () => { + window.matchMedia('(display-mode: standalone)').removeEventListener('change', () => setHomeScreenApp(false)); + }; } }, [isIosDevice]); @@ -107,6 +111,7 @@ export const NotificationsAlert = ({ tosData }: Props) => { iOSDevice: isIosDevice, iOSVersion: versionOfIos, recomendedIOSVersion: recommendedIosVersionForNotifications, + loading: subscribeStarted, }} /> { const components = useBosComponents(); const accountId = useAuthStore((store) => store.accountId); const { isIosDevice } = useIosDevice(); + const { subscribeStarted } = getNotificationLocalStorage() || {}; return ( { handleTurnOn, handlePushManagerUnsubscribe, iOSDevice: isIosDevice, + loading: subscribeStarted, + disabled: !accountId || subscribeStarted, }} /> ); diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts index e808efe96..c7f3cbd67 100644 --- a/src/utils/notifications.ts +++ b/src/utils/notifications.ts @@ -23,9 +23,21 @@ const GATEWAY_URL = 'https://near.org'; // min version for iOS to support notifications export const recommendedIosVersionForNotifications = 16.4; -const handleRequestPermission = () => Notification.requestPermission(); +const handleRequestPermission = () => { + try { + return Notification.requestPermission(); + } catch (error) { + console.error('Error while requesting permission.', error); + } +}; -const registerServiceWorker = () => navigator.serviceWorker.register('/service-worker.js'); +const registerServiceWorker = () => { + try { + return navigator.serviceWorker.register('/service-worker.js'); + } catch (error) { + console.error('Error while registering service-worker.', error); + } +}; const unregisterServiceWorker = async () => { const registrations = await navigator.serviceWorker.getRegistrations(); @@ -115,7 +127,9 @@ export const handleOnCancelBanner = () => { }; export const showNotificationModal = () => { - if (isPermisionGranted() && getNotificationLocalStorage()?.permission) { + const grantedPermission = isPermisionGranted(); + const { permission: initialPermissionGrantedByUser } = getNotificationLocalStorage() ?? {}; + if (grantedPermission && initialPermissionGrantedByUser) { return false; } diff --git a/src/utils/notificationsLocalStorage.ts b/src/utils/notificationsLocalStorage.ts index 75f43ad0b..237eaf8c6 100644 --- a/src/utils/notificationsLocalStorage.ts +++ b/src/utils/notificationsLocalStorage.ts @@ -1,3 +1,5 @@ +import { openToast } from '@/components/lib/Toast'; + import { isLocalStorageSupported, isNotificationSupported, @@ -92,6 +94,13 @@ export const setProcessError = (error: any) => { subscribeError: errorMessage, }), ); + openToast({ + id: 'notifications-subscription-error', + type: 'ERROR', + title: 'Push notification error', + description: `${errorMessage}. Please try again later or send us a message if the problem persists.`, + duration: 5000, + }); }; export const setProcessEnded = () => {