Skip to content

Commit

Permalink
아이폰용, 안드로이드용 어플 설치 여부를 홈 화면에서 보여주기 (feat.PWA) (#345)
Browse files Browse the repository at this point in the history
* feat: (#342) 안드로이드용 화면 구현

* feat: (#342) 아이폰용 화면 구현

* feat: (#342) 모바일 안드로이드, IOS에서 접속했을 경우 하단에 설치를 해달라는 문구 보이도록 구현

* style: (#342) 볼더 탑 연하게 수정, 큰 화면에서도 자연스럽게 보이도록 수정

* 사이트를 웹앱으로 실행되도록 하고, meta, favicon 설정 (#320)

* feat: (#319) favicon 및 디바이스별로 보여줄 보투게더 로고 이미지 적용

* feat: (#319) 모바일 즐겨찾기 후 사용시 웹앱 네이티브로 보이도록 선언
아이패드가 켜질 때 로딩중 화면 설정

* feat: (#319) 라인, 카카오톡 공유시 사이트 정보를 미리볼 수 있도록 설정

* feat: (#319) 모바일 환경에서 사이트에 접근할 경우 홈으로 즐겨찾기 여부를 묻는 기능 구현

* refactor: (#319) 모바일 사용자에게 즐겨찾기를 묻는 함수 코드 가독성 개선

* fix: (#319) 모바일 디바이스에 물어보도록 수정

* feat: (#310) PWA(프로그레시브 웹 앱) 요소를 충족시키는 조건 설정

* chore: (#319) 사용하지 않는 코드 삭제

* chore: (#319) EOL을 위한 개행 추가

* refactor: (#342) beforeinstallprompt 이벤트에 대한 타입 선언 및 적용

* refactor: (#342) 사용자에게 보여주는 문구 수정 및 사용하지 않는 코드 삭제
  • Loading branch information
Gilpop8663 authored and tjdtls690 committed Sep 12, 2023
1 parent c69ec15 commit 616674e
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 1 deletion.
3 changes: 3 additions & 0 deletions frontend/src/assets/arrow-up-on-square.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 frontend/src/assets/x_mark_black.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Meta, StoryObj } from '@storybook/react';

import MobileInstallPrompt from '.';

const meta: Meta<typeof MobileInstallPrompt> = {
component: MobileInstallPrompt,
};

export default meta;
type Story = StoryObj<typeof MobileInstallPrompt>;

export const Android: Story = {
render: () => (
<MobileInstallPrompt
platform="android"
handleInstallClick={() => {}}
handleCancelClick={() => {}}
/>
),
};

export const Ios: Story = {
render: () => (
<MobileInstallPrompt
platform="ios"
handleInstallClick={() => {}}
handleCancelClick={() => {}}
/>
),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import arrowUp from '@assets/arrow-up-on-square.svg';
import logo from '@assets/logo.svg';
import cancel from '@assets/x_mark_black.svg';

import * as S from './style';

interface MobileInstallPromptProps {
platform: 'ios' | 'android';
handleInstallClick: () => void;
handleCancelClick: () => void;
}

export default function MobileInstallPrompt({
platform,
handleInstallClick,
handleCancelClick,
}: MobileInstallPromptProps) {
return (
<S.Container>
<S.Content>
<S.Header>
<S.LogoImage src={logo} alt="보투게더 로고 이미지" />
<S.HeaderContent>
<S.HeaderTop>
<S.Title>VoTogether</S.Title>
<S.CancelButton onClick={handleCancelClick}>
<S.IconImage src={cancel} alt="취소 아이콘" />
</S.CancelButton>
</S.HeaderTop>
<S.Description>
VoTogether는 앱처럼 원활히 사용할 수 있습니다. 설치하시겠습니까?
</S.Description>
</S.HeaderContent>
</S.Header>
{platform === 'ios' && (
<S.IosContainer>
<S.Description>
브라우저 메뉴바에서 <S.IconImage src={arrowUp} alt="추가하기 아이콘" /> 모양 버튼을
눌러 "홈 화면에 추가하기"를 통해 설치를 할 수 있습니다.
</S.Description>
</S.IosContainer>
)}
{platform === 'android' && (
<S.InstallButton onClick={handleInstallClick}>홈 화면에 추가</S.InstallButton>
)}
</S.Content>
</S.Container>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { styled } from 'styled-components';

import { theme } from '@styles/theme';

export const Container = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: 100vw;
border-top: 1px solid rgba(0, 0, 0, 0.3);
position: fixed;
bottom: 0;
left: 0;
background-color: white;
z-index: ${theme.zIndex.modal};
`;

export const Content = styled.div`
display: flex;
flex-direction: column;
justify-content: space-between;
width: max-content;
padding: 30px 20px;
`;

export const Header = styled.div`
display: flex;
justify-content: space-between;
margin-bottom: 50px;
`;

export const LogoImage = styled.img`
border-radius: 16px;
width: 80px;
height: 80px;
`;

export const HeaderContent = styled.div`
display: flex;
flex-direction: column;
margin-left: 24px;
`;

export const HeaderTop = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
`;

export const Title = styled.span`
font-size: 2.4rem;
font-weight: 700;
`;

export const Description = styled.p`
font-size: 1.6rem;
font-weight: 700;
`;

export const CancelButton = styled.button`
padding: 10px;
position: relative;
bottom: 10px;
left: 10px;
cursor: pointer;
`;

export const IconImage = styled.img`
width: 24px;
height: 24px;
`;

export const InstallButton = styled.button`
align-self: end;
border-radius: 6px;
width: 190px;
height: 40px;
font-size: 1.6rem;
font-weight: 500;
color: white;
background-color: #5383ed;
cursor: pointer;
`;

export const IosContainer = styled.div`
display: flex;
align-items: center;
align-self: end;
gap: 8px;
`;
49 changes: 49 additions & 0 deletions frontend/src/components/common/AppInstallPrompt/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Fragment, useEffect, useState } from 'react';

import { BeforeInstallPromptEvent } from '../../../../window';

import MobileInstallPrompt from './MobileInstallPrompt';

export default function AppInstallPrompt() {
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
const isDeviceIOS = /iPad|iPhone|iPod/.test(window.navigator.userAgent);

useEffect(() => {
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);

return () => {
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
};
}, []);

const handleBeforeInstallPrompt = (event: BeforeInstallPromptEvent) => {
event.preventDefault();
setDeferredPrompt(event);
};

const handleInstallClick = () => {
if (deferredPrompt) {
deferredPrompt.prompt();

deferredPrompt.userChoice.then(() => {
setDeferredPrompt(null);
});
}
};

const handleCancelClick = () => {
setDeferredPrompt(null);
};

return (
<Fragment>
{deferredPrompt && (
<MobileInstallPrompt
handleInstallClick={handleInstallClick}
handleCancelClick={handleCancelClick}
platform={isDeviceIOS ? 'ios' : 'android'}
/>
)}
</Fragment>
);
}
2 changes: 2 additions & 0 deletions frontend/src/pages/Home/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import AppInstallPrompt from '@components/common/AppInstallPrompt';
import Layout from '@components/common/Layout';
import PostListPage from '@components/post/PostListPage';

export default function Home() {
return (
<Layout isSidebarVisible={true}>
<PostListPage />
<AppInstallPrompt />
</Layout>
);
}
9 changes: 8 additions & 1 deletion frontend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@
},
"outDir": "./dist"
},
"include": ["src", "src/custom.d.ts", "__test__", "styled-components.d.ts", "env.d.ts"],
"include": [
"src",
"src/custom.d.ts",
"__test__",
"styled-components.d.ts",
"env.d.ts",
"window.d.ts"
],
"exclude": ["node_modules"]
}
14 changes: 14 additions & 0 deletions frontend/window.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface BeforeInstallPromptEvent extends Event {
readonly platforms: string[];
readonly userChoice: Promise<{
outcome: 'accepted' | 'dismissed';
platform: string;
}>;
prompt(): Promise<void>;
}

declare global {
interface WindowEventMap {
beforeinstallprompt: BeforeInstallPromptEvent;
}
}

0 comments on commit 616674e

Please sign in to comment.