Skip to content

Commit

Permalink
💄 style: improve PWA install guide (lobehub#2617)
Browse files Browse the repository at this point in the history
* ✨ feat: Update PWA install

* ✨ feat: Update usePlatform

* ✨ feat: Update isSonomaOrLaterSafari

* 🐛 fix: Fix isSonomaOrLaterSafari

* 🐛 fix: Fix isSupportInstallPWA

* ✅ test: Add test

* ✅ test: Add isSonomaOrLaterSafari test
  • Loading branch information
canisminor1990 authored May 23, 2024
1 parent 4a23cad commit 7fee545
Show file tree
Hide file tree
Showing 13 changed files with 346 additions and 31 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"@clerk/themes": "^2.1.6",
"@google/generative-ai": "^0.11.3",
"@icons-pack/react-simple-icons": "^9.5.0",
"@khmyznikov/pwa-install": "^0.3.9",
"@lobehub/chat-plugin-sdk": "latest",
"@lobehub/chat-plugins-gateway": "latest",
"@lobehub/icons": "^1.22.0",
Expand Down
4 changes: 2 additions & 2 deletions src/app/(main)/_layout/Desktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { useTheme } from 'antd-style';
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';

import { useIsPWA } from '@/hooks/useIsPWA';
import { usePlatform } from '@/hooks/usePlatform';

import { LayoutProps } from './type';

const Layout = memo<LayoutProps>(({ children, nav }) => {
const isPWA = useIsPWA();
const { isPWA } = usePlatform();
const theme = useTheme();

return (
Expand Down
2 changes: 2 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { isRtlLang } from 'rtl-detect';

import Analytics from '@/components/Analytics';
import { DEFAULT_LANG, LOBE_LOCALE_COOKIE } from '@/const/locale';
import PWAInstall from '@/features/PWAInstall';
import AuthProvider from '@/layout/AuthProvider';
import GlobalProvider from '@/layout/GlobalProvider';
import { isMobileDevice } from '@/utils/responsive';
Expand All @@ -31,6 +32,7 @@ const RootLayout = async ({ children, modal }: RootLayoutProps) => {
{children}
{modal}
</AuthProvider>
<PWAInstall />
</GlobalProvider>
<Analytics />
{inVercel && <SpeedInsights />}
Expand Down
1 change: 1 addition & 0 deletions src/const/layoutTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export const MOBILE_HEADER_ICON_SIZE = { blockSize: 36, fontSize: 22 };
export const DESKTOP_HEADER_ICON_SIZE = { fontSize: 24 };
export const HEADER_ICON_SIZE = (mobile?: boolean) =>
mobile ? MOBILE_HEADER_ICON_SIZE : DESKTOP_HEADER_ICON_SIZE;
export const PWA_INSTALL_ID = 'pwa-install';
22 changes: 22 additions & 0 deletions src/features/PWAInstall/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use client';

import dynamic from 'next/dynamic';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';

import { PWA_INSTALL_ID } from '@/const/layoutTokens';
import { usePlatform } from '@/hooks/usePlatform';

// @ts-ignore
const PWA: any = dynamic(() => import('@khmyznikov/pwa-install/dist/pwa-install.react.js'), {
ssr: false,
});

const PWAInstall = memo(() => {
const { t } = useTranslation('metadata');
const { isPWA } = usePlatform();
if (isPWA) return null;
return <PWA description={t('chat.description')} id={PWA_INSTALL_ID} />;
});

export default PWAInstall;
13 changes: 0 additions & 13 deletions src/hooks/useIsPWA.ts

This file was deleted.

78 changes: 78 additions & 0 deletions src/hooks/usePWAInstall.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { act, renderHook } from '@testing-library/react';
import { pwaInstallHandler } from 'pwa-install-handler';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { PWA_INSTALL_ID } from '@/const/layoutTokens';

import { usePWAInstall } from './usePWAInstall';
import { usePlatform } from './usePlatform';

// Mocks
vi.mock('./usePlatform', () => ({
usePlatform: vi.fn(),
}));

vi.mock('@/utils/env', () => ({
isOnServerSide: false,
}));

vi.mock('pwa-install-handler', () => ({
pwaInstallHandler: {
addListener: vi.fn(),
removeListener: vi.fn(),
getEvent: vi.fn(),
},
}));

describe('usePWAInstall', () => {
afterEach(() => {
vi.clearAllMocks();
});

it('should return canInstall as false when in PWA', () => {
vi.mocked(usePlatform).mockReturnValue({ isSupportInstallPWA: true, isPWA: true } as any);

const { result } = renderHook(() => usePWAInstall());

expect(result.current.canInstall).toBe(false);
});

it('should return canInstall based on canInstall state when support PWA', () => {
vi.mocked(usePlatform).mockReturnValue({ isSupportInstallPWA: true, isPWA: false } as any);

const { result, rerender } = renderHook(() => usePWAInstall());

expect(result.current.canInstall).toBe(false);

act(() => {
vi.mocked(pwaInstallHandler.addListener).mock.calls[0][0](true);
});

rerender();

expect(result.current.canInstall).toBe(true);
});

it('should return canInstall as true when not support PWA', () => {
vi.mocked(usePlatform).mockReturnValue({ isSupportInstallPWA: false, isPWA: false } as any);

const { result } = renderHook(() => usePWAInstall());

expect(result.current.canInstall).toBe(true);
});

it('should call pwa.showDialog when install is called', () => {
const mockShowDialog = vi.fn();
document.body.innerHTML = `<div id="${PWA_INSTALL_ID}"></div>`;
const pwaElement: any = document.querySelector(`#${PWA_INSTALL_ID}`);
pwaElement.showDialog = mockShowDialog;

const { result } = renderHook(() => usePWAInstall());

act(() => {
result.current.install();
});

expect(mockShowDialog).toHaveBeenCalledWith(true);
});
});
25 changes: 23 additions & 2 deletions src/hooks/usePWAInstall.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,39 @@
import { pwaInstallHandler } from 'pwa-install-handler';
import { useEffect, useState } from 'react';

import { PWA_INSTALL_ID } from '@/const/layoutTokens';
import { isOnServerSide } from '@/utils/env';

import { usePlatform } from './usePlatform';

export const usePWAInstall = () => {
const [canInstall, setCanInstall] = useState(false);
const { isSupportInstallPWA, isPWA } = usePlatform();

useEffect(() => {
if (isOnServerSide) return;
pwaInstallHandler.addListener(setCanInstall);
return () => {
pwaInstallHandler.removeListener(setCanInstall);
};
}, []);

const installCheck = () => {
// 当在 PWA 中时,不显示安装按钮
if (isPWA) return false;
// 其他情况下,根据是否可以安装来显示安装按钮 (如已经安装则不显示)
if (isSupportInstallPWA) return canInstall;
// 当在不支持 PWA 的环境中时,安装按钮 (此时为安装教程)
return true;
};

return {
canInstall,
install: pwaInstallHandler.install,
canInstall: installCheck(),
install: () => {
const pwa: any = document.querySelector(`#${PWA_INSTALL_ID}`);
if (!pwa) return;
pwa.externalPromptEvent = pwaInstallHandler.getEvent();
pwa?.showDialog(true);
},
};
};
82 changes: 82 additions & 0 deletions src/hooks/usePlatform.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { renderHook } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';

import * as platformUtils from '@/utils/platform';

import { usePlatform } from './usePlatform';

// Mocks
vi.mock('@/utils/platform', () => ({
getBrowser: vi.fn(),
getPlatform: vi.fn(),
isInStandaloneMode: vi.fn(),
isSonomaOrLaterSafari: vi.fn(),
}));

describe('usePlatform', () => {
it('should return correct platform info for Mac OS and Chrome', () => {
vi.mocked(platformUtils.getPlatform).mockReturnValue('Mac OS');
vi.mocked(platformUtils.getBrowser).mockReturnValue('Chrome');
vi.mocked(platformUtils.isInStandaloneMode).mockReturnValue(false);
vi.mocked(platformUtils.isSonomaOrLaterSafari).mockReturnValue(false);

const { result } = renderHook(() => usePlatform());

expect(result.current).toEqual({
isApple: true,
isChrome: true,
isChromium: true,
isEdge: false,
isIOS: false,
isMacOS: true,
isPWA: false,
isSafari: false,
isSonomaOrLaterSafari: false,
isSupportInstallPWA: true,
});
});

it('should return correct platform info for iOS and Safari', () => {
vi.mocked(platformUtils.getPlatform).mockReturnValue('iOS');
vi.mocked(platformUtils.getBrowser).mockReturnValue('Safari');
vi.mocked(platformUtils.isInStandaloneMode).mockReturnValue(true);
vi.mocked(platformUtils.isSonomaOrLaterSafari).mockReturnValue(true);

const { result } = renderHook(() => usePlatform());

expect(result.current).toEqual({
isApple: true,
isChrome: false,
isChromium: false,
isEdge: false,
isIOS: true,
isMacOS: false,
isPWA: true,
isSafari: true,
isSonomaOrLaterSafari: true,
isSupportInstallPWA: false,
});
});

it('should return correct platform info for Windows and Edge', () => {
vi.mocked(platformUtils.getPlatform).mockReturnValue('Windows');
vi.mocked(platformUtils.getBrowser).mockReturnValue('Edge');
vi.mocked(platformUtils.isInStandaloneMode).mockReturnValue(false);
vi.mocked(platformUtils.isSonomaOrLaterSafari).mockReturnValue(false);

const { result } = renderHook(() => usePlatform());

expect(result.current).toEqual({
isApple: false,
isChrome: false,
isChromium: true,
isEdge: true,
isIOS: false,
isMacOS: false,
isPWA: false,
isSafari: false,
isSonomaOrLaterSafari: false,
isSupportInstallPWA: true,
});
});
});
21 changes: 19 additions & 2 deletions src/hooks/usePlatform.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
import { useRef } from 'react';

import { getBrowser, getPlatform } from '@/utils/platform';
import {
getBrowser,
getPlatform,
isInStandaloneMode,
isSonomaOrLaterSafari,
} from '@/utils/platform';

export const usePlatform = () => {
const platform = useRef(getPlatform());
const browser = useRef(getBrowser());
return {

const platformInfo = {
isApple: platform.current && ['Mac OS', 'iOS'].includes(platform.current),
isChrome: browser.current === 'Chrome',
isChromium: browser.current && ['Chrome', 'Edge', 'Opera', 'Brave'].includes(browser.current),
isEdge: browser.current === 'Edge',
isIOS: platform.current === 'iOS',
isMacOS: platform.current === 'Mac OS',
isPWA: isInStandaloneMode(),
isSafari: browser.current === 'Safari',
isSonomaOrLaterSafari: isSonomaOrLaterSafari(),
};

return {
...platformInfo,
isSupportInstallPWA:
(platformInfo.isChromium && !platformInfo.isIOS) ||
(platformInfo.isMacOS && platformInfo.isSonomaOrLaterSafari),
};
};
10 changes: 0 additions & 10 deletions src/utils/matchMedia.ts

This file was deleted.

Loading

0 comments on commit 7fee545

Please sign in to comment.