Skip to content

Commit

Permalink
Added manifest.json, works with no navbar for PWA (#6640)
Browse files Browse the repository at this point in the history
* Added manifest.json, works on iOS with no navbar for PWA

* Added working ios prompt

* Some css additions

* Fixed some scss errors and flushed out android testing + design accurate component

* Added fixes suggested by Marcin (some outstanding)

* Added correct common svg

* Added new png asset for manifest instead of svg

* Added icon from phosphorous and resolved some scss issues

* Small .scss fixes

* extracted magic strings to a const

* no prompt on marketing page

* Addressed latest round of fixes suggested by Marcin

* Addressed more comments + fixed cancel button that broke last commit

* Removed console logs

* Fix for icons and background color

* Removed OG icon from add to homescreen prompt

* Growl now behind prompt

* Opening to dashboard

* Changed copy

* Adding a delay for loading prompt

* Removed ternary

* Reset back to ternary

---------

Co-authored-by: Marcin <[email protected]>
  • Loading branch information
mw2000 and masvelio authored Feb 22, 2024
1 parent c4d8dab commit c43fd75
Show file tree
Hide file tree
Showing 19 changed files with 520 additions and 6 deletions.
11 changes: 8 additions & 3 deletions packages/commonwealth/client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
sizes="64x64"
href="/static/brand_assets/64x64.png"
/>
<link rel="apple-touch-icon" href="/static/brand_assets/180x180.png" />
<link
rel="manifest"
href="/static/manifest.json"
crossorigin="use-credentials"
/>
<link rel="apple-touch-icon" href="/static/img/branding/common-white.png" />
<meta name="title" content="Common" />
<meta
name="description"
Expand All @@ -33,7 +38,7 @@
/>
<meta
name="twitter:image:src"
content="https://commonwealth.im/static/brand_assets/logo_stacked.png"
content="https://commonwealth.im/static/img/branding/common.png"
/>

<meta property="og:type" content="website" />
Expand All @@ -46,7 +51,7 @@
/>
<meta
property="og:image"
content="https://commonwealth.im/static/brand_assets/logo_stacked.png"
content="https://commonwealth.im/static/img/branding/common.png"
/>
<meta property="og:image:width" content="339" />
<meta property="og:image:height" content="221" />
Expand Down
33 changes: 31 additions & 2 deletions packages/commonwealth/client/scripts/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand All @@ -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 (
<StrictMode>
Expand All @@ -32,8 +55,14 @@ const App = () => {
{isLoading ? (
<Splash />
) : (
<RouterProvider router={router(customDomain)} />
<>
<RouterProvider router={router(customDomain)} />
{isAddedToHomeScreen || isMarketingPage || !showPrompt ? null : (
<AddToHomeScreenPrompt isIOS={isIOS} isAndroid={isAndroid} />
)}
</>
)}

<ToastContainer />
<ReactQueryDevtools />
</OpenFeatureProvider>
Expand Down
19 changes: 19 additions & 0 deletions packages/commonwealth/client/scripts/hooks/useAppStatus.ts
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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 ? (
<IOSPrompt
hidePromptAction={hidePromptForNDays}
showPrompt={showPrompt}
setShowPrompt={setShowPrompt}
/>
) : isAndroid ? (
<AndroidPrompt
hidePromptAction={hidePromptForNDays}
showPrompt={showPrompt}
setShowPrompt={setShowPrompt}
/>
) : null
) : null;
};
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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 (
<div className="AndroidPrompt">
<div className="prompt-content">
<CWText className="title">Install App</CWText>
<div className="header">
<div className="icon">
<img src="/static/img/branding/common.svg" alt="Commonwealth" />
</div>
<div className="app">
<CWText className="app-name">Common</CWText>
<CWText className="app-url">common.xyz</CWText>
</div>
</div>
<CWText className="description">
For the best mobile experience we recommend installing the Common
web-app.
</CWText>
<CWCheckbox
className="hide-prompt"
label="Show less often"
onChange={handleCheckboxChange}
/>
<div className="button-container">
<CWButton
buttonType="tertiary"
className="prompt-button"
label="Cancel"
onClick={handleCancelClick}
/>
<CWButton
buttonType="tertiary"
className="prompt-button"
label="Install"
onClick={handleInstallClick}
/>
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './AndroidPrompt';
Loading

0 comments on commit c43fd75

Please sign in to comment.