From de389e2da5c3957e21a71bdfc6f8b5e0caf3780f Mon Sep 17 00:00:00 2001 From: Daniil Chemerkin Date: Wed, 9 Apr 2025 07:46:07 +0000 Subject: [PATCH] AW-429-comp-v1-ios-ab --- .../pages/Camera/android/index.tsx | 391 ++++++++++++++++++ .../CompatibilityV2/pages/Camera/index.tsx | 387 +---------------- .../pages/Camera/iphone/index.tsx | 348 ++++++++++++++++ .../CompatibilityV2/pages/ScanHand/index.tsx | 15 +- .../pages/ScannedPhoto/index.tsx | 5 + src/hooks/ab/unleash/useUnleash.ts | 2 + src/services/metric/metricService.ts | 4 + src/services/permission/permisson.ts | 22 + 8 files changed, 794 insertions(+), 380 deletions(-) create mode 100644 src/components/CompatibilityV2/pages/Camera/android/index.tsx create mode 100644 src/components/CompatibilityV2/pages/Camera/iphone/index.tsx create mode 100644 src/services/permission/permisson.ts diff --git a/src/components/CompatibilityV2/pages/Camera/android/index.tsx b/src/components/CompatibilityV2/pages/Camera/android/index.tsx new file mode 100644 index 0000000..7b5c3b0 --- /dev/null +++ b/src/components/CompatibilityV2/pages/Camera/android/index.tsx @@ -0,0 +1,391 @@ + +import Modal from "@/components/Modal"; +import styles from "../styles.module.scss"; +import { useTranslations } from "@/hooks/translations"; +import { ELocalesPlacement } from "@/locales"; +import Title from "@/components/Title"; +import CameraModal from "@/components/CompatibilityV2/components/CameraModal"; +import Loader, { LoaderColor } from "@/components/Loader"; +import Toast from "@/components/pages/ABDesign/v1/components/Toast"; +import routes from "@/routes"; +import { useNavigate } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { actions } from "@/store"; +import { useApi } from "@/api"; +import { useDispatch } from "react-redux"; +import { IPalmistryFinger, IPalmistryLine } from "@/api/resources/Palmistry"; +import { ICompatibilityV2FingerLocal } from "@/store/compatibilityV2"; +import { DataURIToBlob } from "@/services/data"; +import metricService, { EGoals, EMetrics } from "@/services/metric/metricService"; +import { EUnleashFlags, useUnleash } from "@/hooks/ab/unleash/useUnleash"; +import { checkCameraPermissionState } from "@/services/permission/permisson"; + +enum EToastVisible { + "try_again" = "try_again", + "try_again_or_next" = "try_again_or_next", + "no_access_camera" = "no_access_camera", + "reload_page" = "reload_page", + "upload_photo" = "upload_photo", +} + +const isInstagramAndroid = window?.navigator?.userAgent?.includes("Instagram") && /Android/.test(window?.navigator?.userAgent); + +function AndroidCamera() { + const api = useApi(); + const dispatch = useDispatch(); + const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2); + const navigate = useNavigate(); + + const { isReady, variant: cameraRequestModalCompatibilityV2 } = useUnleash({ + flag: EUnleashFlags.cameraRequestModalCompatibilityV2 + }); + + const isShowCameraRequestModal = cameraRequestModalCompatibilityV2 !== "hide"; + + const { variant: compatibilityV2ScanHand } = useUnleash({ + flag: EUnleashFlags.compatibilityV2ScanHand + }); + + const isShowScanHand = compatibilityV2ScanHand !== "hide"; + + const [isCameraModalOpen, setIsCameraModalOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isRequestCameraModalOpen, setIsRequestCameraModalOpen] = useState(isShowCameraRequestModal); + const [toastVisible, setToastVisible] = useState(null); + + const handleToScanHand = () => { + metricService.reachGoal(EGoals.SCAN_ARTIFICIAL_PHOTO, [EMetrics.YANDEX, EMetrics.KLAVIYO]); + return navigate(routes.client.compatibilityV2ScanHand()) + } + + const handleToScannedPhoto = () => { + metricService.reachGoal(EGoals.CAMERA_SUCCESS, [EMetrics.YANDEX, EMetrics.KLAVIYO]); + return navigate(routes.client.compatibilityV2ScannedPhoto()) + } + + const handleCameraError = (error: string | DOMException) => { + console.log("camera error: ", error) + if (!isShowScanHand) { + return setToastVisible(EToastVisible.upload_photo) + } + return handleToScanHand() + } + + const handleCameraSuccess = (photo: string) => { + onTakePhoto(photo) + } + + + const handleRequestCameraModalCancel = () => { + setIsRequestCameraModalOpen(false) + setToastVisible(EToastVisible.no_access_camera) + } + + const handleRequestCameraModalAllow = () => { + if (isInstagramAndroid) { + return handleToScanHand() + } + setIsCameraModalOpen(true) + setIsRequestCameraModalOpen(false) + } + + useEffect(() => { + if (!isShowCameraRequestModal) { + if (isInstagramAndroid) { + return handleToScanHand() + } + setIsCameraModalOpen(true) + } + }, [isShowCameraRequestModal]) + + useEffect(() => { + (async () => { + const permissionState = await checkCameraPermissionState(); + if (permissionState === "denied") { + handleToScanHand() + } + })() + }, []) + + useEffect(() => { + if (isInstagramAndroid) { + metricService.reachGoal(EGoals.CAMERA_ANDROID_INSTAGRAM, [EMetrics.YANDEX, EMetrics.KLAVIYO]); + } + }, [isInstagramAndroid]) + + // LOGIC TODO: Make hook + + const onTakePhoto = async (photo: string) => { + try { + const file = DataURIToBlob(photo); + + const result = await getLines(file); + + URL.revokeObjectURL(URL.createObjectURL(file)); + + dispatch( + actions.compatibilityV2.update({ + photo, + }) + ); + if (!checkPalmistryLines(result?.lines || [])) return; + handleToScannedPhoto(); + } catch (error) { + console.error('Ошибка при обработке фото:', error); + } finally { + // Принудительный запуск сборщика мусора (не гарантировано, но может помочь) + if (window.gc) { + window.gc(); + } + } + }; + + const fingersNames = { + thumb: translate("thumb"), + index_finger: translate("index_finger"), + middle_finger: translate("middle_finger"), + ring_finger: translate("ring_finger"), + pinky: translate("pinky"), + }; + + const setFingersNames = ( + fingers: IPalmistryFinger[] + ): ICompatibilityV2FingerLocal[] => { + if (!fingers) return []; + return fingers.map((finger) => { + return { + ...finger, + fingerName: fingersNames[finger.name as keyof typeof fingersNames], + }; + }); + }; + + const getLines = async (file: File | Blob) => { + setIsLoading(true); + const formData = new FormData(); + formData.append("file", file); + try { + const result = await api.getPalmistryLines({ formData }); + const fingers = setFingersNames(result?.fingers); + + dispatch( + actions.compatibilityV2.update({ + lines: result?.lines, + fingers, + }) + ); + return result; + } catch (error) { + dispatch( + actions.compatibilityV2.update({ + lines: [], + fingers: [], + }) + ); + return null; + } finally { + setIsLoading(false); + } + }; + + const checkPalmistryLines = (lines: IPalmistryLine[]): boolean => { + if (!lines.length || lines.length < 2) { + metricService.reachGoal(EGoals.CAMERA_ERROR, [EMetrics.YANDEX, EMetrics.KLAVIYO]); + setToastVisible(EToastVisible.try_again); + return false; + } + if (lines.length === 2) { + setToastVisible(EToastVisible.try_again_or_next); + return false; + } + return true; + }; + + const onSelectFile = async (event: React.ChangeEvent) => { + setToastVisible(null); + if (!event.target.files || event.target.files.length === 0) return; + + const result = await getLines(event.target.files[0]); + + const reader = new FileReader(); + + reader.onloadend = () => { + dispatch( + actions.compatibilityV2.update({ + photo: reader.result as string, + }) + ); + if (!checkPalmistryLines(result?.lines || [])) return; + handleToScannedPhoto(); + }; + + reader.readAsDataURL(event.target.files[0]); + }; + + if (!isReady) return ; + + return ( + <> + {/* Модальное окно запроса камеры */} + { }} + className={styles.modal} + containerClassName={styles["modal-container"]} + > + + {translate("/camera.modal.title")} + +
+
+

+ {translate("/camera.modal.cancel")} +

+
+
+

+ {translate("/camera.modal.allow")} +

+
+
+
+ + {/* Модальное окно загрузки фото */} + {/* {!isProduction && uploadMenuModalIsOpen && ( + setUploadMenuModalIsOpen(false)} + onSelectFile={onSelectFile} + onChooseCamera={() => true} + /> + )} */} + {/* Кнопка загрузки фото */} + {/* {!isProduction && ( + + )} */} + + {/* Модальное окно камеры */} + {!isLoading && console.log("close")} + onTakePhoto={handleCameraSuccess} + onError={handleCameraError} + isCameraVisible={isCameraModalOpen} + // reinitializeKey={reinitializeCameraCount} + />} + + + {/* Лоадер */} + {isLoading && ( + + )} + + {/* Тост если фото плохое */} + {toastVisible === EToastVisible.try_again && ( + +
+ {translate("/camera.bad_photo")} +
+ + +
+
+
+ )} + + {/* Тост если нет доступа к камере */} + {toastVisible === EToastVisible.no_access_camera && ( + +
+ {translate("/camera.no_access_camera")} +
+ + +
+
+
+ )} + + {/* Тост загрузки фото */} + {toastVisible === EToastVisible.upload_photo && ( + +
+ {translate("/camera.no_access_camera")} +
+ +
+
+
+ )} + + {/* Тост если фото можно улучшить */} + {toastVisible === EToastVisible.try_again_or_next && ( + +
+ {translate("/camera.do_better")} +
+ + +
+
+
+ )} + + ) +} + +export default AndroidCamera; \ No newline at end of file diff --git a/src/components/CompatibilityV2/pages/Camera/index.tsx b/src/components/CompatibilityV2/pages/Camera/index.tsx index 533496b..fc412b4 100644 --- a/src/components/CompatibilityV2/pages/Camera/index.tsx +++ b/src/components/CompatibilityV2/pages/Camera/index.tsx @@ -1,32 +1,7 @@ -import styles from "./styles.module.scss"; -import { DataURIToBlob } from "@/services/data"; -import { useApi } from "@/api"; -import { IPalmistryFinger, IPalmistryLine } from "@/api/resources/Palmistry"; -import { ICompatibilityV2FingerLocal } from "@/store/compatibilityV2"; -import { useDispatch } from "react-redux"; -import { actions } from "@/store"; -import { useNavigate } from "react-router-dom"; -import routes from "@/routes"; -import { useEffect, useMemo, useState } from "react"; -import Loader, { LoaderColor } from "@/components/Loader"; -import UploadModal from "@/components/palmistry/upload-modal/upload-modal"; -import Toast from "@/components/pages/ABDesign/v1/components/Toast"; -import { useTranslations } from "@/hooks/translations"; -import { ELocalesPlacement } from "@/locales"; -import CameraModal from "../../components/CameraModal"; +import { useEffect, useMemo } from "react"; import metricService, { EGoals, EMetrics } from "@/services/metric/metricService"; -import Modal from "@/components/Modal"; -import Title from "@/components/Title"; -import { EUnleashFlags, useUnleash } from "@/hooks/ab/unleash/useUnleash"; - -const isProduction = import.meta.env.MODE === "production"; - -enum EToastVisible { - "try_again" = "try_again", - "try_again_or_next" = "try_again_or_next", - "no_access_camera" = "no_access_camera", - "reload_page" = "reload_page", -} +import AndroidCamera from "./android"; +import IphoneCamera from "./iphone"; function Camera() { const isIphoneSafari = useMemo((): boolean => { @@ -41,363 +16,19 @@ function Camera() { return isIOS && isSafari; }, []); - const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2); - const navigate = useNavigate(); - const api = useApi(); - const dispatch = useDispatch(); - const [isLoading, setIsLoading] = useState(false); - const [uploadMenuModalIsOpen, setUploadMenuModalIsOpen] = useState(false); - const [toastVisible, setToastVisible] = useState(null); - const [cameraKey, setCameraKey] = useState(0); - const [isCameraModalOpen, setIsCameraModalOpen] = useState(false); - - const { isReady, variant: cameraRequestModalCompatibilityV2 } = useUnleash({ - flag: EUnleashFlags.cameraRequestModalCompatibilityV2 - }); - - const { variant: compatibilityV2ScanHand } = useUnleash({ - flag: EUnleashFlags.compatibilityV2ScanHand - }); - const isScanHand = compatibilityV2ScanHand === "show"; - - const isCameraRequestModal = cameraRequestModalCompatibilityV2 !== "hide"; - - const [isRequestCameraModalOpen, setIsRequestCameraModalOpen] = useState((isIphoneSafari || !isCameraRequestModal) ? false : true); - - const handleNext = () => { - metricService.reachGoal(EGoals.CAMERA_SUCCESS, [EMetrics.YANDEX, EMetrics.KLAVIYO]); - navigate(routes.client.compatibilityV2ScannedPhoto()); - }; - - const fingersNames = { - thumb: translate("thumb"), - index_finger: translate("index_finger"), - middle_finger: translate("middle_finger"), - ring_finger: translate("ring_finger"), - pinky: translate("pinky"), - }; - - const setFingersNames = ( - fingers: IPalmistryFinger[] - ): ICompatibilityV2FingerLocal[] => { - if (!fingers) return []; - return fingers.map((finger) => { - return { - ...finger, - fingerName: fingersNames[finger.name as keyof typeof fingersNames], - }; - }); - }; - - /** - * Check if the palmistry lines are valid for the next step. - * If the length of the lines is less than 2, show the "try_again" toast. - * If the length of the lines is 2, show the "try_again_or_next" toast. - * Otherwise, return true. - * @param {IPalmistryLine[]} lines - The palmistry lines. - * @returns {boolean} Whether the palmistry lines are valid for the next step. - */ - const checkPalmistryLines = (lines: IPalmistryLine[]): boolean => { - if (!lines.length || lines.length < 2) { - setToastVisible(EToastVisible.try_again); - return false; - } - if (lines.length === 2) { - setToastVisible(EToastVisible.try_again_or_next); - return false; - } - return true; - }; - - const getLines = async (file: File | Blob) => { - setIsLoading(true); - const formData = new FormData(); - formData.append("file", file); - try { - const result = await api.getPalmistryLines({ formData }); - const fingers = setFingersNames(result?.fingers); - - dispatch( - actions.compatibilityV2.update({ - lines: result?.lines, - fingers, - }) - ); - return result; - } catch (error) { - dispatch( - actions.compatibilityV2.update({ - lines: [], - fingers: [], - }) - ); - return null; - } finally { - setIsLoading(false); - } - }; - - const onTakePhoto = async (photo: string) => { - setUploadMenuModalIsOpen(false); - - try { - const file = DataURIToBlob(photo); - - const result = await getLines(file); - - URL.revokeObjectURL(URL.createObjectURL(file)); - - dispatch( - actions.compatibilityV2.update({ - photo, - }) - ); - if (!checkPalmistryLines(result?.lines || [])) return; - handleNext(); - } catch (error) { - console.error('Ошибка при обработке фото:', error); - } finally { - // Принудительный запуск сборщика мусора (не гарантировано, но может помочь) - if (window.gc) { - window.gc(); - } - } - }; - - const onSelectFile = async (event: React.ChangeEvent) => { - setToastVisible(null); - setUploadMenuModalIsOpen(false); - - if (!event.target.files || event.target.files.length === 0) return; - - const result = await getLines(event.target.files[0]); - - const reader = new FileReader(); - - reader.onloadend = () => { - dispatch( - actions.compatibilityV2.update({ - photo: reader.result as string, - }) - ); - if (!checkPalmistryLines(result?.lines || [])) return; - handleNext(); - }; - - reader.readAsDataURL(event.target.files[0]); - }; - useEffect(() => { metricService.reachGoal(EGoals.CAMERA_OPEN, [EMetrics.YANDEX, EMetrics.KLAVIYO]); }, []); - useEffect(() => { - if (toastVisible === EToastVisible.try_again) { - metricService.reachGoal(EGoals.CAMERA_ERROR, [EMetrics.YANDEX, EMetrics.KLAVIYO]); - } - }, [toastVisible]); - - const cameraError = (error: string | DOMException) => { - console.error("Camera error", error) - if (isScanHand) { - if (!isIphoneSafari) { - return navigate(routes.client.compatibilityV2ScanHand()) - } - if (isIphoneSafari && cameraKey > 1) { - return navigate(routes.client.compatibilityV2ScanHand()) - } - } - if (!isIphoneSafari || !isCameraRequestModal) return; - if (error === "Video is not ready") { - return setToastVisible(EToastVisible.no_access_camera) - } - return setIsRequestCameraModalOpen(true) - } - - const requestCameraPermission = async () => { - if (cameraKey > 2) { - setIsRequestCameraModalOpen(false); - setToastVisible(EToastVisible.reload_page); - return; - } - try { - const stream = await navigator.mediaDevices.getUserMedia({ video: true }); - setToastVisible(null); - stream.getTracks().forEach(track => track.stop()); - setCameraKey(prev => prev + 1); - } catch (error) { - setCameraKey(prev => prev + 1); - console.error("Ошибка при запросе доступа к камере:", error); - setIsRequestCameraModalOpen(true); - } - }; - - if (!isReady) return ; + // useEffect(() => { + // if (toastVisible === EToastVisible.try_again) { + // metricService.reachGoal(EGoals.CAMERA_ERROR, [EMetrics.YANDEX, EMetrics.KLAVIYO]); + // } + // }, [toastVisible]); return ( <> - { }} - className={styles.modal} - containerClassName={styles["modal-container"]} - > - - {translate("/camera.modal.title")} - -
-
{ - setIsRequestCameraModalOpen(false); - setToastVisible(EToastVisible.no_access_camera); - }}> -

- {translate("/camera.modal.cancel")} -

-
-
{ - setIsRequestCameraModalOpen(false) - if (isIphoneSafari) { - requestCameraPermission() - } else { - setIsCameraModalOpen(true) - if (window.navigator.userAgent.includes("Instagram")) { - setToastVisible(EToastVisible.no_access_camera) - } - } - }}> -

- {translate("/camera.modal.allow")} -

-
-
-
- {!isProduction && uploadMenuModalIsOpen && ( - setUploadMenuModalIsOpen(false)} - onSelectFile={onSelectFile} - onChooseCamera={() => true} - /> - )} - {!isProduction && ( - - )} - {!isLoading && !uploadMenuModalIsOpen && ( - // console.log("close")} - // onTakePhoto={onTakePhoto} - // /> - console.log("close")} - onTakePhoto={onTakePhoto} - onError={cameraError} - isCameraVisible={(isIphoneSafari || isCameraRequestModal) ? true : isCameraModalOpen} - reinitializeKey={cameraKey} - /> - )} - {isLoading && ( - - )} - {toastVisible === EToastVisible.try_again && ( - -
- {translate("/camera.bad_photo")} -
- - -
-
-
- )} - {toastVisible === EToastVisible.no_access_camera && ( - -
- {translate("/camera.no_access_camera")} -
- - -
-
-
- )} - {toastVisible === EToastVisible.reload_page && ( - -
- {translate("/camera.reload_page")} - -
-
- )} - {toastVisible === EToastVisible.try_again_or_next && ( - -
- {translate("/camera.do_better")} -
- - -
-
-
- )} + {isIphoneSafari ? : } ); } diff --git a/src/components/CompatibilityV2/pages/Camera/iphone/index.tsx b/src/components/CompatibilityV2/pages/Camera/iphone/index.tsx new file mode 100644 index 0000000..ce4a9c4 --- /dev/null +++ b/src/components/CompatibilityV2/pages/Camera/iphone/index.tsx @@ -0,0 +1,348 @@ + +import Modal from "@/components/Modal"; +import styles from "../styles.module.scss"; +import Title from "@/components/Title"; +import { useTranslations } from "@/hooks/translations"; +import { ELocalesPlacement } from "@/locales"; +import Loader, { LoaderColor } from "@/components/Loader"; +import { useEffect, useState } from "react"; +import CameraModal from "@/components/CompatibilityV2/components/CameraModal"; +import { useNavigate } from "react-router-dom"; +import routes from "@/routes"; +import Toast from "@/components/pages/ABDesign/v1/components/Toast"; +import { DataURIToBlob } from "@/services/data"; +import { actions } from "@/store"; +import { IPalmistryFinger, IPalmistryLine } from "@/api/resources/Palmistry"; +import { ICompatibilityV2FingerLocal } from "@/store/compatibilityV2"; +import { useApi } from "@/api"; +import { useDispatch } from "react-redux"; +import metricService, { EGoals, EMetrics } from "@/services/metric/metricService"; +import { EUnleashFlags, useUnleash } from "@/hooks/ab/unleash/useUnleash"; + +enum EToastVisible { + "try_again" = "try_again", + "try_again_or_next" = "try_again_or_next", + "no_access_camera" = "no_access_camera", + "reload_page" = "reload_page", +} + +function IphoneCamera() { + const api = useApi(); + const dispatch = useDispatch(); + const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2); + const navigate = useNavigate(); + + const { isReady, variant: cameraRequestModalCompatibilityV2 } = useUnleash({ + flag: EUnleashFlags.cameraRequestModalCompatibilityV2 + }); + + const isShowCameraRequestModal = cameraRequestModalCompatibilityV2 !== "hide"; + + const { variant: compatibilityV2ScanHand } = useUnleash({ + flag: EUnleashFlags.compatibilityV2ScanHand + }); + + const isShowScanHand = compatibilityV2ScanHand !== "hide"; + + const [isLoading, setIsLoading] = useState(false); + const [isRequestCameraModalOpen, setIsRequestCameraModalOpen] = useState(isShowCameraRequestModal); + const [isCameraModalOpen, setIsCameraModalOpen] = useState(false); + const [reinitializeCameraCount, setReinitializeCameraCount] = useState(0); + const [toastVisible, setToastVisible] = useState(null); + + const handleToScanHand = () => { + metricService.reachGoal(EGoals.SCAN_ARTIFICIAL_PHOTO, [EMetrics.YANDEX, EMetrics.KLAVIYO]); + return navigate(routes.client.compatibilityV2ScanHand()) + } + + const handleToScannedPhoto = () => { + metricService.reachGoal(EGoals.CAMERA_SUCCESS, [EMetrics.YANDEX, EMetrics.KLAVIYO]); + return navigate(routes.client.compatibilityV2ScannedPhoto()) + } + + const handleCameraError = (error: string | DOMException) => { + console.log("camera error: ", error) + if (reinitializeCameraCount < 2) { + setToastVisible(EToastVisible.no_access_camera) + } else { + if (!isShowScanHand) { + return setToastVisible(EToastVisible.reload_page) + } + return handleToScanHand() + } + } + + const handleCameraSuccess = (photo: string) => { + onTakePhoto(photo) + } + + + const handleRequestCameraModalCancel = () => { + setIsRequestCameraModalOpen(false) + setToastVisible(EToastVisible.no_access_camera) + } + + const handleRequestCameraModalAllow = () => { + setIsRequestCameraModalOpen(false) + handleRequestCamera() + } + + const handleRequestCamera = () => { + setToastVisible(null) + if (!isCameraModalOpen) { + return setIsCameraModalOpen(true) + } + setReinitializeCameraCount(prev => prev + 1) + } + + useEffect(() => { + if (!isShowCameraRequestModal) { + setIsCameraModalOpen(true) + } + }, [isShowCameraRequestModal]) + + // LOGIC TODO: Make hook + + const onTakePhoto = async (photo: string) => { + try { + const file = DataURIToBlob(photo); + + const result = await getLines(file); + + URL.revokeObjectURL(URL.createObjectURL(file)); + + dispatch( + actions.compatibilityV2.update({ + photo, + }) + ); + if (!checkPalmistryLines(result?.lines || [])) return; + handleToScannedPhoto(); + } catch (error) { + console.error('Ошибка при обработке фото:', error); + } finally { + // Принудительный запуск сборщика мусора (не гарантировано, но может помочь) + if (window.gc) { + window.gc(); + } + } + }; + + const fingersNames = { + thumb: translate("thumb"), + index_finger: translate("index_finger"), + middle_finger: translate("middle_finger"), + ring_finger: translate("ring_finger"), + pinky: translate("pinky"), + }; + + const setFingersNames = ( + fingers: IPalmistryFinger[] + ): ICompatibilityV2FingerLocal[] => { + if (!fingers) return []; + return fingers.map((finger) => { + return { + ...finger, + fingerName: fingersNames[finger.name as keyof typeof fingersNames], + }; + }); + }; + + const getLines = async (file: File | Blob) => { + setIsLoading(true); + const formData = new FormData(); + formData.append("file", file); + try { + const result = await api.getPalmistryLines({ formData }); + const fingers = setFingersNames(result?.fingers); + + dispatch( + actions.compatibilityV2.update({ + lines: result?.lines, + fingers, + }) + ); + return result; + } catch (error) { + dispatch( + actions.compatibilityV2.update({ + lines: [], + fingers: [], + }) + ); + return null; + } finally { + setIsLoading(false); + } + }; + + const checkPalmistryLines = (lines: IPalmistryLine[]): boolean => { + if (!lines.length || lines.length < 2) { + metricService.reachGoal(EGoals.CAMERA_ERROR, [EMetrics.YANDEX, EMetrics.KLAVIYO]); + setToastVisible(EToastVisible.try_again); + return false; + } + if (lines.length === 2) { + setToastVisible(EToastVisible.try_again_or_next); + return false; + } + return true; + }; + + const onSelectFile = async (event: React.ChangeEvent) => { + setToastVisible(null); + if (!event.target.files || event.target.files.length === 0) return; + + const result = await getLines(event.target.files[0]); + + const reader = new FileReader(); + + reader.onloadend = () => { + dispatch( + actions.compatibilityV2.update({ + photo: reader.result as string, + }) + ); + if (!checkPalmistryLines(result?.lines || [])) return; + handleToScannedPhoto(); + }; + + reader.readAsDataURL(event.target.files[0]); + }; + + if (!isReady) return ; + + return ( + <> + {/* Модальное окно запроса камеры */} + { }} + className={styles.modal} + containerClassName={styles["modal-container"]} + > + + {translate("/camera.modal.title")} + +
+
+

+ {translate("/camera.modal.cancel")} +

+
+
+

+ {translate("/camera.modal.allow")} +

+
+
+
+ + {/* Модальное окно камеры */} + {!isLoading && ( + console.log("close")} + onTakePhoto={handleCameraSuccess} + onError={handleCameraError} + isCameraVisible={isCameraModalOpen} + reinitializeKey={reinitializeCameraCount} + /> + )} + + {/* Лоадер */} + {isLoading && ( + + )} + + {/* Тост если фото плохое */} + {toastVisible === EToastVisible.try_again && ( + +
+ {translate("/camera.bad_photo")} +
+ + +
+
+
+ )} + + {/* Тост если нет доступа к камере */} + {toastVisible === EToastVisible.no_access_camera && ( + +
+ {translate("/camera.no_access_camera")} +
+ + +
+
+
+ )} + + {/* Тост если фото можно улучшить */} + {toastVisible === EToastVisible.try_again_or_next && ( + +
+ {translate("/camera.do_better")} +
+ + +
+
+
+ )} + + {/* Тост если нужно перезагрузить страницу */} + {toastVisible === EToastVisible.reload_page && ( + +
+ {translate("/camera.reload_page")} + +
+
+ )} + + ) +} + +export default IphoneCamera; \ No newline at end of file diff --git a/src/components/CompatibilityV2/pages/ScanHand/index.tsx b/src/components/CompatibilityV2/pages/ScanHand/index.tsx index 15532aa..5588305 100644 --- a/src/components/CompatibilityV2/pages/ScanHand/index.tsx +++ b/src/components/CompatibilityV2/pages/ScanHand/index.tsx @@ -14,6 +14,7 @@ import { IPalmistryFinger } from "@/api/resources/Palmistry"; import { ICompatibilityV2FingerLocal } from "@/store/compatibilityV2"; import { useApi } from "@/api"; import { DataURIToBlob } from "@/services/data"; +import { EUnleashFlags, useUnleash } from "@/hooks/ab/unleash/useUnleash"; function ScanHand() { const api = useApi(); @@ -24,17 +25,25 @@ function ScanHand() { const imageRef = useRef(null); const gender = (useSelector(selectors.selectQuestionnaire)?.gender || "female"); + const { isReady, variant: scanHandTimeCompatibilityV2 } = useUnleash({ + flag: EUnleashFlags.scanHandTimeCompatibilityV2 + }); + + const TIME_SCAN_HAND = scanHandTimeCompatibilityV2 === "v1" ? 8000 : 6000; + const handleNext = () => { navigate(`${routes.client.compatibilityV2ScannedPhoto()}?fromScanHand=true`) } useEffect(() => { + if (!isReady) return; + const timer = setTimeout(() => { handleHandOn() - }, 6000); + }, TIME_SCAN_HAND); return () => clearTimeout(timer); - }, []); + }, [isReady]); const convertImageToBase64 = (img: HTMLImageElement): Promise => { return new Promise((resolve, reject) => { @@ -129,6 +138,8 @@ function ScanHand() { } } + if (!isReady) return ; + return (
{ + metricService.reachGoal(EGoals.CAMERA_HAND, [EMetrics.YANDEX, EMetrics.KLAVIYO]); + }, []) + useEffect(() => { if (isIOSPath) { (async () => { diff --git a/src/hooks/ab/unleash/useUnleash.ts b/src/hooks/ab/unleash/useUnleash.ts index e8b374c..e976c9c 100644 --- a/src/hooks/ab/unleash/useUnleash.ts +++ b/src/hooks/ab/unleash/useUnleash.ts @@ -25,6 +25,7 @@ export enum EUnleashFlags { "compatibilityV1EmailEnter" = "compatibilityV1EmailEnter", "compatibilityV2ScanHand" = "compatibilityV2ScanHand", "preloadImages" = "preloadImages", + "scanHandTimeCompatibilityV2" = "scanHandTimeCompatibilityV2" } interface IUseUnleashProps { @@ -54,6 +55,7 @@ interface IVariants { [EUnleashFlags.compatibilityV1EmailEnter]: "show" | "hide"; [EUnleashFlags.compatibilityV2ScanHand]: "show" | "hide"; [EUnleashFlags.preloadImages]: "yes" | "no"; + [EUnleashFlags.scanHandTimeCompatibilityV2]: "v0" | "v1"; } /** diff --git a/src/services/metric/metricService.ts b/src/services/metric/metricService.ts index d8a6704..023e630 100644 --- a/src/services/metric/metricService.ts +++ b/src/services/metric/metricService.ts @@ -56,6 +56,10 @@ export enum EGoals { DOWNLOAD_APP = "DownloadApp", + CAMERA_HAND = "CameraHand", + SCAN_ARTIFICIAL_PHOTO = "ScanArtificialPhoto", + CAMERA_ANDROID_INSTAGRAM = "CameraAndroidInstagram", + // FB LEAD = "Lead", PURCHASE = "Purchase", diff --git a/src/services/permission/permisson.ts b/src/services/permission/permisson.ts new file mode 100644 index 0000000..d665f34 --- /dev/null +++ b/src/services/permission/permisson.ts @@ -0,0 +1,22 @@ +export async function checkCameraPermissionState(): Promise { + if (!navigator.permissions || !navigator.permissions.query) { + // Permissions API не поддерживается + console.warn("Permissions API не поддерживается в этом браузере."); + return null; + } + + try { + const permissionStatus = await navigator.permissions.query({ name: 'camera' as PermissionName }); + console.log(`Статус разрешения камеры: ${permissionStatus.state}`); + + // Можно добавить обработку изменения статуса + // permissionStatus.onchange = () => { + // console.log(`Статус разрешения камеры изменился: ${permissionStatus.state}`); + // }; + + return permissionStatus.state; // 'granted', 'denied', 'prompt' + } catch (error) { + console.error("Ошибка при проверке разрешения камеры:", error); + return null; + } +} \ No newline at end of file