diff --git a/src/App.tsx b/src/App.tsx index 86b7598..081837d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,11 @@ import { Outlet } from "react-router-dom"; +import { useChannelTalk } from "@/hooks/common/useChannelTalk"; import useHotjar from "@/hooks/common/useHotjar"; const App = () => { useHotjar(); + useChannelTalk(); return (
diff --git a/src/assets/svg/ic-logo.svg b/src/assets/svg/ic-logo.svg deleted file mode 100644 index 263293a..0000000 --- a/src/assets/svg/ic-logo.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/assets/svg/ic-share.svg b/src/assets/svg/ic-share.svg new file mode 100644 index 0000000..61499ca --- /dev/null +++ b/src/assets/svg/ic-share.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ChannelTalk/channelTalk.ts b/src/components/ChannelTalk/channelTalk.ts new file mode 100644 index 0000000..e366b3d --- /dev/null +++ b/src/components/ChannelTalk/channelTalk.ts @@ -0,0 +1,251 @@ +/* eslint-disable prefer-rest-params */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +declare global { + interface Window { + ChannelIO?: IChannelIO; + + ChannelIOInitialized?: boolean; + } +} + +interface IChannelIO { + c?: (...args: any) => void; + + q?: [methodName: string, ...args: any[]][]; + + (...args: any): void; +} + +interface BootOption { + appearance?: string; + + customLauncherSelector?: string; + + hideChannelButtonOnBoot?: boolean; + + hidePopup?: boolean; + + language?: string; + + memberHash?: string; + + memberId?: string; + + pluginKey: string; + + profile?: Profile; + + trackDefaultEvent?: boolean; + + trackUtmSource?: boolean; + + unsubscribe?: boolean; + + unsubscribeEmail?: boolean; + + unsubscribeTexting?: boolean; + + zIndex?: number; +} + +interface Callback { + (error: Error | null, user: CallbackUser | null): void; +} + +interface CallbackUser { + alert: number; + + avatarUrl: string; + + id: string; + + language: string; + + memberId: string; + + name?: string; + + profile?: Profile | null; + + tags?: string[] | null; + + unsubscribeEmail: boolean; + + unsubscribeTexting: boolean; +} + +interface UpdateUserInfo { + language?: string; + + profile?: Profile | null; + + profileOnce?: Profile; + + tags?: string[] | null; + + unsubscribeEmail?: boolean; + + unsubscribeTexting?: boolean; +} + +interface Profile { + [key: string]: string | number | boolean | null | undefined; +} + +interface FollowUpProfile { + name?: string | null; + + mobileNumber?: string | null; + + email?: string | null; +} + +interface EventProperty { + [key: string]: string | number | boolean | null | undefined; +} + +type Appearance = "light" | "dark" | "system" | null; + +class ChannelService { + loadScript() { + (function () { + const w = window; + + if (w.ChannelIO || w.ChannelIOInitialized) { + return; + } + + if (w.ChannelIO) { + return w.console.error("ChannelIO script included twice."); + } + + const ch: IChannelIO = function () { + ch.c?.(arguments); + }; + + ch.q = []; + + ch.c = function (args) { + ch.q?.push(args); + }; + + w.ChannelIO = ch; + + function l() { + if (w.ChannelIOInitialized) { + return; + } + + w.ChannelIOInitialized = true; + + const s = document.createElement("script"); + + s.type = "text/javascript"; + + s.async = true; + + s.src = "https://cdn.channel.io/plugin/ch-plugin-web.js"; + + const x = document.getElementsByTagName("script")[0]; + + if (x.parentNode) { + x.parentNode.insertBefore(s, x); + } + } + + if (document.readyState === "complete") { + l(); + } else { + w.addEventListener("DOMContentLoaded", l); + + w.addEventListener("load", l); + } + })(); + } + + boot(option: BootOption, callback?: Callback) { + window.ChannelIO?.("boot", option, callback); + } + + shutdown() { + window.ChannelIO?.("shutdown"); + } + + showMessenger() { + window.ChannelIO?.("showMessenger"); + } + + hideMessenger() { + window.ChannelIO?.("hideMessenger"); + } + + openChat(chatId?: string | number, message?: string) { + window.ChannelIO?.("openChat", chatId, message); + } + + track(eventName: string, eventProperty?: EventProperty) { + window.ChannelIO?.("track", eventName, eventProperty); + } + + onShowMessenger(callback: () => void) { + window.ChannelIO?.("onShowMessenger", callback); + } + + onHideMessenger(callback: () => void) { + window.ChannelIO?.("onHideMessenger", callback); + } + + onBadgeChanged(callback: (unread: number, alert: number) => void) { + window.ChannelIO?.("onBadgeChanged", callback); + } + + onChatCreated(callback: () => void) { + window.ChannelIO?.("onChatCreated", callback); + } + + onFollowUpChanged(callback: (profile: FollowUpProfile) => void) { + window.ChannelIO?.("onFollowUpChanged", callback); + } + + onUrlClicked(callback: (url: string) => void) { + window.ChannelIO?.("onUrlClicked", callback); + } + + clearCallbacks() { + window.ChannelIO?.("clearCallbacks"); + } + + updateUser(userInfo: UpdateUserInfo, callback?: Callback) { + window.ChannelIO?.("updateUser", userInfo, callback); + } + + addTags(tags: string[], callback?: Callback) { + window.ChannelIO?.("addTags", tags, callback); + } + + removeTags(tags: string[], callback?: Callback) { + window.ChannelIO?.("removeTags", tags, callback); + } + + setPage(page: string) { + window.ChannelIO?.("setPage", page); + } + + resetPage() { + window.ChannelIO?.("resetPage"); + } + + showChannelButton() { + window.ChannelIO?.("showChannelButton"); + } + + hideChannelButton() { + window.ChannelIO?.("hideChannelButton"); + } + + setAppearance(appearance: Appearance) { + window.ChannelIO?.("setAppearance", appearance); + } +} + +export default new ChannelService(); diff --git a/src/components/Navbar/Navbar.stories.tsx b/src/components/Navbar/Navbar.stories.tsx index baad3fb..511e64a 100644 --- a/src/components/Navbar/Navbar.stories.tsx +++ b/src/components/Navbar/Navbar.stories.tsx @@ -27,9 +27,6 @@ export const HomeNavbar: Story = { name: "HomeNavbar", render: () => ( - - - 앱 공유하기 diff --git a/src/components/ui/Icon/Icon.tsx b/src/components/ui/Icon/Icon.tsx index fc51337..8fec8fc 100644 --- a/src/components/ui/Icon/Icon.tsx +++ b/src/components/ui/Icon/Icon.tsx @@ -4,9 +4,9 @@ import CloseIcon from "@/assets/svg/ic-close.svg?react"; import EmptyCircleIcon from "@/assets/svg/ic-empty-circle.svg?react"; import GalleryIcon from "@/assets/svg/ic-gallery.svg?react"; import LeftArrowIcon from "@/assets/svg/ic-left-arrow.svg?react"; -import LogoIcon from "@/assets/svg/ic-logo.svg?react"; import PasteIcon from "@/assets/svg/ic-paste.svg?react"; import PlusIcon from "@/assets/svg/ic-plus.svg?react"; +import ShareIcon from "@/assets/svg/ic-share.svg?react"; export type IconNameType = | "camera" @@ -17,7 +17,7 @@ export type IconNameType = | "plus" | "checkCircle" | "emptyCircle" - | "logo"; + | "share"; export interface IconProps { name: IconNameType; @@ -32,7 +32,7 @@ export const ICONS = { plus: PlusIcon, checkCircle: CheckCircleIcon, emptyCircle: EmptyCircleIcon, - logo: LogoIcon, + share: ShareIcon, }; // 추후 사이즈, 컬러등 추가 가능 diff --git a/src/hooks/common/useChannelTalk.ts b/src/hooks/common/useChannelTalk.ts new file mode 100644 index 0000000..6d64833 --- /dev/null +++ b/src/hooks/common/useChannelTalk.ts @@ -0,0 +1,20 @@ +import { useEffect } from "react"; + +import ChannelService from "@/components/ChannelTalk/channelTalk"; + +export const useChannelTalk = () => { + const openChannelTalk = () => { + ChannelService.showMessenger(); + }; + + useEffect(() => { + ChannelService.loadScript(); + + ChannelService.boot({ + pluginKey: import.meta.env.VITE_CHANNEL_TALK_KEY || "", + hideChannelButtonOnBoot: true, + }); + }, []); + + return { openChannelTalk }; +}; diff --git a/src/pages/HomePage/HomePage.module.scss b/src/pages/HomePage/HomePage.module.scss index bf2e739..12debbb 100644 --- a/src/pages/HomePage/HomePage.module.scss +++ b/src/pages/HomePage/HomePage.module.scss @@ -1,5 +1,27 @@ @use "@/styles/_mixins.scss" as *; +.FeedbackButton { + padding: 0.5rem 0.75rem; + background-color: var(--color-gray200); + border-radius: 0.75rem; + height: 2.25rem; + white-space: nowrap; + color: var(--color-text-primary); + line-height: 1.5rem; + align-items: center; + @include bodyM; +} + +.ShareButton { + width: 2.25rem; + height: 2.25rem; + background-color: var(--color-gray200); + border-radius: 0.75rem; + align-items: center; + justify-content: center; + margin-left: 0.625rem; +} + .Home { padding-left: 1.25rem; padding-right: 1.25rem; @@ -59,7 +81,7 @@ display: flex; justify-content: center; align-items: center; - margin-top: 3.75rem; + z-index: 1; & > img { width: 14.375rem; diff --git a/src/pages/HomePage/HomePage.tsx b/src/pages/HomePage/HomePage.tsx index b7344fa..c3f7f4d 100644 --- a/src/pages/HomePage/HomePage.tsx +++ b/src/pages/HomePage/HomePage.tsx @@ -3,9 +3,12 @@ import { useEffect } from "react"; import Navbar from "@/components/Navbar/Navbar"; import { AppBridgeMessageType } from "@/components/provider/AppBridgeProvider/AppBridgeMessage.types"; import { useAppBridge } from "@/components/provider/AppBridgeProvider/AppBridgeProvider"; +import { useUserAgent } from "@/components/provider/UserAgentProvider"; +import Icon from "@/components/ui/Icon/Icon"; import IconButton from "@/components/ui/IconButton/IconButton"; import Text from "@/components/ui/Text/Text"; +import { useChannelTalk } from "@/hooks/common/useChannelTalk"; import { useRoute } from "@/hooks/common/useRoute"; import styles from "@/pages/HomePage/HomePage.module.scss"; @@ -18,12 +21,25 @@ const SHARE_TEXT = "영수증을 촬영하면 AI가 자동으로 맛집 리뷰를 생성! 🍽️✨ 간편하게 추억을 남기고, 나만의 미식 기록을 완성하세요. 미식 경험을 더욱 스마트하게, 미식 MISIK!"; const HomePage = () => { + const { isIOS, isAndroid } = useUserAgent(); + + const { openChannelTalk } = useChannelTalk(); + const { send } = useAppBridge(); const { scanData } = useScanDataStore(); const { navigateToReceiptEdit, navigateToRecognitionFail } = useRoute(); + const getShareText = () => { + if (isIOS) { + return `${SHARE_TEXT} http://apps.apple.com/kr/app/id6741109313`; + } else if (isAndroid) { + return `${SHARE_TEXT} https://play.google.com/store/apps/details?id=com.nexters.misik`; + } + return SHARE_TEXT; + }; + useEffect(() => { if (scanData === "error") { navigateToRecognitionFail(); @@ -37,16 +53,20 @@ const HomePage = () => { return ( <> - { - gTagLogEvent("share_button"); - - send({ type: AppBridgeMessageType.SHARE, payload: { shareText: SHARE_TEXT } }); - }} - > - - 앱 공유하기 - + + + @@ -58,9 +78,10 @@ const HomePage = () => { 손쉬운 음식 리뷰 작성 -
- mainLogo -
+ + +
+ mainLogo
diff --git a/src/pages/SelectStylePage/SelectStylePage.module.scss b/src/pages/SelectStylePage/SelectStylePage.module.scss index dd88e33..a8acdbd 100644 --- a/src/pages/SelectStylePage/SelectStylePage.module.scss +++ b/src/pages/SelectStylePage/SelectStylePage.module.scss @@ -43,7 +43,6 @@ padding: 0.375rem 0.75rem; border-radius: 0.875rem; background-color: var(--color-white); - width: 5.9375rem; & > span { white-space: nowrap;