391 lines
15 KiB
TypeScript
391 lines
15 KiB
TypeScript
|
|
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; |