diff --git a/src/atoms/event2023FallInfo.ts b/src/atoms/event2023FallInfo.ts index aa8456747..0ab1c85d9 100644 --- a/src/atoms/event2023FallInfo.ts +++ b/src/atoms/event2023FallInfo.ts @@ -1,10 +1,13 @@ +import { Quest, QuestId } from "types/event2023fall"; + import { atom } from "recoil"; export type Event2023FallInfoType = Nullable<{ creditAmount: number; - eventStatus: string[]; + completedQuests: QuestId[]; ticket1Amount: number; ticket2Amount: number; + quests: Quest[]; }>; const event2023FallInfoAtom = atom({ diff --git a/src/components/Event/EventProvider/index.tsx b/src/components/Event/EventProvider/index.tsx deleted file mode 100644 index 2de19f5f8..000000000 --- a/src/components/Event/EventProvider/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -const EventProvider = () => { - return null; -}; - -export default EventProvider; diff --git a/src/components/ModalPopup/ModalChatPayment.tsx b/src/components/ModalPopup/ModalChatPayment.tsx index e65a454c0..775e9fecc 100644 --- a/src/components/ModalPopup/ModalChatPayment.tsx +++ b/src/components/ModalPopup/ModalChatPayment.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import useAccountFromChats from "hooks/chat/useAccountFromChats"; +import { useEvent2023FallQuestComplete } from "hooks/event/useEvent2023FallQuestComplete"; import { useValueRecoilState } from "hooks/useFetchRecoilState"; import { useAxios } from "hooks/useTaxiAPI"; @@ -22,7 +23,7 @@ import LocalAtmRoundedIcon from "@mui/icons-material/LocalAtmRounded"; import { ReactComponent as KakaoPayLogo } from "static/assets/serviceLogos/KakaoPayLogo.svg"; import { ReactComponent as TossLogo } from "static/assets/serviceLogos/TossLogo.svg"; -type ModalChatSettlementProps = Omit< +type ModalChatPaymentProps = Omit< Parameters[0], "padding" | "children" | "onEnter" > & { @@ -31,12 +32,12 @@ type ModalChatSettlementProps = Omit< account: ReturnType; }; -const ModalChatSettlement = ({ +const ModalChatPayment = ({ roomInfo, account, onRecall, ...modalProps -}: ModalChatSettlementProps) => { +}: ModalChatPaymentProps) => { const axios = useAxios(); const setAlert = useSetRecoilState(alertAtom); const { oid: userOid } = useValueRecoilState("loginInfo") || {}; @@ -49,6 +50,9 @@ const ModalChatSettlement = ({ [userOid, roomInfo] ); const onCopy = useCallback(() => setIsCopied(true), [setIsCopied]); + //#region event2023Fall + const event2023FallQuestComplete = useEvent2023FallQuestComplete(); + //#endregion useEffect(() => { if (isCopied) { @@ -65,6 +69,10 @@ const ModalChatSettlement = ({ method: "post", data: { roomId: roomInfo._id }, onSuccess: () => { + //#region event2023Fall + event2023FallQuestComplete("payingAndSending"); + event2023FallQuestComplete("paying"); + //#endregion modalProps.onChangeIsOpen?.(false); onRecall?.(); }, @@ -209,4 +217,4 @@ const ModalChatSettlement = ({ ); }; -export default ModalChatSettlement; +export default ModalChatPayment; diff --git a/src/components/ModalPopup/ModalChatSaveAccount.tsx b/src/components/ModalPopup/ModalChatSaveAccount.tsx index 7031080c2..4b4b7c857 100644 --- a/src/components/ModalPopup/ModalChatSaveAccount.tsx +++ b/src/components/ModalPopup/ModalChatSaveAccount.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useState } from "react"; +import { useEvent2023FallQuestComplete } from "hooks/event/useEvent2023FallQuestComplete"; import { useFetchRecoilState, useValueRecoilState, @@ -31,6 +32,9 @@ const ModalChatSaveAcount = ({ const { account: accountOrigin } = useValueRecoilState("loginInfo") || {}; const [account, setAccount] = useState(accountDefault || ""); const fetchLoginInfo = useFetchRecoilState("loginInfo"); + //#region event2023Fall + const event2023FallQuestComplete = useEvent2023FallQuestComplete(); + //#endregion useEffect(() => setAccount(accountDefault || ""), [accountDefault]); @@ -40,10 +44,15 @@ const ModalChatSaveAcount = ({ url: "/users/editAccount", method: "post", data: { account }, - onSuccess: () => fetchLoginInfo(), + onSuccess: () => { + //#region event2023Fall + event2023FallQuestComplete("accountChanging"); + //#endregion + fetchLoginInfo(); + }, onError: () => setAlert("계좌번호 저장을 실패하였습니다."), }); - }, [account]); + }, [account, event2023FallQuestComplete]); const styleTitle = { ...theme.font18, diff --git a/src/components/ModalPopup/ModalChatSettlement.tsx b/src/components/ModalPopup/ModalChatSettlement.tsx index ff3824581..01817f6a8 100644 --- a/src/components/ModalPopup/ModalChatSettlement.tsx +++ b/src/components/ModalPopup/ModalChatSettlement.tsx @@ -1,6 +1,7 @@ import { useMemo, useRef, useState } from "react"; import useSendMessage from "hooks/chat/useSendMessage"; +import { useEvent2023FallQuestComplete } from "hooks/event/useEvent2023FallQuestComplete"; import { useValueRecoilState } from "hooks/useFetchRecoilState"; import { useAxios } from "hooks/useTaxiAPI"; @@ -39,6 +40,7 @@ const ModalChatSettlement = ({ const isValidAccount = useMemo(() => regExpTest.account(account), [account]); const isRequesting = useRef(false); const sendMessage = useSendMessage(roomInfo._id, isRequesting); + const event2023FallQuestComplete = useEvent2023FallQuestComplete(); const onClickOk = () => { if (isRequesting.current || !isValidAccount) return; @@ -55,6 +57,10 @@ const ModalChatSettlement = ({ isRequesting.current = false; if (account !== defaultAccount) openSaveAccountModal?.(account); } + //#region event2023Fall + event2023FallQuestComplete("payingAndSending"); + event2023FallQuestComplete("sending"); + //#endregion modalProps.onChangeIsOpen?.(false); }, onError: () => { diff --git a/src/components/ModalPopup/ModalEvent2023FallItem.tsx b/src/components/ModalPopup/ModalEvent2023FallItem.tsx index e5bd57a92..10323f8cf 100644 --- a/src/components/ModalPopup/ModalEvent2023FallItem.tsx +++ b/src/components/ModalPopup/ModalEvent2023FallItem.tsx @@ -49,7 +49,12 @@ const ModalEvent2023FallItem = ({ }, onError: () => setAlert("구매를 실패하였습니다."), }), - [itemInfo._id, fetchItems, modalProps.onChangeIsOpen] + [ + itemInfo._id, + fetchItems, + modalProps.onChangeIsOpen, + fetchEvent2023FallInfo, + ] ); const [isDisabled, buttonText] = useMemo( diff --git a/src/components/ModalPopup/ModalMypageModify.tsx b/src/components/ModalPopup/ModalMypageModify.tsx index d0a21971d..3aef64390 100644 --- a/src/components/ModalPopup/ModalMypageModify.tsx +++ b/src/components/ModalPopup/ModalMypageModify.tsx @@ -2,6 +2,7 @@ import axiosOri from "axios"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; +import { useEvent2023FallQuestComplete } from "hooks/event/useEvent2023FallQuestComplete"; import { useFetchRecoilState, useValueRecoilState, @@ -152,6 +153,9 @@ const ModalMypageModify = ({ const loginInfo = useValueRecoilState("loginInfo"); const fetchLoginInfo = useFetchRecoilState("loginInfo"); + //#region event2023Fall + const event2023FallQuestComplete = useEvent2023FallQuestComplete(); + //#endregion const setAlert = useSetRecoilState(alertAtom); useEffect(() => { @@ -174,6 +178,7 @@ const ModalMypageModify = ({ method: "post", data: { nickname }, onError: () => setAlert(t("page_modify.nickname_failed")), + onSuccess: () => event2023FallQuestComplete("nicknameChanging"), // event2023Fall }); } if (account !== loginInfo?.account) { @@ -183,6 +188,7 @@ const ModalMypageModify = ({ method: "post", data: { account }, onError: () => setAlert(t("page_modify.account_failed")), + onSuccess: () => event2023FallQuestComplete("accountChanging"), // event2023Fall }); } if (isNeedToUpdateLoginInfo) { diff --git a/src/components/ModalPopup/ModalRoomShare.tsx b/src/components/ModalPopup/ModalRoomShare.tsx index c6cf02a11..6bd8cf45b 100644 --- a/src/components/ModalPopup/ModalRoomShare.tsx +++ b/src/components/ModalPopup/ModalRoomShare.tsx @@ -1,3 +1,5 @@ +import { useEvent2023FallQuestComplete } from "hooks/event/useEvent2023FallQuestComplete"; + import Modal from "components/Modal"; import BodyRoomShare, { BodyRoomShareProps } from "./Body/BodyRoomShare"; @@ -17,6 +19,9 @@ const ModalRoomShare = ({ onChangeIsOpen, roomInfo, }: ModalRoomShareProps) => { + //#region event2023Fall + const event2023FallQuestComplete = useEvent2023FallQuestComplete(); + //#endregion const styleTitle = { ...theme.font18, display: "flex", @@ -27,11 +32,17 @@ const ModalRoomShare = ({ fontSize: "21px", margin: "0 4px 0 0", }; + //#region Event2023Fall + const onChangeIsOpenWithEvent2023Fall = (isOpen: boolean) => { + onChangeIsOpen?.(isOpen); + !isOpen && event2023FallQuestComplete("roomSharing"); + }; + //#endregion return (
diff --git a/src/components/Skeleton/index.tsx b/src/components/Skeleton/index.tsx index ba58d6298..cd81a8c69 100644 --- a/src/components/Skeleton/index.tsx +++ b/src/components/Skeleton/index.tsx @@ -1,6 +1,7 @@ import { ReactNode, useMemo } from "react"; import { useLocation } from "react-router-dom"; +import { useEventEffect } from "hooks/event/useEventEffect"; import useCSSVariablesEffect from "hooks/skeleton/useCSSVariablesEffect"; import useChannelTalkEffect from "hooks/skeleton/useChannelTalkEffect"; import useFirebaseMessagingEffect from "hooks/skeleton/useFirebaseMessagingEffect"; @@ -63,6 +64,9 @@ const Skeleton = ({ children }: SkeletonProps) => { [pathname] ); + //#region event2023Fall + useEventEffect(); + //#endregion useSyncRecoilStateEffect(); // loginIngo, taxiLocations, myRooms, notificationOptions 초기화 및 동기화 useI18nextEffect(); useScrollRestorationEffect(); diff --git a/src/hooks/event/useEvent2023FallQuestComplete.ts b/src/hooks/event/useEvent2023FallQuestComplete.ts new file mode 100644 index 000000000..5dda28b06 --- /dev/null +++ b/src/hooks/event/useEvent2023FallQuestComplete.ts @@ -0,0 +1,28 @@ +import { useCallback } from "react"; + +import type { QuestId } from "types/event2023fall"; + +import { + useFetchRecoilState, + useValueRecoilState, +} from "hooks/useFetchRecoilState"; + +export const useEvent2023FallQuestComplete = () => { + const fetchEvent2023FallInfo = useFetchRecoilState("event2023FallInfo"); + + const { completedQuests, quests } = + useValueRecoilState("event2023FallInfo") || {}; + + return useCallback( + (id: QuestId) => { + if (!completedQuests || !quests) return; + const questMaxCount = + quests?.find((quest) => quest.id === id)?.maxCount || 0; + const questCompletedCount = completedQuests?.filter( + (questId) => questId === id + ).length; + questCompletedCount < questMaxCount && fetchEvent2023FallInfo(); + }, + [completedQuests, fetchEvent2023FallInfo, quests] + ); +}; diff --git a/src/hooks/event/useEventEffect.ts b/src/hooks/event/useEventEffect.ts new file mode 100644 index 000000000..87c34d623 --- /dev/null +++ b/src/hooks/event/useEventEffect.ts @@ -0,0 +1,40 @@ +import { useEffect, useRef } from "react"; + +import type { QuestId } from "types/event2023fall"; + +import { useValueRecoilState } from "hooks/useFetchRecoilState"; + +import { sendPopupInAppNotificationEventToFlutter } from "tools/sendEventToFlutter"; + +export const useEventEffect = () => { + const { completedQuests, quests } = + useValueRecoilState("event2023FallInfo") || {}; + + const prevEventStatusRef = useRef(); + + useEffect(() => { + if (!completedQuests || !quests) return; + prevEventStatusRef.current = prevEventStatusRef.current || completedQuests; + if (prevEventStatusRef.current.length === completedQuests.length) return; + + const questsForCompare = [...completedQuests]; + prevEventStatusRef.current.forEach((questId) => { + const index = questsForCompare.indexOf(questId); + if (index < 0) return; + questsForCompare.splice(index, 1); + }); + questsForCompare.forEach((questId) => { + const quest = quests.find(({ id }) => id === questId); + if (!quest) return; + sendPopupInAppNotificationEventToFlutter({ + type: "default", + imageUrl: quest.imageUrl, + title: "퀘스트 완료", + subtitle: quest.name, + content: quest.description, + button: { text: "확인하기", path: "/event/2023fall-missions" }, + }); + }); + prevEventStatusRef.current = completedQuests; + }, [completedQuests]); +}; diff --git a/src/hooks/skeleton/useFlutterEventCommunicationEffect.tsx b/src/hooks/skeleton/useFlutterEventCommunicationEffect.tsx index 524c69c69..aa46f2e98 100644 --- a/src/hooks/skeleton/useFlutterEventCommunicationEffect.tsx +++ b/src/hooks/skeleton/useFlutterEventCommunicationEffect.tsx @@ -154,12 +154,35 @@ export const sendClipboardCopyEventToFlutter = async (value: string) => { } }; +export const sendPopupInAppNotificationEventToFlutter = async ( + value: ( + | { type: "chat"; profileUrl?: string; imageUrl?: never } + | { type: "default"; imageUrl?: string; profileUrl?: never } + ) & { + title?: string; + subtitle?: string; + content?: string; + button?: { text: string; path: string }; + } +) => { + console.log("fake notification call", value); + if (!isWebViewInFlutter) return true; + try { + await window.flutter_inappwebview.callHandler( + "popup_inAppNotification", + value + ); + } catch (e) { + console.error(e); + } +}; + // 알림을 "on"으로 설정 시 Flutter에게 이벤트를 전달하고 앱의 알림 설정 여부를 반환받습니다. export const sendPopupInstagramStoryShareToFlutter = async (value: { backgroundLayerUrl: string; stickerLayerUrl: string; }) => { - console.log(value); + console.log("fake instagram call", value); if (!isWebViewInFlutter) return true; try { await window.flutter_inappwebview.callHandler( diff --git a/src/index.tsx b/src/index.tsx index d9fee51da..48b4dda48 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,7 +3,6 @@ import { CookiesProvider } from "react-cookie"; import { createRoot } from "react-dom/client"; import { BrowserRouter as Router } from "react-router-dom"; -import EventProvider from "components/Event/EventProvider"; import Loading from "components/Loading"; import ModalProvider from "components/Modal/ModalProvider"; import Skeleton from "components/Skeleton"; @@ -23,7 +22,6 @@ const App = () => ( - }> diff --git a/src/pages/Addroom/index.tsx b/src/pages/Addroom/index.tsx index a6a1c8939..002f7c2e6 100644 --- a/src/pages/Addroom/index.tsx +++ b/src/pages/Addroom/index.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useCookies } from "react-cookie"; import { useHistory } from "react-router-dom"; +import { useEvent2023FallQuestComplete } from "hooks/event/useEvent2023FallQuestComplete"; import { useFetchRecoilState, useValueRecoilState, @@ -59,6 +60,9 @@ const AddRoom = () => { const isLogin = !!useValueRecoilState("loginInfo")?.id; const myRooms = useValueRecoilState("myRooms"); const fetchMyRooms = useFetchRecoilState("myRooms"); + //#region event2023Fall + const event2023FallQuestComplete = useEvent2023FallQuestComplete(); + //#endregion useEffect(() => { const expirationDate = new Date(); @@ -111,6 +115,9 @@ const AddRoom = () => { }, onSuccess: () => { fetchMyRooms(); + //#region event2023Fall + event2023FallQuestComplete("firstRoomCreation"); + //#endregion history.push("/myroom"); }, onError: () => setAlert("방 개설에 실패하였습니다."), diff --git a/src/tools/sendEventToFlutter.ts b/src/tools/sendEventToFlutter.ts index 9acc734f6..4cdbaae48 100644 --- a/src/tools/sendEventToFlutter.ts +++ b/src/tools/sendEventToFlutter.ts @@ -3,4 +3,5 @@ export { sendAuthLogoutEventToFlutter, sendTryNotificationEventToFlutter, sendClipboardCopyEventToFlutter, + sendPopupInAppNotificationEventToFlutter, } from "hooks/skeleton/useFlutterEventCommunicationEffect"; diff --git a/src/types/event2023fall.d.ts b/src/types/event2023fall.d.ts index d8d5452fc..b93ea3982 100644 --- a/src/types/event2023fall.d.ts +++ b/src/types/event2023fall.d.ts @@ -9,6 +9,28 @@ export type EventItem = { itemType: number; }; +export type Quest = { + description: string; + id: QuestId; + imageUrl: string; + maxCount: number; + name: string; + reward: { credit: number; ticket1?: number; ticket2?: number }; +}; + +export type QuestId = + | "firstLogin" + | "payingAndSending" + | "firstRoomCreation" + | "roomSharing" + | "paying" + | "sending" + | "nicknameChanging" + | "accountChanging" + | "adPushAgreement" + | "eventSharingOnInstagram" + | "purchaseSharingOnInstagram"; + export type Transaction = { _id: string; type: "get" | "use";