From 616674e9b3ef51e44750d709103bca6e9a2a9334 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EA=B9=80=EC=98=81=EA=B8=B8/KIM=20YOUNG=20GIL?=
<80146176+Gilpop8663@users.noreply.github.com>
Date: Sat, 12 Aug 2023 14:49:25 +0900
Subject: [PATCH] =?UTF-8?q?=EC=95=84=EC=9D=B4=ED=8F=B0=EC=9A=A9,=20?=
=?UTF-8?q?=EC=95=88=EB=93=9C=EB=A1=9C=EC=9D=B4=EB=93=9C=EC=9A=A9=20?=
=?UTF-8?q?=EC=96=B4=ED=94=8C=20=EC=84=A4=EC=B9=98=20=EC=97=AC=EB=B6=80?=
=?UTF-8?q?=EB=A5=BC=20=ED=99=88=20=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20?=
=?UTF-8?q?=EB=B3=B4=EC=97=AC=EC=A3=BC=EA=B8=B0=20(feat.PWA)=20(#345)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 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) 사용자에게 보여주는 문구 수정 및 사용하지 않는 코드 삭제
---
frontend/src/assets/arrow-up-on-square.svg | 3 +
frontend/src/assets/x_mark_black.svg | 3 +
.../MobileInstallPrompt.stories.tsx | 30 +++++
.../MobileInstallPrompt/index.tsx | 49 ++++++++
.../MobileInstallPrompt/style.ts | 107 ++++++++++++++++++
.../common/AppInstallPrompt/index.tsx | 49 ++++++++
frontend/src/pages/Home/index.tsx | 2 +
frontend/tsconfig.json | 9 +-
frontend/window.d.ts | 14 +++
9 files changed, 265 insertions(+), 1 deletion(-)
create mode 100644 frontend/src/assets/arrow-up-on-square.svg
create mode 100644 frontend/src/assets/x_mark_black.svg
create mode 100644 frontend/src/components/common/AppInstallPrompt/MobileInstallPrompt/MobileInstallPrompt.stories.tsx
create mode 100644 frontend/src/components/common/AppInstallPrompt/MobileInstallPrompt/index.tsx
create mode 100644 frontend/src/components/common/AppInstallPrompt/MobileInstallPrompt/style.ts
create mode 100644 frontend/src/components/common/AppInstallPrompt/index.tsx
create mode 100644 frontend/window.d.ts
diff --git a/frontend/src/assets/arrow-up-on-square.svg b/frontend/src/assets/arrow-up-on-square.svg
new file mode 100644
index 000000000..c6eca45db
--- /dev/null
+++ b/frontend/src/assets/arrow-up-on-square.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/assets/x_mark_black.svg b/frontend/src/assets/x_mark_black.svg
new file mode 100644
index 000000000..457d8e626
--- /dev/null
+++ b/frontend/src/assets/x_mark_black.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/components/common/AppInstallPrompt/MobileInstallPrompt/MobileInstallPrompt.stories.tsx b/frontend/src/components/common/AppInstallPrompt/MobileInstallPrompt/MobileInstallPrompt.stories.tsx
new file mode 100644
index 000000000..5de676e8d
--- /dev/null
+++ b/frontend/src/components/common/AppInstallPrompt/MobileInstallPrompt/MobileInstallPrompt.stories.tsx
@@ -0,0 +1,30 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import MobileInstallPrompt from '.';
+
+const meta: Meta = {
+ component: MobileInstallPrompt,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Android: Story = {
+ render: () => (
+ {}}
+ handleCancelClick={() => {}}
+ />
+ ),
+};
+
+export const Ios: Story = {
+ render: () => (
+ {}}
+ handleCancelClick={() => {}}
+ />
+ ),
+};
diff --git a/frontend/src/components/common/AppInstallPrompt/MobileInstallPrompt/index.tsx b/frontend/src/components/common/AppInstallPrompt/MobileInstallPrompt/index.tsx
new file mode 100644
index 000000000..973374b8e
--- /dev/null
+++ b/frontend/src/components/common/AppInstallPrompt/MobileInstallPrompt/index.tsx
@@ -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 (
+
+
+
+
+
+
+ VoTogether
+
+
+
+
+
+ VoTogether는 앱처럼 원활히 사용할 수 있습니다. 설치하시겠습니까?
+
+
+
+ {platform === 'ios' && (
+
+
+ 브라우저 메뉴바에서 모양 버튼을
+ 눌러 "홈 화면에 추가하기"를 통해 설치를 할 수 있습니다.
+
+
+ )}
+ {platform === 'android' && (
+ 홈 화면에 추가
+ )}
+
+
+ );
+}
diff --git a/frontend/src/components/common/AppInstallPrompt/MobileInstallPrompt/style.ts b/frontend/src/components/common/AppInstallPrompt/MobileInstallPrompt/style.ts
new file mode 100644
index 000000000..30e444909
--- /dev/null
+++ b/frontend/src/components/common/AppInstallPrompt/MobileInstallPrompt/style.ts
@@ -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;
+`;
diff --git a/frontend/src/components/common/AppInstallPrompt/index.tsx b/frontend/src/components/common/AppInstallPrompt/index.tsx
new file mode 100644
index 000000000..68c0e23f7
--- /dev/null
+++ b/frontend/src/components/common/AppInstallPrompt/index.tsx
@@ -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(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 (
+
+ {deferredPrompt && (
+
+ )}
+
+ );
+}
diff --git a/frontend/src/pages/Home/index.tsx b/frontend/src/pages/Home/index.tsx
index 006adadb6..7590b4bb8 100644
--- a/frontend/src/pages/Home/index.tsx
+++ b/frontend/src/pages/Home/index.tsx
@@ -1,3 +1,4 @@
+import AppInstallPrompt from '@components/common/AppInstallPrompt';
import Layout from '@components/common/Layout';
import PostListPage from '@components/post/PostListPage';
@@ -5,6 +6,7 @@ export default function Home() {
return (
+
);
}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index 23c9de5d5..63e5fb99d 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -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"]
}
diff --git a/frontend/window.d.ts b/frontend/window.d.ts
new file mode 100644
index 000000000..284bd9cce
--- /dev/null
+++ b/frontend/window.d.ts
@@ -0,0 +1,14 @@
+export interface BeforeInstallPromptEvent extends Event {
+ readonly platforms: string[];
+ readonly userChoice: Promise<{
+ outcome: 'accepted' | 'dismissed';
+ platform: string;
+ }>;
+ prompt(): Promise;
+}
+
+declare global {
+ interface WindowEventMap {
+ beforeinstallprompt: BeforeInstallPromptEvent;
+ }
+}