diff --git a/packages/commonwealth/client/index.html b/packages/commonwealth/client/index.html index e1ab39a9add..2179f9d159a 100644 --- a/packages/commonwealth/client/index.html +++ b/packages/commonwealth/client/index.html @@ -17,7 +17,12 @@ sizes="64x64" href="/static/brand_assets/64x64.png" /> - + + @@ -46,7 +51,7 @@ /> diff --git a/packages/commonwealth/client/scripts/App.tsx b/packages/commonwealth/client/scripts/App.tsx index 5e8874a3326..f07ccbdce76 100644 --- a/packages/commonwealth/client/scripts/App.tsx +++ b/packages/commonwealth/client/scripts/App.tsx @@ -4,11 +4,14 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import useInitApp from 'hooks/useInitApp'; import router from 'navigation/Router'; -import React, { StrictMode } from 'react'; +import React, { StrictMode, useEffect, useState } from 'react'; import { RouterProvider } from 'react-router-dom'; import { ToastContainer } from 'react-toastify'; import { queryClient } from 'state/api/config'; import { openFeatureProvider } from './helpers/feature-flags'; +import useAppStatus from './hooks/useAppStatus'; +import { AddToHomeScreenPrompt } from './views/components/AddToHomeScreenPrompt'; + import CWLoadingSpinner from './views/components/component_kit/new_designs/CWLoadingSpinner'; const Splash = () => { @@ -24,6 +27,26 @@ OpenFeature.setProvider(openFeatureProvider); const App = () => { const { customDomain, isLoading } = useInitApp(); + const { isAddedToHomeScreen, isMarketingPage, isIOS, isAndroid } = + useAppStatus(); + + const [showPrompt, setShowPrompt] = useState(false); + const [isSplashUnloaded, setIsSplashUnloaded] = useState(false); + + useEffect(() => { + if (!isLoading) { + // Delay the unloading of the Splash component + setTimeout(() => { + setIsSplashUnloaded(true); + }, 1000); // Adjust the delay as needed + } + }, [isLoading]); + + useEffect(() => { + if (isSplashUnloaded) { + setShowPrompt(true); + } + }, [isSplashUnloaded]); return ( @@ -32,8 +55,14 @@ const App = () => { {isLoading ? ( ) : ( - + <> + + {isAddedToHomeScreen || isMarketingPage || !showPrompt ? null : ( + + )} + > )} + diff --git a/packages/commonwealth/client/scripts/hooks/useAppStatus.ts b/packages/commonwealth/client/scripts/hooks/useAppStatus.ts new file mode 100644 index 00000000000..72de0c522c7 --- /dev/null +++ b/packages/commonwealth/client/scripts/hooks/useAppStatus.ts @@ -0,0 +1,19 @@ +const useAppStatus = () => { + const isAddedToHomeScreen = window.matchMedia( + '(display-mode: standalone)', + ).matches; + const isMarketingPage = window.location.pathname === '/'; + const isIOS = window.navigator.userAgent.match(/(iPad|iPhone|iPod)/g) + ? true + : false; + const isAndroid = window.navigator.userAgent.match(/Android/g) ? true : false; + + return { + isAddedToHomeScreen, + isMarketingPage, + isIOS, + isAndroid, + }; +}; + +export default useAppStatus; diff --git a/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/AddToHomeScreenPrompt.tsx b/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/AddToHomeScreenPrompt.tsx new file mode 100644 index 00000000000..c63ed6ed045 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/AddToHomeScreenPrompt.tsx @@ -0,0 +1,55 @@ +import React, { useEffect, useState } from 'react'; +import { AndroidPrompt } from './AndroidPrompt'; +import { IOSPrompt } from './IOSPrompt'; +import { HIDE_PROMPT, HIDE_PROMPT_DAYS, HIDE_PROMPT_TIME } from './constants'; + +interface AddToHomeScreenPromptProps { + isIOS: boolean; + isAndroid: boolean; +} + +export const AddToHomeScreenPrompt = ({ + isIOS, + isAndroid, +}: AddToHomeScreenPromptProps) => { + const [showPrompt, setShowPrompt] = useState(true); + + useEffect(() => { + const hidePromptTime = localStorage.getItem(HIDE_PROMPT_TIME); + if (hidePromptTime && new Date().getTime() < Number(hidePromptTime)) { + setShowPrompt(false); + } + + if (sessionStorage.getItem(HIDE_PROMPT)) { + setShowPrompt(false); + } + }, [showPrompt]); + + const hidePromptForNDays = () => { + const maxDays = 30; + + let n = Number(localStorage.getItem(HIDE_PROMPT_DAYS)) || 1; + n = n * 2 > maxDays ? maxDays : n * 2; + const hideUntil = new Date().getTime() + n * 24; //* 60 * 60 * 1000; + localStorage.setItem(HIDE_PROMPT_TIME, hideUntil.toString()); + localStorage.setItem(HIDE_PROMPT_DAYS, n.toString()); + + setShowPrompt(false); + }; + + return showPrompt ? ( + isIOS ? ( + + ) : isAndroid ? ( + + ) : null + ) : null; +}; diff --git a/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/AndroidPrompt/AndroidPrompt.scss b/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/AndroidPrompt/AndroidPrompt.scss new file mode 100644 index 00000000000..c0fccf595e5 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/AndroidPrompt/AndroidPrompt.scss @@ -0,0 +1,117 @@ +@import '../../../../../styles/shared'; + +.AndroidPrompt { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + position: fixed; + bottom: 0; + left: 0; + width: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1002; + + .header { + display: flex; + align-items: center; + padding-top: 8px; + + .icon { + margin-right: 16px; + } + } + + .app { + .app-name { + color: $neutral-900; + text-align: center; + font-variant-numeric: lining-nums tabular-nums; + font-family: $font-family-roboto; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.16px; + } + .app-url { + color: $neutral-500; + text-align: center; + font-variant-numeric: lining-nums tabular-nums; + font-family: $font-family-roboto; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.24px; + } + } + + .button-container { + display: flex; + justify-content: flex-end; + gap: 32px; + } + + .prompt-button { + color: $primary-500; + text-align: right; + font-variant-numeric: lining-nums tabular-nums; + font-family: 'Roboto'; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.16px; + background-color: $white; + padding: 0px; + margin: 0px; + } + + .prompt-content { + background-color: $white; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); + padding: 8px 24px 8px; + border-radius: 26px; + margin-left: 18px; + margin-right: 18px; + + .title { + font-family: $font-family-roboto; + font-size: 24px; + font-style: normal; + font-weight: 400; + line-height: 24px; + letter-spacing: -0.24px; + color: $neutral-900; + padding: 24px 0px 8px; + } + + .description { + margin-top: 16px; + align-self: stretch; + color: $neutral-900; + font-variant-numeric: lining-nums tabular-nums; + font-family: $font-family-roboto; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.16px; + } + + .hide-prompt { + flex: 1 0 0; + margin-top: 16px; + color: $neutral-900; + font-variant-numeric: lining-nums tabular-nums; + font-family: $font-family-roboto !important; + font-size: 16px !important; + font-style: normal; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.16px; + } + } +} diff --git a/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/AndroidPrompt/AndroidPrompt.tsx b/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/AndroidPrompt/AndroidPrompt.tsx new file mode 100644 index 00000000000..d5996de77d4 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/AndroidPrompt/AndroidPrompt.tsx @@ -0,0 +1,100 @@ +import React, { useState } from 'react'; +import { CWCheckbox } from '../../component_kit/cw_checkbox'; +import { CWText } from '../../component_kit/cw_text'; +import { CWButton } from '../../component_kit/new_designs/cw_button'; +import { HIDE_PROMPT } from '../constants'; +import './AndroidPrompt.scss'; + +interface AndroidPromptProps { + hidePromptAction: () => void; + showPrompt: boolean; + setShowPrompt: (showPrompt: boolean) => void; +} + +export const AndroidPrompt = ({ + hidePromptAction, + showPrompt, + setShowPrompt, +}: AndroidPromptProps) => { + let installPromptEvent = null; + const [checkboxChecked, setCheckboxChecked] = useState(false); + + window.addEventListener('beforeinstallprompt', (event) => { + // Prevent Chrome 67 and earlier from automatically showing the prompt + event.preventDefault(); + + installPromptEvent = event; + }); + + const handleInstallClick = () => { + installPromptEvent.prompt(); + + // Wait for the user to respond to the prompt + installPromptEvent.userChoice.then((choiceResult) => { + if (choiceResult.outcome === 'accepted') { + // Hide after install prompt is accepted + console.log('User accepted the install prompt'); + sessionStorage.setItem(HIDE_PROMPT, 'true'); + setShowPrompt(false); + } else { + // Hide after install prompt is dismissed + sessionStorage.setItem(HIDE_PROMPT, 'true'); + setShowPrompt(false); + } + }); + }; + + const handleCancelClick = () => { + // Hide the prompt for the rest of the session + sessionStorage.setItem(HIDE_PROMPT, 'true'); + setShowPrompt(false); + // If the checkbox is checked, hide the prompt for N days + if (checkboxChecked) { + hidePromptAction(); + } + }; + + const handleCheckboxChange = (event) => { + setCheckboxChecked(event.target.checked); + }; + + return ( + + + Install App + + + + + + Common + common.xyz + + + + For the best mobile experience we recommend installing the Common + web-app. + + + + + + + + + ); +}; diff --git a/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/AndroidPrompt/index.ts b/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/AndroidPrompt/index.ts new file mode 100644 index 00000000000..88c96618426 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/AndroidPrompt/index.ts @@ -0,0 +1 @@ +export * from './AndroidPrompt'; diff --git a/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/IOSPrompt/IOSPrompt.scss b/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/IOSPrompt/IOSPrompt.scss new file mode 100644 index 00000000000..7a0bb3caf4f --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/IOSPrompt/IOSPrompt.scss @@ -0,0 +1,98 @@ +@import '../../../../../styles/shared'; + +.IOSPrompt { + display: flex; + align-items: flex-end; + justify-content: center; + position: fixed; + bottom: 0; + left: 0; + right: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1002; + margin: 0; + box-sizing: border-box; + + .header { + display: flex; + align-items: center; + border-bottom: 1px solid $neutral-200; + padding: 0px 16px 8px 16px; + gap: 16px; + align-self: stretch; + + .title { + color: $neutral-900; + font-family: $font-family-silka; + font-size: 18px; + font-style: normal; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.18px; + } + } + + .prompt-content { + background-color: $white; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); + padding: 16px; + border-radius: 6px; + + .description { + margin-top: 16px; + color: $neutral-500; + font-variant-numeric: lining-nums tabular-nums; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.14px; + flex: 1 0 0; + } + + .instructions { + margin-top: 16px; + + .instruction { + display: flex; + margin: 8px 0px; + font-size: 14px; + color: $neutral-900; + font-variant-numeric: lining-nums tabular-nums; + font-family: $font-family-neue-haas-unica; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.14px; + align-items: flex-start; + gap: 3px; + align-self: stretch; + + .share-icon { + color: $primary-500; + } + } + } + + .hide-prompt { + font-variant-numeric: lining-nums tabular-nums; + font-size: 14px !important; + font-family: $font-family-neue-haas-unica !important; + font-style: normal; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.14px; + } + + .highlight { + background: $neutral-50; + padding-left: 8px; + padding-right: 8px; + color: $neutral-600; + } + } +} diff --git a/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/IOSPrompt/IOSPrompt.tsx b/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/IOSPrompt/IOSPrompt.tsx new file mode 100644 index 00000000000..31dd9ff7974 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/IOSPrompt/IOSPrompt.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { CWIcon } from '../../component_kit/cw_icons/cw_icon'; +import { CWText } from '../../component_kit/cw_text'; +import { CWButton } from '../../component_kit/new_designs/cw_button'; +import './IOSPrompt.scss'; + +interface IOSPromptProps { + hidePromptAction: () => void; + showPrompt: boolean; + setShowPrompt: (showPrompt: boolean) => void; +} + +export const IOSPrompt = ({ + hidePromptAction, + showPrompt, + setShowPrompt, +}: IOSPromptProps) => { + return ( + + + + + + + Add to Home Screen + + + For the best mobile experience we recommend installing the Common + web-app. + + + + 1. Tap the share {' '} + icon + + + 2. Select Add to Home Screen + + + hidePromptAction()} + /> + + + ); +}; diff --git a/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/IOSPrompt/index.ts b/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/IOSPrompt/index.ts new file mode 100644 index 00000000000..c34e0691147 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/IOSPrompt/index.ts @@ -0,0 +1 @@ +export * from './IOSPrompt'; diff --git a/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/constants.ts b/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/constants.ts new file mode 100644 index 00000000000..8a7b181e475 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/constants.ts @@ -0,0 +1,3 @@ +export const HIDE_PROMPT_TIME = 'hidePromptTime'; +export const HIDE_PROMPT_DAYS = 'hidePromptDays'; +export const HIDE_PROMPT = 'hidePrompt'; diff --git a/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/index.tsx b/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/index.tsx new file mode 100644 index 00000000000..fa545fd7fc4 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/AddToHomeScreenPrompt/index.tsx @@ -0,0 +1 @@ +export * from './AddToHomeScreenPrompt'; diff --git a/packages/commonwealth/client/scripts/views/components/component_kit/cw_icons/cw_icon_lookup.ts b/packages/commonwealth/client/scripts/views/components/component_kit/cw_icons/cw_icon_lookup.ts index f4b438acf02..dea2ddcf325 100644 --- a/packages/commonwealth/client/scripts/views/components/component_kit/cw_icons/cw_icon_lookup.ts +++ b/packages/commonwealth/client/scripts/views/components/component_kit/cw_icons/cw_icon_lookup.ts @@ -26,6 +26,7 @@ import { ClockCounterClockwise, Code, Compass, + Export, Eye, Flag, Heart, @@ -77,6 +78,7 @@ export const iconLookup = { keyLockOpened: withPhosphorIcon(LockKeyOpen), keyLockClosed: withPhosphorIcon(LockKey), eye: withPhosphorIcon(Eye), + export: withPhosphorIcon(Export), archiveTray: withPhosphorIcon(ArchiveTray), arrowLeft: Icons.CWArrowLeft, arrowRight: Icons.CWArrowRight, diff --git a/packages/commonwealth/client/scripts/views/components/sidebar/AccountConnectionIndicator/AccountConnectionIndicator.tsx b/packages/commonwealth/client/scripts/views/components/sidebar/AccountConnectionIndicator/AccountConnectionIndicator.tsx index 1677d1a215d..36ac578522c 100644 --- a/packages/commonwealth/client/scripts/views/components/sidebar/AccountConnectionIndicator/AccountConnectionIndicator.tsx +++ b/packages/commonwealth/client/scripts/views/components/sidebar/AccountConnectionIndicator/AccountConnectionIndicator.tsx @@ -3,8 +3,8 @@ import React from 'react'; import { CWText } from 'views/components/component_kit/cw_text'; import { CWIdentificationTag } from 'views/components/component_kit/new_designs/CWIdentificationTag'; -import { CWButton } from 'views/components/component_kit/new_designs/cw_button'; import useJoinCommunity from 'views/components/SublayoutHeader/useJoinCommunity'; +import { CWButton } from 'views/components/component_kit/new_designs/cw_button'; import './AccountConnectionIndicator.scss'; interface AccountConnectionIndicatorProps { diff --git a/packages/commonwealth/client/styles/shared.scss b/packages/commonwealth/client/styles/shared.scss index dd60ac5bd55..c04f0df6a92 100644 --- a/packages/commonwealth/client/styles/shared.scss +++ b/packages/commonwealth/client/styles/shared.scss @@ -33,6 +33,7 @@ $font-family-neue-haas-unica: NeueHaasUnica, 'Segoe UI', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; $font-family-monospace: 'DM Mono', monospace; +$font-family-roboto: 'Roboto', sans-serif; @mixin visibleScrollbar($theme) { &::-webkit-scrollbar { diff --git a/packages/commonwealth/static/img/branding/common-white.png b/packages/commonwealth/static/img/branding/common-white.png new file mode 100644 index 00000000000..1867903138f Binary files /dev/null and b/packages/commonwealth/static/img/branding/common-white.png differ diff --git a/packages/commonwealth/static/img/branding/common.png b/packages/commonwealth/static/img/branding/common.png new file mode 100644 index 00000000000..c3c2a06768a Binary files /dev/null and b/packages/commonwealth/static/img/branding/common.png differ diff --git a/packages/commonwealth/static/img/branding/common.svg b/packages/commonwealth/static/img/branding/common.svg new file mode 100644 index 00000000000..2427009c405 --- /dev/null +++ b/packages/commonwealth/static/img/branding/common.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/packages/commonwealth/static/manifest.json b/packages/commonwealth/static/manifest.json new file mode 100644 index 00000000000..5af51dac53d --- /dev/null +++ b/packages/commonwealth/static/manifest.json @@ -0,0 +1,18 @@ +{ + "name": "Common App", + "short_name": "Common", + "description": "Create onchain together - Community forums with onchain rewards, roles and more", + "start_url": "/dashboard", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#ffffff", + "icons": [ + { + "src": "/static/img/branding/common.png", + "sizes": "192x192", + "type": "image/png" + } + ], + "manifest_version": "0.0.1", + "installable": true +}