AW-429-comp-v1-ios-ab
This commit is contained in:
parent
67e3356d53
commit
de389e2da5
391
src/components/CompatibilityV2/pages/Camera/android/index.tsx
Normal file
391
src/components/CompatibilityV2/pages/Camera/android/index.tsx
Normal file
@ -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<EToastVisible | null>(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<HTMLInputElement>) => {
|
||||
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 <Loader color={LoaderColor.Black} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Модальное окно запроса камеры */}
|
||||
<Modal
|
||||
isCloseButtonVisible={false}
|
||||
open={isRequestCameraModalOpen}
|
||||
onClose={() => { }}
|
||||
className={styles.modal}
|
||||
containerClassName={styles["modal-container"]}
|
||||
>
|
||||
<Title variant="h4" className={styles["modal-title"]}>
|
||||
{translate("/camera.modal.title")}
|
||||
</Title>
|
||||
<div className={styles["modal-answers"]}>
|
||||
<div className={styles["modal-answer"]} onClick={handleRequestCameraModalCancel}>
|
||||
<p className={styles["modal-answer-text"]}>
|
||||
{translate("/camera.modal.cancel")}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles["modal-answer"]} onClick={handleRequestCameraModalAllow}>
|
||||
<p className={styles["modal-answer-text"]}>
|
||||
{translate("/camera.modal.allow")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Модальное окно загрузки фото */}
|
||||
{/* {!isProduction && uploadMenuModalIsOpen && (
|
||||
<UploadModal
|
||||
onClose={() => setUploadMenuModalIsOpen(false)}
|
||||
onSelectFile={onSelectFile}
|
||||
onChooseCamera={() => true}
|
||||
/>
|
||||
)} */}
|
||||
{/* Кнопка загрузки фото */}
|
||||
{/* {!isProduction && (
|
||||
<button
|
||||
className={styles["upload-button"]}
|
||||
onClick={() => setUploadMenuModalIsOpen(true)}
|
||||
>
|
||||
Upload
|
||||
</button>
|
||||
)} */}
|
||||
|
||||
{/* Модальное окно камеры */}
|
||||
{!isLoading && <CameraModal
|
||||
onClose={() => console.log("close")}
|
||||
onTakePhoto={handleCameraSuccess}
|
||||
onError={handleCameraError}
|
||||
isCameraVisible={isCameraModalOpen}
|
||||
// reinitializeKey={reinitializeCameraCount}
|
||||
/>}
|
||||
|
||||
|
||||
{/* Лоадер */}
|
||||
{isLoading && (
|
||||
<Loader className={styles.loader} color={LoaderColor.Black} />
|
||||
)}
|
||||
|
||||
{/* Тост если фото плохое */}
|
||||
{toastVisible === EToastVisible.try_again && (
|
||||
<Toast classNameContainer={styles["toast-container"]} variant="error">
|
||||
<div className={styles["toast-content"]}>
|
||||
<span>{translate("/camera.bad_photo")}</span>
|
||||
<div className={styles["toast-buttons-container"]}>
|
||||
<button onClick={() => setToastVisible(null)}>
|
||||
{translate("/camera.try_again")}
|
||||
</button>
|
||||
<button className={styles.buttonUpload}>
|
||||
<input
|
||||
id="upload-input"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={onSelectFile}
|
||||
/>
|
||||
<label htmlFor="upload-input" className={styles.labelUpload}>
|
||||
{translate("/camera.upload")}
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Toast>
|
||||
)}
|
||||
|
||||
{/* Тост если нет доступа к камере */}
|
||||
{toastVisible === EToastVisible.no_access_camera && (
|
||||
<Toast classNameContainer={styles["toast-container"]} variant="error">
|
||||
<div className={styles["toast-content"]}>
|
||||
<span>{translate("/camera.no_access_camera")}</span>
|
||||
<div className={styles["toast-buttons-container"]}>
|
||||
<button onClick={() => {
|
||||
if (isInstagramAndroid) {
|
||||
return handleToScanHand()
|
||||
}
|
||||
setToastVisible(null)
|
||||
setIsCameraModalOpen(true)
|
||||
}}>
|
||||
{translate("/camera.give_access")}
|
||||
</button>
|
||||
<button className={styles.buttonUpload}>
|
||||
<input
|
||||
id="upload-input"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={onSelectFile}
|
||||
/>
|
||||
<label htmlFor="upload-input" className={styles.labelUpload}>
|
||||
{translate("/camera.upload")}
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Toast>
|
||||
)}
|
||||
|
||||
{/* Тост загрузки фото */}
|
||||
{toastVisible === EToastVisible.upload_photo && (
|
||||
<Toast classNameContainer={styles["toast-container"]} variant="error">
|
||||
<div className={styles["toast-content"]}>
|
||||
<span>{translate("/camera.no_access_camera")}</span>
|
||||
<div className={styles["toast-buttons-container"]}>
|
||||
<button className={styles.buttonUpload}>
|
||||
<input
|
||||
id="upload-input"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={onSelectFile}
|
||||
/>
|
||||
<label htmlFor="upload-input" className={styles.labelUpload}>
|
||||
{translate("/camera.upload")}
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Toast>
|
||||
)}
|
||||
|
||||
{/* Тост если фото можно улучшить */}
|
||||
{toastVisible === EToastVisible.try_again_or_next && (
|
||||
<Toast classNameContainer={styles["toast-container"]} variant="error">
|
||||
<div
|
||||
className={styles["toast-content"]}
|
||||
style={{ flexDirection: "column" }}
|
||||
>
|
||||
<span>{translate("/camera.do_better")}</span>
|
||||
<div className={styles["buttons-container"]}>
|
||||
<button onClick={() => setToastVisible(null)}>
|
||||
{translate("/camera.try_again")}
|
||||
</button>
|
||||
<button onClick={handleToScannedPhoto}>
|
||||
{translate("/camera.next")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Toast>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AndroidCamera;
|
||||
@ -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<EToastVisible | null>(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<HTMLInputElement>) => {
|
||||
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 <Loader color={LoaderColor.Black} />;
|
||||
// useEffect(() => {
|
||||
// if (toastVisible === EToastVisible.try_again) {
|
||||
// metricService.reachGoal(EGoals.CAMERA_ERROR, [EMetrics.YANDEX, EMetrics.KLAVIYO]);
|
||||
// }
|
||||
// }, [toastVisible]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isCloseButtonVisible={false}
|
||||
open={isRequestCameraModalOpen}
|
||||
onClose={() => { }}
|
||||
className={styles.modal}
|
||||
containerClassName={styles["modal-container"]}
|
||||
>
|
||||
<Title variant="h4" className={styles["modal-title"]}>
|
||||
{translate("/camera.modal.title")}
|
||||
</Title>
|
||||
<div className={styles["modal-answers"]}>
|
||||
<div className={styles["modal-answer"]} onClick={() => {
|
||||
setIsRequestCameraModalOpen(false);
|
||||
setToastVisible(EToastVisible.no_access_camera);
|
||||
}}>
|
||||
<p className={styles["modal-answer-text"]}>
|
||||
{translate("/camera.modal.cancel")}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles["modal-answer"]} onClick={() => {
|
||||
setIsRequestCameraModalOpen(false)
|
||||
if (isIphoneSafari) {
|
||||
requestCameraPermission()
|
||||
} else {
|
||||
setIsCameraModalOpen(true)
|
||||
if (window.navigator.userAgent.includes("Instagram")) {
|
||||
setToastVisible(EToastVisible.no_access_camera)
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<p className={styles["modal-answer-text"]}>
|
||||
{translate("/camera.modal.allow")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
{!isProduction && uploadMenuModalIsOpen && (
|
||||
<UploadModal
|
||||
onClose={() => setUploadMenuModalIsOpen(false)}
|
||||
onSelectFile={onSelectFile}
|
||||
onChooseCamera={() => true}
|
||||
/>
|
||||
)}
|
||||
{!isProduction && (
|
||||
<button
|
||||
className={styles["upload-button"]}
|
||||
onClick={() => setUploadMenuModalIsOpen(true)}
|
||||
>
|
||||
Upload
|
||||
</button>
|
||||
)}
|
||||
{!isLoading && !uploadMenuModalIsOpen && (
|
||||
// <PalmCameraModal
|
||||
// onClose={() => console.log("close")}
|
||||
// onTakePhoto={onTakePhoto}
|
||||
// />
|
||||
<CameraModal
|
||||
onClose={() => console.log("close")}
|
||||
onTakePhoto={onTakePhoto}
|
||||
onError={cameraError}
|
||||
isCameraVisible={(isIphoneSafari || isCameraRequestModal) ? true : isCameraModalOpen}
|
||||
reinitializeKey={cameraKey}
|
||||
/>
|
||||
)}
|
||||
{isLoading && (
|
||||
<Loader className={styles.loader} color={LoaderColor.Black} />
|
||||
)}
|
||||
{toastVisible === EToastVisible.try_again && (
|
||||
<Toast classNameContainer={styles["toast-container"]} variant="error">
|
||||
<div className={styles["toast-content"]}>
|
||||
<span>{translate("/camera.bad_photo")}</span>
|
||||
<div className={styles["toast-buttons-container"]}>
|
||||
<button onClick={() => setToastVisible(null)}>
|
||||
{translate("/camera.try_again")}
|
||||
</button>
|
||||
<button className={styles.buttonUpload}>
|
||||
<input
|
||||
id="upload-input"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={onSelectFile}
|
||||
/>
|
||||
<label htmlFor="upload-input" className={styles.labelUpload} onClick={() => {
|
||||
if (!isIphoneSafari) {
|
||||
return navigate(routes.client.compatibilityV2ScanHand());
|
||||
}
|
||||
}}>
|
||||
{translate("/camera.upload")}
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Toast>
|
||||
)}
|
||||
{toastVisible === EToastVisible.no_access_camera && (
|
||||
<Toast classNameContainer={styles["toast-container"]} variant="error">
|
||||
<div className={styles["toast-content"]}>
|
||||
<span>{translate("/camera.no_access_camera")}</span>
|
||||
<div className={styles["toast-buttons-container"]}>
|
||||
<button onClick={() => {
|
||||
setToastVisible(null)
|
||||
if (isIphoneSafari) {
|
||||
requestCameraPermission()
|
||||
} else {
|
||||
setIsCameraModalOpen(true)
|
||||
navigate(routes.client.compatibilityV2ScanHand())
|
||||
}
|
||||
}}>
|
||||
{translate("/camera.give_access")}
|
||||
</button>
|
||||
<button className={styles.buttonUpload}>
|
||||
<input
|
||||
id="upload-input"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={onSelectFile}
|
||||
/>
|
||||
<label htmlFor="upload-input" className={styles.labelUpload} onClick={() => {
|
||||
if (!isIphoneSafari) {
|
||||
return navigate(routes.client.compatibilityV2ScanHand());
|
||||
}
|
||||
}}>
|
||||
{translate("/camera.upload")}
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Toast>
|
||||
)}
|
||||
{toastVisible === EToastVisible.reload_page && (
|
||||
<Toast classNameContainer={styles["toast-container"]} variant="error">
|
||||
<div className={styles["toast-content"]}>
|
||||
<span>{translate("/camera.reload_page")}</span>
|
||||
<button onClick={() => {
|
||||
setToastVisible(null)
|
||||
window.location.reload()
|
||||
}}>
|
||||
{translate("/camera.reload_page_button")}
|
||||
</button>
|
||||
</div>
|
||||
</Toast>
|
||||
)}
|
||||
{toastVisible === EToastVisible.try_again_or_next && (
|
||||
<Toast classNameContainer={styles["toast-container"]} variant="error">
|
||||
<div
|
||||
className={styles["toast-content"]}
|
||||
style={{ flexDirection: "column" }}
|
||||
>
|
||||
<span>{translate("/camera.do_better")}</span>
|
||||
<div className={styles["buttons-container"]}>
|
||||
<button onClick={() => setToastVisible(null)}>
|
||||
{translate("/camera.try_again")}
|
||||
</button>
|
||||
<button onClick={() => handleNext()}>
|
||||
{translate("/camera.next")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Toast>
|
||||
)}
|
||||
{isIphoneSafari ? <IphoneCamera /> : <AndroidCamera />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
348
src/components/CompatibilityV2/pages/Camera/iphone/index.tsx
Normal file
348
src/components/CompatibilityV2/pages/Camera/iphone/index.tsx
Normal file
@ -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<EToastVisible | null>(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<HTMLInputElement>) => {
|
||||
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 <Loader color={LoaderColor.Black} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Модальное окно запроса камеры */}
|
||||
<Modal
|
||||
isCloseButtonVisible={false}
|
||||
open={isRequestCameraModalOpen}
|
||||
onClose={() => { }}
|
||||
className={styles.modal}
|
||||
containerClassName={styles["modal-container"]}
|
||||
>
|
||||
<Title variant="h4" className={styles["modal-title"]}>
|
||||
{translate("/camera.modal.title")}
|
||||
</Title>
|
||||
<div className={styles["modal-answers"]}>
|
||||
<div className={styles["modal-answer"]} onClick={handleRequestCameraModalCancel}>
|
||||
<p className={styles["modal-answer-text"]}>
|
||||
{translate("/camera.modal.cancel")}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles["modal-answer"]} onClick={handleRequestCameraModalAllow}>
|
||||
<p className={styles["modal-answer-text"]}>
|
||||
{translate("/camera.modal.allow")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Модальное окно камеры */}
|
||||
{!isLoading && (
|
||||
<CameraModal
|
||||
onClose={() => console.log("close")}
|
||||
onTakePhoto={handleCameraSuccess}
|
||||
onError={handleCameraError}
|
||||
isCameraVisible={isCameraModalOpen}
|
||||
reinitializeKey={reinitializeCameraCount}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Лоадер */}
|
||||
{isLoading && (
|
||||
<Loader className={styles.loader} color={LoaderColor.Black} />
|
||||
)}
|
||||
|
||||
{/* Тост если фото плохое */}
|
||||
{toastVisible === EToastVisible.try_again && (
|
||||
<Toast classNameContainer={styles["toast-container"]} variant="error">
|
||||
<div className={styles["toast-content"]}>
|
||||
<span>{translate("/camera.bad_photo")}</span>
|
||||
<div className={styles["toast-buttons-container"]}>
|
||||
<button onClick={() => setToastVisible(null)}>
|
||||
{translate("/camera.try_again")}
|
||||
</button>
|
||||
<button className={styles.buttonUpload}>
|
||||
<input
|
||||
id="upload-input"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={onSelectFile}
|
||||
/>
|
||||
<label htmlFor="upload-input" className={styles.labelUpload}>
|
||||
{translate("/camera.upload")}
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Toast>
|
||||
)}
|
||||
|
||||
{/* Тост если нет доступа к камере */}
|
||||
{toastVisible === EToastVisible.no_access_camera && (
|
||||
<Toast classNameContainer={styles["toast-container"]} variant="error">
|
||||
<div className={styles["toast-content"]}>
|
||||
<span>{translate("/camera.no_access_camera")}</span>
|
||||
<div className={styles["toast-buttons-container"]}>
|
||||
<button onClick={handleRequestCamera}>
|
||||
{translate("/camera.give_access")}
|
||||
</button>
|
||||
<button className={styles.buttonUpload}>
|
||||
<input
|
||||
id="upload-input"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={onSelectFile}
|
||||
/>
|
||||
<label htmlFor="upload-input" className={styles.labelUpload}>
|
||||
{translate("/camera.upload")}
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Toast>
|
||||
)}
|
||||
|
||||
{/* Тост если фото можно улучшить */}
|
||||
{toastVisible === EToastVisible.try_again_or_next && (
|
||||
<Toast classNameContainer={styles["toast-container"]} variant="error">
|
||||
<div
|
||||
className={styles["toast-content"]}
|
||||
style={{ flexDirection: "column" }}
|
||||
>
|
||||
<span>{translate("/camera.do_better")}</span>
|
||||
<div className={styles["buttons-container"]}>
|
||||
<button onClick={() => setToastVisible(null)}>
|
||||
{translate("/camera.try_again")}
|
||||
</button>
|
||||
<button onClick={handleToScannedPhoto}>
|
||||
{translate("/camera.next")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Toast>
|
||||
)}
|
||||
|
||||
{/* Тост если нужно перезагрузить страницу */}
|
||||
{toastVisible === EToastVisible.reload_page && (
|
||||
<Toast classNameContainer={styles["toast-container"]} variant="error">
|
||||
<div className={styles["toast-content"]}>
|
||||
<span>{translate("/camera.reload_page")}</span>
|
||||
<button onClick={() => {
|
||||
setToastVisible(null)
|
||||
window.location.reload()
|
||||
}}>
|
||||
{translate("/camera.reload_page_button")}
|
||||
</button>
|
||||
</div>
|
||||
</Toast>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default IphoneCamera;
|
||||
@ -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<HTMLImageElement>(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<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -129,6 +138,8 @@ function ScanHand() {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isReady) return <Loader color={LoaderColor.Black} />;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
|
||||
@ -16,6 +16,7 @@ import { useAuthentication } from "@/hooks/authentication/use-authentication";
|
||||
import { ESourceAuthorization } from "@/api/resources/User";
|
||||
import { getRandomArbitrary } from "@/services/random-value";
|
||||
import { usePreloadImages } from "@/hooks/preload/images";
|
||||
import metricService, { EGoals, EMetrics } from "@/services/metric/metricService";
|
||||
|
||||
const drawElementChangeDelay = 1500;
|
||||
const startDelay = 500;
|
||||
@ -106,6 +107,10 @@ function ScannedPhoto() {
|
||||
]
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
metricService.reachGoal(EGoals.CAMERA_HAND, [EMetrics.YANDEX, EMetrics.KLAVIYO]);
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isIOSPath) {
|
||||
(async () => {
|
||||
|
||||
@ -25,6 +25,7 @@ export enum EUnleashFlags {
|
||||
"compatibilityV1EmailEnter" = "compatibilityV1EmailEnter",
|
||||
"compatibilityV2ScanHand" = "compatibilityV2ScanHand",
|
||||
"preloadImages" = "preloadImages",
|
||||
"scanHandTimeCompatibilityV2" = "scanHandTimeCompatibilityV2"
|
||||
}
|
||||
|
||||
interface IUseUnleashProps<T extends EUnleashFlags> {
|
||||
@ -54,6 +55,7 @@ interface IVariants {
|
||||
[EUnleashFlags.compatibilityV1EmailEnter]: "show" | "hide";
|
||||
[EUnleashFlags.compatibilityV2ScanHand]: "show" | "hide";
|
||||
[EUnleashFlags.preloadImages]: "yes" | "no";
|
||||
[EUnleashFlags.scanHandTimeCompatibilityV2]: "v0" | "v1";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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",
|
||||
|
||||
22
src/services/permission/permisson.ts
Normal file
22
src/services/permission/permisson.ts
Normal file
@ -0,0 +1,22 @@
|
||||
export async function checkCameraPermissionState(): Promise<PermissionState | null> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user