Skip to content

Commit

Permalink
feat: pwa install on desktop (#3889)
Browse files Browse the repository at this point in the history
* feat: init pwa install on desktop

* fix: enum value

* refactor: add implementation in hook

* feat: add browser icons

* feat: add browser event in enum

* feat: add extension step in onboarding

* feat: add extension image

* feat: laptop check

* feat: add skip button

* feat: add skip button and experiment check

* fix: revert default step

* fix: add explicit type in return

* refactor: naming variable

* fix: enable button

* feat: check as completed extension step if installed

* refactor: force not load step if not right browser

* fix: removed typo

* fix: removed typo in exp value

* refactor: extract feature outside, f_auto in src;

* fix: add type in Typography element

* fix: add label change based on browser

* feat: add step on skip extension

* feat: revert changes on useSquadChecklist

* feat: add logic for this step

* feat: add style and icon

* feat: add navigation on resolve, clean logs;

* feat: add double icon/image and better naming variables

* feat: add event log

* fix: wrong experiment name

* fix: lint console.log

* fix: lint consistent return

* feat: add install pwa for firefox users

* feat: enable default for test on preview

* feat: enable default for test on preview

* feat: disable default for test on preview

* feat: add getCurrentBrowserName function and refactored pwa/extension step

* feat: add barrel index for PWA icons and custom text for safari

* fix: safari step doesn't require skipping extension

* fix: dupe variable name in image

* fix: lint import issues

* feat: add onClickNext type to common file

* refactor: rename BrowserName default to singular

* feat: evaluate user only on right condition

* fix: revert default value

* fix: use common type on onClickNext

* refactor: rename interface

* feat: enable by default step

* feat: help test on preview

* feat: help test on preview

* fix: revert debug variables/log

* fix: step logic

---------

Co-authored-by: Lee Hansel Solevilla <[email protected]>
  • Loading branch information
ilasw and sshanzel authored Jan 9, 2025
1 parent adfce2c commit d4e78a7
Show file tree
Hide file tree
Showing 21 changed files with 333 additions and 41 deletions.
9 changes: 9 additions & 0 deletions packages/shared/src/components/icons/PWA/Chrome/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ReactElement } from 'react';
import React from 'react';
import type { IconProps } from '../../../Icon';
import Icon from '../../../Icon';
import OutlinedIcon from './pwa.svg';

export const PWAChromeIcon = (props: IconProps): ReactElement => (
<Icon {...props} IconPrimary={OutlinedIcon} IconSecondary={OutlinedIcon} />
);
8 changes: 8 additions & 0 deletions packages/shared/src/components/icons/PWA/Chrome/pwa.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions packages/shared/src/components/icons/PWA/Edge/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ReactElement } from 'react';
import React from 'react';
import type { IconProps } from '../../../Icon';
import Icon from '../../../Icon';
import OutlinedIcon from './pwa.svg';

export const PWAEdgeIcon = (props: IconProps): ReactElement => (
<Icon {...props} IconPrimary={OutlinedIcon} IconSecondary={OutlinedIcon} />
);
7 changes: 7 additions & 0 deletions packages/shared/src/components/icons/PWA/Edge/pwa.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions packages/shared/src/components/icons/PWA/Safari/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ReactElement } from 'react';
import React from 'react';
import type { IconProps } from '../../../Icon';
import Icon from '../../../Icon';
import OutlinedIcon from './pwa.svg';

export const PWASafariIcon = (props: IconProps): ReactElement => (
<Icon {...props} IconPrimary={OutlinedIcon} IconSecondary={OutlinedIcon} />
);
6 changes: 6 additions & 0 deletions packages/shared/src/components/icons/PWA/Safari/pwa.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions packages/shared/src/components/icons/PWA/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { PWAChromeIcon } from './Chrome';
export { PWAEdgeIcon } from './Edge';
export { PWASafariIcon } from './Safari';
1 change: 1 addition & 0 deletions packages/shared/src/components/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export * from './Pin';
export * from './Play';
export * from './Plus';
export * from './Power';
export * from './PWA';
export * from './ReadingStreak';
export * from './Reddit';
export * from './Redis';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const CreateFeedButton = ({
OnboardingStep.AndroidApp,
OnboardingStep.PWA,
OnboardingStep.Extension,
OnboardingStep.InstallDesktop,
].includes(activeScreen);

const contentTypeNotEmpty =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@ import { LogEvent, TargetType } from '../../../lib/log';
import { useLogContext } from '../../../contexts/LogContext';
import { useOnboardingExtension } from './useOnboardingExtension';
import { cloudinaryOnboardingExtension } from '../../../lib/image';
import { BrowserName } from '../../../lib/func';
import type { OnboardingOnClickNext } from '../common';

export const OnboardingExtension = (): ReactElement => {
export const OnboardingExtension = ({
onClickNext,
}: {
onClickNext: OnboardingOnClickNext;
}): ReactElement => {
const { logEvent } = useLogContext();

const { browser } = useOnboardingExtension();

const imageUrls =
cloudinaryOnboardingExtension[browser.isEdge ? 'edge' : 'chrome'];
const { browserName } = useOnboardingExtension();
const isEdge = browserName === BrowserName.Edge;
const imageUrls = cloudinaryOnboardingExtension[browserName];

return (
<div className="flex flex-1 flex-col laptop:justify-between">
Expand All @@ -46,25 +50,20 @@ export const OnboardingExtension = (): ReactElement => {
</Typography>
<Button
href={downloadBrowserExtension}
icon={
browser.isEdge ? (
<EdgeIcon aria-hidden />
) : (
<ChromeIcon aria-hidden />
)
}
icon={isEdge ? <EdgeIcon aria-hidden /> : <ChromeIcon aria-hidden />}
onClick={() => {
logEvent({
event_name: LogEvent.DownloadExtension,
target_id: browser.isEdge ? TargetType.Edge : TargetType.Chrome,
target_id: isEdge ? TargetType.Edge : TargetType.Chrome,
});
onClickNext?.({ clickExtension: true });
}}
rel={anchorDefaultRel}
tag="a"
target="_blank"
variant={ButtonVariant.Primary}
>
<span>Get it for {browser.isEdge ? 'Edge' : 'Chrome'}</span>
<span>Get it for {isEdge ? 'Edge' : 'Chrome'}</span>
</Button>
<Typography
color={TypographyColor.Secondary}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,24 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import {
checkIsBrowser,
checkIsChromeOnly,
UserAgent,
} from '../../../lib/func';
import { BrowserName, getCurrentBrowserName } from '../../../lib/func';
import { useActions, useViewSize, ViewSize } from '../../../hooks';
import { ActionType } from '../../../graphql/actions';
import { isBrave } from '../../../lib/constants';

interface UseOnboardingExtension {
hasCheckedExtension: boolean;
shouldShowExtensionOnboarding: boolean;
browser: {
isChrome: boolean;
isEdge: boolean;
};
browserName: BrowserName;
}

export const useOnboardingExtension = (): UseOnboardingExtension => {
const router = useRouter();
const isComingFromExtension = router.query.ref === 'install';

const isChrome = checkIsChromeOnly() || isBrave();
const isEdge = checkIsBrowser(UserAgent.Edge);
const browserName = getCurrentBrowserName();
const isChrome = [BrowserName.Chrome, BrowserName.Brave].includes(
browserName,
);
const isEdge = BrowserName.Edge === browserName;
const isValidBrowser = isChrome || isEdge;

const isLaptop = useViewSize(ViewSize.Laptop);
Expand Down Expand Up @@ -53,9 +48,6 @@ export const useOnboardingExtension = (): UseOnboardingExtension => {
return {
hasCheckedExtension: isCheckedExtension,
shouldShowExtensionOnboarding,
browser: {
isChrome,
isEdge,
},
browserName,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const OnboardingHeader = ({
OnboardingStep.PWA,
OnboardingStep.Plus,
OnboardingStep.Extension,
OnboardingStep.InstallDesktop,
];

if (activeScreen !== OnboardingStep.Intro) {
Expand Down
4 changes: 2 additions & 2 deletions packages/shared/src/components/onboarding/OnboardingPWA.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
import { OnboardingTitle } from './common';
import {
cloudinaryPWA,
cloudinaryPWAChrome,
cloudinaryMobilePWAChrome,
cloudinaryPWAVideo,
cloudinaryPWAVideoChrome,
} from '../../lib/image';
Expand All @@ -17,7 +17,7 @@ export const OnboardingPWA = (): ReactElement => {
<div className="rounded-lg pointer-events-none absolute top-0 z-2 flex h-screen w-screen flex-col gap-4 p-6 opacity-0 backdrop-blur transition-all duration-200" />
<video
className="absolute top-0 max-h-screen w-full"
poster={isChrome ? cloudinaryPWAChrome : cloudinaryPWA}
poster={isChrome ? cloudinaryMobilePWAChrome : cloudinaryPWA}
src={isChrome ? cloudinaryPWAVideoChrome : cloudinaryPWAVideo}
muted
autoPlay
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React from 'react';
import type { FC, ReactElement } from 'react';
import classNames from 'classnames';
import {
Typography,
TypographyColor,
TypographyTag,
TypographyType,
} from '../../typography/Typography';
import { Button, ButtonVariant } from '../../buttons/Button';
import { useInstallPWA } from './useInstallPWA';
import {
cloudinaryPWADesktopChrome,
cloudinaryPWADesktopEdge,
cloudinaryPWADesktopSafari,
} from '../../../lib/image';
import { BrowserName } from '../../../lib/func';
import { PWAChromeIcon, PWAEdgeIcon, PWASafariIcon } from '../../icons';
import type { IconProps } from '../../Icon';
import { useLogContext } from '../../../contexts/LogContext';
import { LogEvent } from '../../../lib/log';

const icons: Partial<Record<BrowserName, FC<IconProps>>> = {
[BrowserName.Chrome]: PWAChromeIcon,
[BrowserName.Edge]: PWAEdgeIcon,
[BrowserName.Safari]: PWASafariIcon,
};
const images: Partial<Record<BrowserName, string>> = {
[BrowserName.Chrome]: cloudinaryPWADesktopChrome,
[BrowserName.Edge]: cloudinaryPWADesktopEdge,
[BrowserName.Safari]: cloudinaryPWADesktopSafari,
};

export const OnboardingInstallDesktop = ({
onClickNext,
}: {
onClickNext: () => void;
}): ReactElement => {
const { logEvent } = useLogContext();
const { promptToInstall, browserName } = useInstallPWA();
const isSafari = browserName === BrowserName.Safari;
const PWAIcon = icons[browserName] ?? icons[BrowserName.Chrome];
const imageSrc = images[browserName] ?? images[BrowserName.Chrome];

return (
<div className="flex flex-1 flex-col laptop:justify-between">
<div className="mb-14 flex flex-col items-center gap-6 justify-self-start text-center">
<Typography
bold
tag={TypographyTag.H1}
type={TypographyType.LargeTitle}
>
More daily.dev on your desktop?
<br />
Yes, please! 👀
</Typography>
<Button
icon={<PWAIcon aria-hidden />}
onClick={async () => {
logEvent({
event_name: LogEvent.InstallPWA,
});
await promptToInstall?.();
onClickNext?.();
}}
variant={ButtonVariant.Primary}
>
{isSafari ? 'Add to Dock' : 'Install on desktop'}
</Button>
<Typography
bold
color={TypographyColor.Tertiary}
tag={TypographyTag.P}
type={TypographyType.Subhead}
>
Manual: Press the
<span className="mx-2 inline-grid size-7 place-content-center rounded-1/2 bg-accent-salt-bolder align-middle text-text-primary">
<PWAIcon
className={classNames('inline-block', isSafari && '-mt-0.5')}
aria-hidden
/>
</span>
icon next to the browser search bar and choose{' '}
{isSafari ? `"Add to Dock"` : `"Install"`} to level up!
</Typography>
</div>
<figure className="pointer-events-none mx-auto">
<img
alt="Install daily.dev on desktop"
className="w-full"
loading="lazy"
role="presentation"
src={imageSrc}
/>
</figure>
</div>
);
};
42 changes: 42 additions & 0 deletions packages/shared/src/components/onboarding/PWA/useInstallPWA.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { BrowserName } from '../../../lib/func';
import { getCurrentBrowserName, isPWA } from '../../../lib/func';

interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<'accepted' | 'dismissed'>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}

export interface UseInstallPWA {
isAvailable: boolean;
isCurrentPWA: boolean;
promptToInstall: (() => Promise<null | 'accepted' | 'dismissed'>) | null;
browserName: BrowserName;
}

let installEvent: BeforeInstallPromptEvent | null = null;
globalThis?.addEventListener?.(
'beforeinstallprompt',
(e: BeforeInstallPromptEvent) => {
e.preventDefault();
installEvent = e;
},
{ once: true },
);

export const useInstallPWA = (): UseInstallPWA => {
const isCurrentPWA = isPWA();
const isAvailable = !!installEvent;
const browserName = getCurrentBrowserName();

const promptToInstall = async () => {
if (installEvent) {
await installEvent.prompt?.();
const { outcome } = await installEvent.userChoice;
return outcome;
}

return null;
};

return { isCurrentPWA, isAvailable, promptToInstall, browserName };
};
7 changes: 7 additions & 0 deletions packages/shared/src/components/onboarding/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import classed from '../../lib/classed';

export type OnboardingOnClickNext = (
options?: Partial<{
clickExtension: boolean;
}>,
) => void;

export enum OnboardingStep {
Intro = 'intro',
Topics = 'topics',
Expand All @@ -11,6 +17,7 @@ export enum OnboardingStep {
PWA = 'pwa',
Plus = 'plus',
Extension = 'extension',
InstallDesktop = 'install_desktop',
}

export const OnboardingTitle = classed(
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/lib/featureManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export const featureOnboardingExtension = new Feature(
'onboarding_extension',
false,
);
export const featureOnboardingDesktopPWA = new Feature(
'onboarding_desktop_pwa',
false,
);
export const featureOnboardingPWA = new Feature('onboarding_pwa', false);
export const featureOnboardingAndroid = new Feature(
'onboarding_android',
Expand Down
Loading

0 comments on commit d4e78a7

Please sign in to comment.