Merge branch 'develop' into 'main'
AW-429-comp-v1-ios-ab See merge request witapp/aura-webapp!722
This commit is contained in:
commit
a3a83980e4
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 { useEffect, useMemo } from "react";
|
||||||
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 metricService, { EGoals, EMetrics } from "@/services/metric/metricService";
|
import metricService, { EGoals, EMetrics } from "@/services/metric/metricService";
|
||||||
import Modal from "@/components/Modal";
|
import AndroidCamera from "./android";
|
||||||
import Title from "@/components/Title";
|
import IphoneCamera from "./iphone";
|
||||||
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",
|
|
||||||
}
|
|
||||||
|
|
||||||
function Camera() {
|
function Camera() {
|
||||||
const isIphoneSafari = useMemo((): boolean => {
|
const isIphoneSafari = useMemo((): boolean => {
|
||||||
@ -41,363 +16,19 @@ function Camera() {
|
|||||||
return isIOS && isSafari;
|
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(() => {
|
useEffect(() => {
|
||||||
metricService.reachGoal(EGoals.CAMERA_OPEN, [EMetrics.YANDEX, EMetrics.KLAVIYO]);
|
metricService.reachGoal(EGoals.CAMERA_OPEN, [EMetrics.YANDEX, EMetrics.KLAVIYO]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (toastVisible === EToastVisible.try_again) {
|
// if (toastVisible === EToastVisible.try_again) {
|
||||||
metricService.reachGoal(EGoals.CAMERA_ERROR, [EMetrics.YANDEX, EMetrics.KLAVIYO]);
|
// metricService.reachGoal(EGoals.CAMERA_ERROR, [EMetrics.YANDEX, EMetrics.KLAVIYO]);
|
||||||
}
|
// }
|
||||||
}, [toastVisible]);
|
// }, [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} />;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
{isIphoneSafari ? <IphoneCamera /> : <AndroidCamera />}
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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 { ICompatibilityV2FingerLocal } from "@/store/compatibilityV2";
|
||||||
import { useApi } from "@/api";
|
import { useApi } from "@/api";
|
||||||
import { DataURIToBlob } from "@/services/data";
|
import { DataURIToBlob } from "@/services/data";
|
||||||
|
import { EUnleashFlags, useUnleash } from "@/hooks/ab/unleash/useUnleash";
|
||||||
|
|
||||||
function ScanHand() {
|
function ScanHand() {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
@ -24,17 +25,25 @@ function ScanHand() {
|
|||||||
const imageRef = useRef<HTMLImageElement>(null);
|
const imageRef = useRef<HTMLImageElement>(null);
|
||||||
const gender = (useSelector(selectors.selectQuestionnaire)?.gender || "female");
|
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 = () => {
|
const handleNext = () => {
|
||||||
navigate(`${routes.client.compatibilityV2ScannedPhoto()}?fromScanHand=true`)
|
navigate(`${routes.client.compatibilityV2ScannedPhoto()}?fromScanHand=true`)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isReady) return;
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
handleHandOn()
|
handleHandOn()
|
||||||
}, 6000);
|
}, TIME_SCAN_HAND);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, [isReady]);
|
||||||
|
|
||||||
const convertImageToBase64 = (img: HTMLImageElement): Promise<string> => {
|
const convertImageToBase64 = (img: HTMLImageElement): Promise<string> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -129,6 +138,8 @@ function ScanHand() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isReady) return <Loader color={LoaderColor.Black} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.container}
|
className={styles.container}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { useAuthentication } from "@/hooks/authentication/use-authentication";
|
|||||||
import { ESourceAuthorization } from "@/api/resources/User";
|
import { ESourceAuthorization } from "@/api/resources/User";
|
||||||
import { getRandomArbitrary } from "@/services/random-value";
|
import { getRandomArbitrary } from "@/services/random-value";
|
||||||
import { usePreloadImages } from "@/hooks/preload/images";
|
import { usePreloadImages } from "@/hooks/preload/images";
|
||||||
|
import metricService, { EGoals, EMetrics } from "@/services/metric/metricService";
|
||||||
|
|
||||||
const drawElementChangeDelay = 1500;
|
const drawElementChangeDelay = 1500;
|
||||||
const startDelay = 500;
|
const startDelay = 500;
|
||||||
@ -106,6 +107,10 @@ function ScannedPhoto() {
|
|||||||
]
|
]
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
metricService.reachGoal(EGoals.CAMERA_HAND, [EMetrics.YANDEX, EMetrics.KLAVIYO]);
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isIOSPath) {
|
if (isIOSPath) {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export enum EUnleashFlags {
|
|||||||
"compatibilityV1EmailEnter" = "compatibilityV1EmailEnter",
|
"compatibilityV1EmailEnter" = "compatibilityV1EmailEnter",
|
||||||
"compatibilityV2ScanHand" = "compatibilityV2ScanHand",
|
"compatibilityV2ScanHand" = "compatibilityV2ScanHand",
|
||||||
"preloadImages" = "preloadImages",
|
"preloadImages" = "preloadImages",
|
||||||
|
"scanHandTimeCompatibilityV2" = "scanHandTimeCompatibilityV2"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IUseUnleashProps<T extends EUnleashFlags> {
|
interface IUseUnleashProps<T extends EUnleashFlags> {
|
||||||
@ -54,6 +55,7 @@ interface IVariants {
|
|||||||
[EUnleashFlags.compatibilityV1EmailEnter]: "show" | "hide";
|
[EUnleashFlags.compatibilityV1EmailEnter]: "show" | "hide";
|
||||||
[EUnleashFlags.compatibilityV2ScanHand]: "show" | "hide";
|
[EUnleashFlags.compatibilityV2ScanHand]: "show" | "hide";
|
||||||
[EUnleashFlags.preloadImages]: "yes" | "no";
|
[EUnleashFlags.preloadImages]: "yes" | "no";
|
||||||
|
[EUnleashFlags.scanHandTimeCompatibilityV2]: "v0" | "v1";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -56,6 +56,10 @@ export enum EGoals {
|
|||||||
|
|
||||||
DOWNLOAD_APP = "DownloadApp",
|
DOWNLOAD_APP = "DownloadApp",
|
||||||
|
|
||||||
|
CAMERA_HAND = "CameraHand",
|
||||||
|
SCAN_ARTIFICIAL_PHOTO = "ScanArtificialPhoto",
|
||||||
|
CAMERA_ANDROID_INSTAGRAM = "CameraAndroidInstagram",
|
||||||
|
|
||||||
// FB
|
// FB
|
||||||
LEAD = "Lead",
|
LEAD = "Lead",
|
||||||
PURCHASE = "Purchase",
|
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