w-aura/src/components/CompatibilityV2/pages/Camera/android/index.tsx
2025-04-10 14:50:00 +00:00

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;